From 73de4d402405aea337440520d9848ffc4798ace8 Mon Sep 17 00:00:00 2001 From: "a.tolmachev" Date: Sat, 2 May 2026 13:28:35 +0000 Subject: [PATCH] ui: harden workspace setup rendering --- apps/ui/js/workspace-setup.js | 266 ++++++++++++++++++++++++++++------ 1 file changed, 218 insertions(+), 48 deletions(-) diff --git a/apps/ui/js/workspace-setup.js b/apps/ui/js/workspace-setup.js index b32154d..df84efa 100644 --- a/apps/ui/js/workspace-setup.js +++ b/apps/ui/js/workspace-setup.js @@ -66,6 +66,35 @@ function formElements() { }; } +function createElement(tag, className, text) { + var node = document.createElement(tag); + if (className) { + node.className = className; + } + if (text !== undefined && text !== null) { + node.textContent = text; + } + return node; +} + +function buildIconSvg(href, width, height, viewBox, attrs) { + var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + svg.setAttribute('width', String(width)); + svg.setAttribute('height', String(height)); + if (viewBox) { + svg.setAttribute('viewBox', viewBox); + } + if (attrs) { + Object.keys(attrs).forEach(function(key) { + svg.setAttribute(key, attrs[key]); + }); + } + var use = document.createElementNS('http://www.w3.org/2000/svg', 'use'); + use.setAttribute('href', href); + svg.appendChild(use); + return svg; +} + function workspacePayload() { var elements = formElements(); return { @@ -111,30 +140,89 @@ function addMemberRow(container, membership) { row.className = 'member-row'; var isCurrentUser = membership.user.id === workspaceFormState.currentUserId; var canManage = membership.role !== 'owner'; - row.innerHTML = - '
' + - membership.user.display_name.slice(0, 2).toUpperCase() + - '
' + - '
' + - '
' + membership.user.display_name + - (isCurrentUser ? ' (you)' : '') + - '
' + - '
' + membership.user.email + '
' + - '
' + - '
' + - ( - canManage - ? '' + - '' - : '' + roleLabel(membership.role) + '' + var avatar = createElement( + 'div', + 'member-avatar', + membership.user.display_name.slice(0, 2).toUpperCase() + ); + avatar.style.background = + 'linear-gradient(135deg,' + workspaceFormState.selectedColor + ',#6366f1)'; + row.appendChild(avatar); + + var info = createElement('div', 'member-info'); + var name = createElement('div', 'member-name', membership.user.display_name); + if (isCurrentUser) { + name.appendChild(document.createTextNode(' ')); + var currentUserLabel = createElement('span', null, '(you)'); + currentUserLabel.style.fontSize = '11px'; + currentUserLabel.style.fontWeight = '400'; + currentUserLabel.style.color = 'var(--text-muted)'; + name.appendChild(currentUserLabel); + } + info.appendChild(name); + info.appendChild(createElement('div', 'member-email', membership.user.email)); + row.appendChild(info); + + row.appendChild(createElement('div', 'member-last-active', '—')); + + if (canManage) { + var select = createElement('select', 'member-role-select'); + select.dataset.previous = membership.role; + [ + ['owner', tKey('workspace_setup.role.owner')], + ['admin', tKey('workspace_setup.role.admin')], + ['operator', tKey('workspace_setup.role.operator')], + ['viewer', tKey('workspace_setup.role.viewer')] + ].forEach(function(optionData) { + var option = document.createElement('option'); + option.value = optionData[0]; + option.textContent = optionData[1]; + option.selected = membership.role === optionData[0]; + select.appendChild(option); + }); + select.addEventListener('change', function() { + updateRole(select, membership.user.id); + }); + row.appendChild(select); + + var removeButton = createElement('button', 'member-remove-btn'); + removeButton.type = 'button'; + removeButton.title = tKey('workspace_setup.members.remove'); + removeButton.appendChild( + buildIconSvg( + '', + 12, + 12, + '0 0 16 16', + { + fill: 'none', + stroke: 'currentColor', + 'stroke-width': '1.8', + 'stroke-linecap': 'round', + 'stroke-linejoin': 'round' + } + ) ); + var removeSvg = removeButton.querySelector('svg'); + var pathA = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + pathA.setAttribute('d', 'M2 4h12M5 4V3a1 1 0 011-1h4a1 1 0 011 1v1M10 8v4M6 8v4'); + var pathB = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + pathB.setAttribute('d', 'M3 4l1 9a1 1 0 001 1h6a1 1 0 001-1l1-9'); + removeSvg.appendChild(pathA); + removeSvg.appendChild(pathB); + removeButton.addEventListener('click', function() { + removeMember(membership.user.id, membership.user.display_name); + }); + row.appendChild(removeButton); + } else { + row.appendChild( + createElement( + 'span', + 'member-role-badge ' + roleClass(membership.role), + roleLabel(membership.role) + ) + ); + } container.appendChild(row); } @@ -174,26 +262,80 @@ function renderInvitations() { } pending.style.display = ''; - pending.innerHTML = - '
' + - tKey('workspace_setup.members.pending') + ' ' + items.length + '' + - '
'; + pending.innerHTML = ''; + var heading = createElement('div', null, tKey('workspace_setup.members.pending') + ' '); + heading.style.fontSize = '11px'; + heading.style.fontWeight = '600'; + heading.style.textTransform = 'uppercase'; + heading.style.letterSpacing = '0.06em'; + heading.style.color = 'var(--text-muted)'; + heading.style.marginBottom = '10px'; + var countPill = createElement('span', null, String(items.length)); + countPill.id = 'pending-count'; + countPill.style.background = 'rgba(139,148,158,0.15)'; + countPill.style.borderRadius = '10px'; + countPill.style.padding = '1px 7px'; + countPill.style.fontSize = '10px'; + heading.appendChild(countPill); + pending.appendChild(heading); items.forEach(function (record) { var invitation = record.invitation; var row = document.createElement('div'); row.className = 'member-row'; - row.innerHTML = - '
?
' + - '
' + - '
' + invitation.email + '
' + - '
' + tfKey('workspace_setup.members.invited_pending', { date: invitation.created_at.slice(0, 10) }) + '
' + - '
' + - '
' + - '' + roleLabel(invitation.role) + '' + - ''; + var inviteAvatar = createElement('div', 'member-avatar', '?'); + inviteAvatar.style.background = 'var(--bg-canvas)'; + inviteAvatar.style.border = '1.5px dashed var(--border)'; + inviteAvatar.style.color = 'var(--text-muted)'; + inviteAvatar.style.fontSize = '16px'; + row.appendChild(inviteAvatar); + + var inviteInfo = createElement('div', 'member-info'); + inviteInfo.appendChild(createElement('div', 'member-name', invitation.email)); + inviteInfo.appendChild( + createElement( + 'div', + 'member-email', + tfKey('workspace_setup.members.invited_pending', { date: invitation.created_at.slice(0, 10) }) + ) + ); + row.appendChild(inviteInfo); + row.appendChild(createElement('div', 'member-last-active', '—')); + + var roleBadge = createElement('span', 'member-role-badge', roleLabel(invitation.role)); + roleBadge.style.background = 'rgba(139,148,158,0.12)'; + roleBadge.style.color = 'var(--text-muted)'; + roleBadge.style.border = '1px solid var(--border)'; + row.appendChild(roleBadge); + + var revokeButton = createElement('button', 'member-remove-btn'); + revokeButton.type = 'button'; + revokeButton.title = tKey('workspace_setup.members.revoke'); + var revokeSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + revokeSvg.setAttribute('width', '12'); + revokeSvg.setAttribute('height', '12'); + revokeSvg.setAttribute('viewBox', '0 0 16 16'); + revokeSvg.setAttribute('fill', 'none'); + revokeSvg.setAttribute('stroke', 'currentColor'); + revokeSvg.setAttribute('stroke-width', '1.8'); + revokeSvg.setAttribute('stroke-linecap', 'round'); + var lineA = document.createElementNS('http://www.w3.org/2000/svg', 'line'); + lineA.setAttribute('x1', '2'); + lineA.setAttribute('y1', '2'); + lineA.setAttribute('x2', '14'); + lineA.setAttribute('y2', '14'); + var lineB = document.createElementNS('http://www.w3.org/2000/svg', 'line'); + lineB.setAttribute('x1', '14'); + lineB.setAttribute('y1', '2'); + lineB.setAttribute('x2', '2'); + lineB.setAttribute('y2', '14'); + revokeSvg.appendChild(lineA); + revokeSvg.appendChild(lineB); + revokeButton.appendChild(revokeSvg); + revokeButton.addEventListener('click', function() { + revokeInviteById(invitation.id); + }); + row.appendChild(revokeButton); pending.appendChild(row); }); } @@ -364,16 +506,44 @@ function addInviteRow() { var rows = document.getElementById('invite-rows'); var node = document.createElement('div'); node.className = 'invite-row'; - node.innerHTML = - '' + - '' + - ''; + var emailInput = createElement('input', 'form-input'); + emailInput.type = 'email'; + emailInput.placeholder = 'colleague@company.com'; + emailInput.autocomplete = 'off'; + emailInput.style.flex = '1'; + emailInput.style.marginBottom = '0'; + node.appendChild(emailInput); + + var roleSelect = createElement('select', 'form-input'); + roleSelect.style.width = '130px'; + roleSelect.style.marginBottom = '0'; + [ + ['admin', tKey('workspace_setup.role.admin')], + ['developer', tKey('workspace_setup.role.operator')], + ['viewer', tKey('workspace_setup.role.viewer')] + ].forEach(function(entry) { + var option = document.createElement('option'); + option.value = entry[0]; + option.textContent = entry[1]; + option.selected = entry[0] === 'developer'; + roleSelect.appendChild(option); + }); + node.appendChild(roleSelect); + + var removeButton = createElement('button', 'invite-row-remove'); + removeButton.type = 'button'; + removeButton.title = tKey('workspace_setup.members.remove'); + removeButton.appendChild( + buildIconSvg( + (window.APP_BASE || '') + 'icons/general/close.svg#icon', + 12, + 12 + ) + ); + removeButton.addEventListener('click', function() { + removeInviteRow(removeButton); + }); + node.appendChild(removeButton); rows.appendChild(node); }