Limit community to single-user workspace
Deploy / deploy (push) Successful in 2m33s
CI / Rust Checks (push) Successful in 5m35s
CI / UI Checks (push) Successful in 5s
CI / Deployment Manifests (push) Successful in 2s
CI / Frontend E2E (push) Successful in 4m19s

This commit is contained in:
github-ops
2026-06-19 17:41:50 +00:00
parent ba29ac7b94
commit 58cfd86d90
23 changed files with 134 additions and 1696 deletions
-6
View File
@@ -32,12 +32,6 @@
<div class="ws-dropdown" id="ws-dropdown" hidden>
<div class="ws-dropdown-label" data-i18n="nav.workspaces">Your workspaces</div>
<div id="ws-dropdown-list"></div>
<div class="ws-dropdown-footer">
<button class="ws-dropdown-create" onclick="window.location.href='/workspace-setup?mode=create'">
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M8 3v10M3 8h10"/></svg>
<span data-i18n="nav.create_workspace">Create workspace</span>
</button>
</div>
</div>
</div>
-6
View File
@@ -49,12 +49,6 @@
<div class="ws-dropdown" id="ws-dropdown" hidden>
<div class="ws-dropdown-label" data-i18n="nav.workspaces">Your workspaces</div>
<div id="ws-dropdown-list"></div>
<div class="ws-dropdown-footer">
<button class="ws-dropdown-create" onclick="window.location.href='/workspace-setup?mode=create'">
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M8 3v10M3 8h10"/></svg>
<span data-i18n="nav.create_workspace">Create workspace</span>
</button>
</div>
</div>
</div>
+1 -1
View File
@@ -23,7 +23,7 @@
<div class="login-box">
<div class="login-heading" data-i18n="login.title">Sign in</div>
<div class="login-sub" data-i18n="login.subtitle">Welcome back to your workspace</div>
<div class="login-note" data-i18n="login.coming_soon">Password reset and SSO will be added later.</div>
<div class="login-note" data-i18n="login.password_only">Sign in with email and password.</div>
<div class="login-error" id="login-error" data-i18n="login.error.invalid">
Invalid email or password. Please try again.
-6
View File
@@ -30,12 +30,6 @@
<div class="ws-dropdown" id="ws-dropdown" hidden>
<div class="ws-dropdown-label" data-i18n="nav.workspaces">Your workspaces</div>
<div id="ws-dropdown-list"></div>
<div class="ws-dropdown-footer">
<button class="ws-dropdown-create" onclick="window.location.href='/workspace-setup?mode=create'">
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M8 3v10M3 8h10"/></svg>
<span data-i18n="nav.create_workspace">Create workspace</span>
</button>
</div>
</div>
</div>
-6
View File
@@ -63,12 +63,6 @@
<div class="ws-dropdown" id="ws-dropdown" hidden>
<div class="ws-dropdown-label" data-i18n="nav.workspaces">Your workspaces</div>
<div id="ws-dropdown-list"></div>
<div class="ws-dropdown-footer">
<button class="ws-dropdown-create" onclick="window.location.href='/workspace-setup?mode=create'">
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M8 3v10M3 8h10"/></svg>
<span data-i18n="nav.create_workspace">Create workspace</span>
</button>
</div>
</div>
</div>
-6
View File
@@ -30,12 +30,6 @@
<div class="ws-dropdown" id="ws-dropdown" hidden>
<div class="ws-dropdown-label" data-i18n="nav.workspaces">Your workspaces</div>
<div id="ws-dropdown-list"></div>
<div class="ws-dropdown-footer">
<button class="ws-dropdown-create" onclick="window.location.href='/workspace-setup?mode=create'">
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M8 3v10M3 8h10"/></svg>
<span data-i18n="nav.create_workspace">Create workspace</span>
</button>
</div>
</div>
</div>
-6
View File
@@ -37,12 +37,6 @@
<div class="ws-dropdown" id="ws-dropdown" hidden>
<div class="ws-dropdown-label" data-i18n="nav.workspaces">Your workspaces</div>
<div id="ws-dropdown-list"></div>
<div class="ws-dropdown-footer">
<button class="ws-dropdown-create" onclick="window.location.href='/workspace-setup?mode=create'">
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M8 3v10M3 8h10"/></svg>
<span data-i18n="nav.create_workspace">Create workspace</span>
</button>
</div>
</div>
</div>
-6
View File
@@ -30,12 +30,6 @@
<div class="ws-dropdown" id="ws-dropdown" hidden>
<div class="ws-dropdown-label" data-i18n="nav.workspaces">Your workspaces</div>
<div id="ws-dropdown-list"></div>
<div class="ws-dropdown-footer">
<button class="ws-dropdown-create" onclick="window.location.href='/workspace-setup?mode=create'">
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M8 3v10M3 8h10"/></svg>
<span data-i18n="nav.create_workspace">Create workspace</span>
</button>
</div>
</div>
</div>
+3 -168
View File
@@ -36,7 +36,7 @@
<div class="ws-setup-header">
<div class="ws-setup-title" id="page-title" data-i18n="workspace_setup.title">Workspace settings</div>
<div class="ws-setup-subtitle" id="page-subtitle" data-i18n="workspace_setup.page_subtitle">Configure your workspace identity and team members.</div>
<div class="ws-setup-subtitle" id="page-subtitle" data-i18n="workspace_setup.page_subtitle">Configure the identity of your self-hosted workspace.</div>
</div>
<!-- ── Identity ── -->
@@ -79,164 +79,6 @@
</div>
</div>
<!-- ── Members (edit mode only) ── -->
<div id="section-members" class="ws-form-section" hidden style="padding:0; overflow:hidden;">
<div style="display:flex;align-items:center;justify-content:space-between;padding:20px 24px 16px;">
<div>
<div class="ws-form-section-title" style="margin-bottom:2px;" data-i18n="workspace_setup.members.title">Team members</div>
<div style="font-size:12px;color:var(--text-muted);" id="members-count-label">4 members</div>
</div>
<button class="btn-primary" type="button" style="padding:7px 14px;font-size:13px;" onclick="toggleInviteForm()">
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor" style="margin-right:4px;"><path d="M7.75 2a.75.75 0 01.75.75V7h4.25a.75.75 0 010 1.5H8.5v4.25a.75.75 0 01-1.5 0V8.5H2.75a.75.75 0 010-1.5H7V2.75A.75.75 0 017.75 2z"/></svg>
<span data-i18n="workspace_setup.members.invite">Invite member</span>
</button>
</div>
<!-- Invite form -->
<div id="invite-form" hidden style="padding: 0 24px 16px; border-bottom: 1px solid var(--border-subtle);">
<div style="display:flex; gap:10px; align-items:flex-end; flex-wrap:wrap;">
<div class="form-group" style="flex:1; min-width:200px; margin-bottom:0;">
<label class="form-label" data-i18n="workspace_setup.members.email">Email address</label>
<input class="form-input" id="invite-email" type="email" placeholder="colleague@company.com" autocomplete="off">
</div>
<div class="form-group" style="width:140px; margin-bottom:0;">
<label class="form-label" data-i18n="workspace_setup.members.role">Role</label>
<select class="form-input" id="invite-role">
<option value="admin" data-i18n="workspace_setup.role.admin">Admin</option>
<option value="developer" selected data-i18n="workspace_setup.role.operator">Developer</option>
<option value="viewer" data-i18n="workspace_setup.role.viewer">Viewer</option>
</select>
</div>
<button class="btn-primary" type="button" style="padding:7px 14px;font-size:13px;" onclick="sendInvite()" data-i18n="workspace_setup.members.send_invite">Send invite</button>
<button class="btn-ghost-sm" type="button" style="padding:7px 12px;" onclick="toggleInviteForm()" data-i18n="workspace_setup.members.cancel">Cancel</button>
</div>
<div id="invite-success" hidden style="margin-top:10px; font-size:13px; color: #3fb950;" data-i18n="workspace_setup.members.invite_sent">✓ Invite sent.</div>
</div>
<!-- Members list -->
<div style="padding: 4px 24px 8px;" id="members-list">
<div class="member-row">
<div class="member-avatar" style="background:linear-gradient(135deg,#0d9488,#6366f1);">AT</div>
<div class="member-info">
<div class="member-name">Crank <span style="font-size:11px;font-weight:400;color:var(--text-muted);" data-i18n="workspace_setup.members.you">(you)</span></div>
<div class="member-email">operator@acme.com</div>
</div>
<div class="member-last-active">Today</div>
<span class="member-role-badge role-owner" data-i18n="workspace_setup.role.owner">Owner</span>
</div>
<div class="member-row">
<div class="member-avatar" style="background:linear-gradient(135deg,#7c3aed,#4f46e5);">BS</div>
<div class="member-info">
<div class="member-name">Blake Smith</div>
<div class="member-email">blake@acme.com</div>
</div>
<div class="member-last-active">2 days ago</div>
<select class="member-role-select" onchange="updateRole(this, 'blake@acme.com')">
<option value="admin" selected data-i18n="workspace_setup.role.admin">Admin</option>
<option value="developer" data-i18n="workspace_setup.role.operator">Developer</option>
<option value="viewer" data-i18n="workspace_setup.role.viewer">Viewer</option>
</select>
<button class="member-remove-btn" onclick="removeMember(this, 'Blake Smith')" data-i18n-title="workspace_setup.members.remove" title="Remove member">
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2 4h12M5 4V3a1 1 0 011-1h4a1 1 0 011 1v1M10 8v4M6 8v4"/><path d="M3 4l1 9a1 1 0 001 1h6a1 1 0 001-1l1-9"/></svg>
</button>
</div>
<div class="member-row">
<div class="member-avatar" style="background:linear-gradient(135deg,#d29922,#f59e0b);">CL</div>
<div class="member-info">
<div class="member-name">Carol Lane</div>
<div class="member-email">carol@acme.com</div>
</div>
<div class="member-last-active">1 week ago</div>
<select class="member-role-select" onchange="updateRole(this, 'carol@acme.com')">
<option value="admin" data-i18n="workspace_setup.role.admin">Admin</option>
<option value="developer" selected data-i18n="workspace_setup.role.operator">Developer</option>
<option value="viewer" data-i18n="workspace_setup.role.viewer">Viewer</option>
</select>
<button class="member-remove-btn" onclick="removeMember(this, 'Carol Lane')" data-i18n-title="workspace_setup.members.remove" title="Remove member">
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2 4h12M5 4V3a1 1 0 011-1h4a1 1 0 011 1v1M10 8v4M6 8v4"/><path d="M3 4l1 9a1 1 0 001 1h6a1 1 0 001-1l1-9"/></svg>
</button>
</div>
<div class="member-row">
<div class="member-avatar" style="background:linear-gradient(135deg,#0891b2,#06b6d4);">DM</div>
<div class="member-info">
<div class="member-name">Dan Morgan</div>
<div class="member-email">dan@acme.com</div>
</div>
<div class="member-last-active">Never</div>
<select class="member-role-select" onchange="updateRole(this, 'dan@acme.com')">
<option value="admin" data-i18n="workspace_setup.role.admin">Admin</option>
<option value="developer" data-i18n="workspace_setup.role.operator">Developer</option>
<option value="viewer" selected data-i18n="workspace_setup.role.viewer">Viewer</option>
</select>
<button class="member-remove-btn" onclick="removeMember(this, 'Dan Morgan')" data-i18n-title="workspace_setup.members.remove" title="Remove member">
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2 4h12M5 4V3a1 1 0 011-1h4a1 1 0 011 1v1M10 8v4M6 8v4"/><path d="M3 4l1 9a1 1 0 001 1h6a1 1 0 001-1l1-9"/></svg>
</button>
</div>
</div>
<!-- Pending invites -->
<div id="pending-invites" hidden style="border-top: 1px solid var(--border-subtle); padding: 12px 24px 16px;">
<div style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.06em;color:var(--text-muted);margin-bottom:10px;">
<span data-i18n="workspace_setup.members.pending">Pending invites</span> <span id="pending-count" style="background:rgba(139,148,158,0.15);border-radius:10px;padding:1px 7px;font-size:10px;">1</span>
</div>
<div class="member-row" id="invite-john">
<div class="member-avatar" style="background:var(--bg-canvas);border:1.5px dashed var(--border);color:var(--text-muted);font-size:16px;">?</div>
<div class="member-info">
<div class="member-name">john@startup.com</div>
<div class="member-email">Invited 3 days ago · not yet accepted</div>
</div>
<div class="member-last-active"></div>
<span class="member-role-badge" style="background:rgba(139,148,158,0.12);color:var(--text-muted);border:1px solid var(--border);" data-i18n="workspace_setup.role.operator">Developer</span>
<button class="member-remove-btn" onclick="revokeInvite(this)" data-i18n-title="workspace_setup.members.revoke" title="Revoke invite">
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><line x1="2" y1="2" x2="14" y2="14"/><line x1="14" y1="2" x2="2" y2="14"/></svg>
</button>
</div>
</div>
<!-- Roles reference -->
<div style="border-top:1px solid var(--border-subtle);padding:16px 24px 20px;">
<div style="font-size:12px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.06em;margin-bottom:14px;" data-i18n="workspace_setup.roles.title">Roles &amp; permissions</div>
<div class="role-def-row">
<span class="member-role-badge role-owner" style="min-width:80px;text-align:center;" data-i18n="workspace_setup.role.owner">Owner</span>
<div class="role-def-text" data-i18n="workspace_setup.roles.owner_body">Full workspace control, billing access, can transfer or delete the workspace.</div>
</div>
<div class="role-def-row">
<span class="member-role-badge role-admin" style="min-width:80px;text-align:center;" data-i18n="workspace_setup.role.admin">Admin</span>
<div class="role-def-text" data-i18n="workspace_setup.roles.admin_body">Manage members and roles, create and edit all operations and agents, manage API keys.</div>
</div>
<div class="role-def-row">
<span class="member-role-badge role-developer" style="min-width:80px;text-align:center;" data-i18n="workspace_setup.role.operator">Developer</span>
<div class="role-def-text" data-i18n="workspace_setup.roles.developer_body">Create and edit operations and agents. Cannot manage members or workspace settings.</div>
</div>
<div class="role-def-row">
<span class="member-role-badge role-viewer" style="min-width:80px;text-align:center;" data-i18n="workspace_setup.role.viewer">Viewer</span>
<div class="role-def-text" data-i18n="workspace_setup.roles.viewer_body">Read-only access to operations, agents, and logs.</div>
</div>
</div>
</div>
<!-- ── Invite team members (create mode only) ── -->
<div id="section-invite" class="ws-form-section" hidden>
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:4px;">
<div class="ws-form-section-title" style="margin-bottom:0;"><span data-i18n="workspace_setup.invite.title">Invite team members</span> <span style="font-size:12px;font-weight:400;color:var(--text-muted);" data-i18n="workspace_setup.optional">(optional)</span></div>
<button class="btn-ghost-sm" type="button" onclick="addInviteRow()" style="font-size:12px;padding:4px 10px;" data-i18n="workspace_setup.invite.add_person">+ Add person</button>
</div>
<div class="form-hint" style="margin-bottom:14px;" data-i18n="workspace_setup.invite.later">You can invite more people later from Workspace settings.</div>
<div class="invite-rows" id="invite-rows">
<div class="invite-row">
<input class="form-input" type="email" placeholder="colleague@company.com" autocomplete="off" style="flex:1;margin-bottom:0;">
<select class="form-input" style="width:130px;margin-bottom:0;">
<option value="admin" data-i18n="workspace_setup.role.admin">Admin</option>
<option value="developer" selected data-i18n="workspace_setup.role.operator">Developer</option>
<option value="viewer" data-i18n="workspace_setup.role.viewer">Viewer</option>
</select>
<button class="invite-row-remove" onclick="removeInviteRow(this)" data-i18n-title="workspace_setup.members.remove" title="Remove">
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><line x1="1" y1="1" x2="11" y2="11"/><line x1="11" y1="1" x2="1" y2="11"/></svg>
</button>
</div>
</div>
</div>
<!-- ── Actions ── -->
<div class="ws-setup-actions">
<a href="javascript:history.back()" class="btn-ghost-sm" style="padding:9px 18px;font-size:13px;text-decoration:none;color:var(--text-secondary);" data-i18n="btn.cancel">Cancel</a>
@@ -244,7 +86,7 @@
</div>
<div class="ws-setup-footer-note" id="footer-note" hidden>
<span data-i18n="workspace_setup.create.footer">You'll be the Owner of this workspace. You can invite additional members after creation.</span>
<span data-i18n="workspace_setup.create.footer">This Community installation uses one workspace.</span>
</div>
<!-- ── Danger zone (edit mode only) ── -->
@@ -256,17 +98,10 @@
<div class="danger-zone-action">
<div class="danger-zone-text">
<div class="danger-zone-title" data-i18n="workspace_setup.danger.export_title">Export all data</div>
<div class="danger-zone-desc" data-i18n="workspace_setup.danger.export_body">Download a JSON snapshot of workspace settings, memberships, invitations, operations, agents and platform API keys.</div>
<div class="danger-zone-desc" data-i18n="workspace_setup.danger.export_body">Download a JSON snapshot of workspace settings, operations, agents, secrets, usage data and agent access keys.</div>
</div>
<button class="btn-danger" id="export-workspace-btn" type="button" data-i18n="workspace_setup.export">Export</button>
</div>
<div class="danger-zone-action">
<div class="danger-zone-text">
<div class="danger-zone-title" data-i18n="workspace_setup.danger.delete_title">Delete workspace</div>
<div class="danger-zone-desc" data-i18n="workspace_setup.danger.delete_body">Permanently deletes all operations, keys, logs and settings. This action cannot be undone. You will be logged out immediately.</div>
</div>
<button class="btn-danger" id="delete-workspace-btn" type="button" data-i18n="workspace_setup.delete">Delete workspace</button>
</div>
</div>
</div>
-6
View File
@@ -30,12 +30,6 @@
<div class="ws-dropdown" id="ws-dropdown" hidden>
<div class="ws-dropdown-label" data-i18n="nav.workspaces">Your workspaces</div>
<div id="ws-dropdown-list"></div>
<div class="ws-dropdown-footer">
<button class="ws-dropdown-create" onclick="window.location.href='/workspace-setup?mode=create'">
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M8 3v10M3 8h10"/></svg>
<span data-i18n="nav.create_workspace">Create workspace</span>
</button>
</div>
</div>
</div>
-31
View File
@@ -162,52 +162,21 @@
body: JSON.stringify(payload),
});
},
setCurrentWorkspace: function(workspaceId) {
return request(AUTH_BASE + '/current-workspace', {
method: 'POST',
headers: headers({ 'Content-Type': 'application/json' }),
body: JSON.stringify({ workspace_id: workspaceId }),
});
},
listWorkspaces: function() {
return get('/workspaces');
},
getCapabilities: function() {
return get('/capabilities');
},
createWorkspace: function(payload) {
return post('/workspaces', payload);
},
getWorkspace: function(workspaceId) {
return get('/workspaces/' + encodeURIComponent(workspaceId));
},
updateWorkspace: function(workspaceId, payload) {
return patch('/workspaces/' + encodeURIComponent(workspaceId), payload);
},
listMemberships: function(workspaceId) {
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/members');
},
updateMembership: function(workspaceId, userId, payload) {
return patch('/workspaces/' + encodeURIComponent(workspaceId) + '/members/' + encodeURIComponent(userId), payload);
},
deleteMembership: function(workspaceId, userId) {
return del('/workspaces/' + encodeURIComponent(workspaceId) + '/members/' + encodeURIComponent(userId));
},
listInvitations: function(workspaceId) {
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/invitations');
},
createInvitation: function(workspaceId, payload) {
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/invitations', payload);
},
deleteInvitation: function(workspaceId, invitationId) {
return del('/workspaces/' + encodeURIComponent(workspaceId) + '/invitations/' + encodeURIComponent(invitationId));
},
exportWorkspace: function(workspaceId) {
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/export');
},
deleteWorkspace: function(workspaceId) {
return del('/workspaces/' + encodeURIComponent(workspaceId));
},
listOperations: function(workspaceId) {
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/operations');
},
+10 -116
View File
@@ -360,7 +360,6 @@ var TRANSLATIONS = {
'settings.title': 'Settings',
'settings.nav.workspace': 'Workspace',
'settings.nav.profile': 'Profile',
'settings.nav.members': 'Members',
'settings.nav.security': 'Security',
'settings.nav.notif': 'Notifications',
'settings.nav.danger': 'Danger zone',
@@ -459,7 +458,7 @@ var TRANSLATIONS = {
// Workspace setup
'workspace_setup.title': 'Workspace settings',
'workspace_setup.page_subtitle': 'Configure your workspace identity and team members.',
'workspace_setup.page_subtitle': 'Configure the identity of your self-hosted workspace.',
'workspace_setup.back': 'Back',
'workspace_setup.identity.title': 'Workspace identity',
'workspace_setup.identity.avatar_hint': 'Avatar is derived from your workspace name.',
@@ -470,68 +469,26 @@ var TRANSLATIONS = {
'workspace_setup.slug_hint': 'Only lowercase letters, numbers, and hyphens. Used in MCP endpoint URLs.',
'workspace_setup.description': 'Description',
'workspace_setup.description_placeholder': 'What does this workspace do?',
'workspace_setup.members.title': 'Team members',
'workspace_setup.members.invite': 'Invite member',
'workspace_setup.members.email': 'Email address',
'workspace_setup.members.role': 'Role',
'workspace_setup.members.send_invite': 'Send invite',
'workspace_setup.members.cancel': 'Cancel',
'workspace_setup.members.invite_sent': '✓ Invite sent.',
'workspace_setup.members.you': '(you)',
'workspace_setup.members.today': 'Today',
'workspace_setup.members.days_ago.one': '{count} day ago',
'workspace_setup.members.days_ago.other': '{count} days ago',
'workspace_setup.members.week_ago': '1 week ago',
'workspace_setup.members.never': 'Never',
'workspace_setup.members.pending': 'Pending invites',
'workspace_setup.members.invited_pending': 'Invited {date} · pending',
'workspace_setup.members.revoke': 'Revoke invite',
'workspace_setup.members.remove': 'Remove member',
'workspace_setup.roles.title': 'Roles & permissions',
'workspace_setup.roles.owner_body': 'Full workspace control, billing access, can transfer or delete the workspace.',
'workspace_setup.roles.admin_body': 'Manage members and roles, create and edit all operations and agents, manage API keys.',
'workspace_setup.roles.developer_body': 'Create and edit operations and agents. Cannot manage members or workspace settings.',
'workspace_setup.roles.viewer_body': 'Read-only access to operations, agents, and logs.',
'workspace_setup.invite.title': 'Invite team members',
'workspace_setup.invite.add_person': '+ Add person',
'workspace_setup.invite.later': 'You can invite more people later from Workspace settings.',
'workspace_setup.actions.create': 'Create workspace',
'workspace_setup.actions.save': 'Save changes',
'workspace_setup.create.title': 'Create a new workspace',
'workspace_setup.create.subtitle': "A workspace is a shared environment for your team's MCP operations and agents. Each workspace gets its own MCP endpoint namespace.",
'workspace_setup.create.footer': "You'll be the Owner of this workspace. You can invite additional members after creation.",
'workspace_setup.create.subtitle': 'This Community installation uses one workspace for MCP operations and agents.',
'workspace_setup.create.footer': 'This Community installation uses one workspace.',
'workspace_setup.danger.title': 'Danger zone',
'workspace_setup.danger.export_title': 'Export all data',
'workspace_setup.danger.export_body': 'Download a JSON snapshot of workspace settings, memberships, invitations, operations, agents and agent access keys.',
'workspace_setup.danger.delete_title': 'Delete workspace',
'workspace_setup.danger.delete_body': 'Permanently deletes all operations, keys, logs and settings. This action cannot be undone. You will be logged out immediately.',
'workspace_setup.danger.export_body': 'Download a JSON snapshot of workspace settings, operations, agents, secrets, usage data and agent access keys.',
'workspace_setup.export': 'Export',
'workspace_setup.delete': 'Delete workspace',
'workspace_setup.role.owner': 'Owner',
'workspace_setup.role.admin': 'Admin',
'workspace_setup.role.operator': 'Developer',
'workspace_setup.role.viewer': 'Viewer',
'workspace_setup.member_count.one': '{count} member',
'workspace_setup.member_count.other': '{count} members',
'workspace_setup.saving_create': 'Creating…',
'workspace_setup.saving_update': 'Saving…',
'workspace_setup.saved': 'Saved ✓',
'workspace_setup.save_error': 'Failed to save workspace',
'workspace_setup.save_error_title': 'Save failed',
'workspace_setup.invite_error': 'Failed to send invite',
'workspace_setup.invite_error_title': 'Invite failed',
'workspace_setup.role_error': 'Failed to update role',
'workspace_setup.role_error_title': 'Role update failed',
'workspace_setup.remove_confirm': 'Remove {name} from this workspace?',
'workspace_setup.remove_error': 'Failed to remove member',
'workspace_setup.remove_error_title': 'Member removal failed',
'workspace_setup.revoke_error': 'Failed to revoke invite',
'workspace_setup.revoke_error_title': 'Invite revoke failed',
'workspace_setup.export_error': 'Failed to export workspace',
'workspace_setup.export_error_title': 'Export failed',
'workspace_setup.delete_confirm': 'Delete this workspace? This cannot be undone.',
'workspace_setup.delete_error': 'Failed to delete workspace',
'workspace_setup.delete_error_title': 'Delete failed',
'workspace.switch_error': 'Failed to switch workspace',
'workspace.switch_error_title': 'Workspace switch failed',
'workspace_setup.custom_protocol.rest': 'REST',
@@ -843,7 +800,7 @@ var TRANSLATIONS = {
'agents.subtitle.community': 'Dedicated MCP endpoints for AI agents. Community keeps machine access on static agent keys.',
'agents.new': 'New agent',
'agents.limit.title': 'Agent limit reached',
'agents.limit.message': 'This Community build allows up to {limit} AI agent. Upgrade the edition or remove an existing agent to add another one.',
'agents.limit.message': 'This edition allows up to {limit} AI agent. Remove an existing agent to add another one.',
'agents.stats.total': 'Total agents',
'agents.stats.total_sub': 'across this workspace',
'agents.stats.published': 'Published',
@@ -949,17 +906,9 @@ var TRANSLATIONS = {
'login.email': 'Email',
'login.password': 'Password',
'login.submit': 'Sign in',
'login.sso': 'Continue with Google',
'login.email_label': 'Email address',
'login.email_placeholder': 'you@acme.com',
'login.forgot': 'Forgot password?',
'login.divider': 'or continue with',
'login.sso_short': 'Google SSO',
'login.request_prefix': "Don't have an account?",
'login.request_link': 'Request access',
'login.planned_badge': 'Planned',
'login.password_only': 'Sign in with email and password.',
'login.coming_soon': 'Password reset and SSO will be added later.',
'login.error.required': 'Please enter your email and password.',
'login.error.invalid': 'Invalid email or password. Please try again.',
'login.error.generic': 'Unable to sign in right now. Please try again.',
@@ -1325,7 +1274,6 @@ var TRANSLATIONS = {
'settings.title': 'Настройки',
'settings.nav.workspace': 'Воркспейс',
'settings.nav.profile': 'Профиль',
'settings.nav.members': 'Участники',
'settings.nav.security': 'Безопасность',
'settings.nav.notif': 'Уведомления',
'settings.nav.danger': 'Опасная зона',
@@ -1424,7 +1372,7 @@ var TRANSLATIONS = {
// Workspace setup
'workspace_setup.title': 'Настройки воркспейса',
'workspace_setup.page_subtitle': 'Настройте идентичность воркспейса и состав команды.',
'workspace_setup.page_subtitle': 'Настройте идентичность вашего self-hosted воркспейса.',
'workspace_setup.back': 'Назад',
'workspace_setup.identity.title': 'Идентичность воркспейса',
'workspace_setup.identity.avatar_hint': 'Аватар формируется из имени воркспейса.',
@@ -1435,72 +1383,26 @@ var TRANSLATIONS = {
'workspace_setup.slug_hint': 'Только строчные буквы, цифры и дефисы. Используется в URL MCP endpoint-ов.',
'workspace_setup.description': 'Описание',
'workspace_setup.description_placeholder': 'Чем занимается этот воркспейс?',
'workspace_setup.members.title': 'Участники команды',
'workspace_setup.members.invite': 'Пригласить участника',
'workspace_setup.members.email': 'Email адрес',
'workspace_setup.members.role': 'Роль',
'workspace_setup.members.send_invite': 'Отправить приглашение',
'workspace_setup.members.cancel': 'Отмена',
'workspace_setup.members.invite_sent': '✓ Приглашение отправлено.',
'workspace_setup.members.you': '(вы)',
'workspace_setup.members.today': 'Сегодня',
'workspace_setup.members.days_ago.one': '{count} день назад',
'workspace_setup.members.days_ago.few': '{count} дня назад',
'workspace_setup.members.days_ago.many': '{count} дней назад',
'workspace_setup.members.days_ago.other': '{count} дня назад',
'workspace_setup.members.week_ago': '1 неделю назад',
'workspace_setup.members.never': 'Никогда',
'workspace_setup.members.pending': 'Ожидающие приглашения',
'workspace_setup.members.invited_pending': 'Приглашен {date} · ожидает принятия',
'workspace_setup.members.revoke': 'Отозвать приглашение',
'workspace_setup.members.remove': 'Удалить участника',
'workspace_setup.roles.title': 'Роли и права',
'workspace_setup.roles.owner_body': 'Полный контроль над воркспейсом, доступ к биллингу, возможность передать или удалить воркспейс.',
'workspace_setup.roles.admin_body': 'Управление участниками и ролями, создание и редактирование всех операций и агентов, управление API-ключами.',
'workspace_setup.roles.developer_body': 'Создание и редактирование операций и агентов. Нельзя управлять участниками или настройками воркспейса.',
'workspace_setup.roles.viewer_body': 'Доступ только на чтение к операциям, агентам и логам.',
'workspace_setup.invite.title': 'Пригласить участников команды',
'workspace_setup.invite.add_person': '+ Добавить человека',
'workspace_setup.invite.later': 'Позже можно пригласить людей из настроек воркспейса.',
'workspace_setup.actions.create': 'Создать воркспейс',
'workspace_setup.actions.save': 'Сохранить изменения',
'workspace_setup.create.title': 'Создать новый воркспейс',
'workspace_setup.create.subtitle': 'Воркспейс — это общее окружение для MCP-операций и агентов вашей команды. У каждого воркспейса свой namespace MCP endpoint-ов.',
'workspace_setup.create.footer': 'Вы станете владельцем этого воркспейса. После создания можно пригласить дополнительных участников.',
'workspace_setup.create.subtitle': 'В Community используется один воркспейс для MCP-операций и агентов.',
'workspace_setup.create.footer': 'В Community используется один воркспейс.',
'workspace_setup.danger.title': 'Опасная зона',
'workspace_setup.danger.export_title': 'Экспортировать все данные',
'workspace_setup.danger.export_body': 'Скачать JSON-снимок настроек воркспейса, участников, приглашений, операций, агентов и ключей доступа агентов.',
'workspace_setup.danger.delete_title': 'Удалить воркспейс',
'workspace_setup.danger.delete_body': 'Безвозвратно удаляет все операции, ключи, логи и настройки. Действие нельзя отменить. Вы будете немедленно разлогинены.',
'workspace_setup.danger.export_body': 'Скачать JSON-снимок настроек воркспейса, операций, агентов, секретов, данных использования и ключей доступа агентов.',
'workspace_setup.export': 'Экспорт',
'workspace_setup.delete': 'Удалить воркспейс',
'workspace_setup.role.owner': 'Владелец',
'workspace_setup.role.admin': 'Администратор',
'workspace_setup.role.operator': 'Разработчик',
'workspace_setup.role.viewer': 'Наблюдатель',
'workspace_setup.member_count.one': '{count} участник',
'workspace_setup.member_count.few': '{count} участника',
'workspace_setup.member_count.many': '{count} участников',
'workspace_setup.member_count.other': '{count} участника',
'workspace_setup.saving_create': 'Создание…',
'workspace_setup.saving_update': 'Сохранение…',
'workspace_setup.saved': 'Сохранено ✓',
'workspace_setup.save_error': 'Не удалось сохранить воркспейс',
'workspace_setup.save_error_title': 'Не удалось сохранить',
'workspace_setup.invite_error': 'Не удалось отправить приглашение',
'workspace_setup.invite_error_title': 'Не удалось пригласить',
'workspace_setup.role_error': 'Не удалось обновить роль',
'workspace_setup.role_error_title': 'Не удалось обновить роль',
'workspace_setup.remove_confirm': 'Удалить {name} из этого воркспейса?',
'workspace_setup.remove_error': 'Не удалось удалить участника',
'workspace_setup.remove_error_title': 'Не удалось удалить участника',
'workspace_setup.revoke_error': 'Не удалось отозвать приглашение',
'workspace_setup.revoke_error_title': 'Не удалось отозвать приглашение',
'workspace_setup.export_error': 'Не удалось экспортировать воркспейс',
'workspace_setup.export_error_title': 'Не удалось экспортировать',
'workspace_setup.delete_confirm': 'Удалить этот воркспейс? Действие нельзя отменить.',
'workspace_setup.delete_error': 'Не удалось удалить воркспейс',
'workspace_setup.delete_error_title': 'Не удалось удалить',
'workspace.switch_error': 'Не удалось переключить воркспейс',
'workspace.switch_error_title': 'Не удалось переключить воркспейс',
'workspace_setup.custom_protocol.rest': 'REST',
@@ -1816,7 +1718,7 @@ var TRANSLATIONS = {
'agents.subtitle.community': 'Отдельные MCP endpoint-ы для AI-агентов. В Community машинный доступ держится на статических ключах агента.',
'agents.new': 'Новый агент',
'agents.limit.title': 'Достигнут лимит агентов',
'agents.limit.message': 'В этой Community-сборке доступно не более {limit} AI-агента. Чтобы добавить еще одного, удалите существующего агента или перейдите на другую редакцию.',
'agents.limit.message': 'В этой редакции доступно не более {limit} AI-агента. Чтобы добавить еще одного, удалите существующего агента.',
'agents.stats.total': 'Всего агентов',
'agents.stats.total_sub': 'в этом воркспейсе',
'agents.stats.published': 'Опубликованы',
@@ -1922,17 +1824,9 @@ var TRANSLATIONS = {
'login.email': 'Email',
'login.password': 'Пароль',
'login.submit': 'Войти',
'login.sso': 'Войти через Google',
'login.email_label': 'Email адрес',
'login.email_placeholder': 'you@acme.com',
'login.forgot': 'Забыли пароль?',
'login.divider': 'или продолжить через',
'login.sso_short': 'Google SSO',
'login.request_prefix': 'Нет аккаунта?',
'login.request_link': 'Запросить доступ',
'login.planned_badge': 'Запланировано',
'login.password_only': 'Войдите с помощью email и пароля.',
'login.coming_soon': 'Сброс пароля и SSO появятся позже.',
'login.error.required': 'Введите email и пароль.',
'login.error.invalid': 'Неверный email или пароль. Попробуйте еще раз.',
'login.error.generic': 'Сейчас не удается войти. Попробуйте еще раз.',
+1 -1
View File
@@ -56,7 +56,7 @@
limits: {
max_workspaces: 1,
max_users_per_workspace: 1,
max_agents_per_workspace: 1,
max_agents_per_workspace: null,
},
};
}
+18 -506
View File
@@ -5,16 +5,10 @@ var workspaceFormState = {
formDirty: false,
workspaceId: null,
workspaceRecord: null,
memberships: [],
invitations: [],
currentUserId: null,
};
function tKey(key) {
return typeof t === 'function' ? t(key) : key;
}
function tfKey(key, vars) {
return typeof tf === 'function' ? tf(key, vars) : key;
}
function normalizeSlug(value) {
return value
@@ -24,28 +18,6 @@ function normalizeSlug(value) {
.replace(/^-|-$/g, '');
}
function uiRoleToApi(value) {
if (value === 'developer') {
return 'operator';
}
return value;
}
function roleLabel(value) {
if (value === 'operator') return tKey('workspace_setup.role.operator');
if (value === 'owner') return tKey('workspace_setup.role.owner');
if (value === 'admin') return tKey('workspace_setup.role.admin');
if (value === 'viewer') return tKey('workspace_setup.role.viewer');
return value.charAt(0).toUpperCase() + value.slice(1);
}
function roleClass(value) {
if (value === 'operator') {
return 'role-developer';
}
return 'role-' + value;
}
function setFormDirty(value) {
workspaceFormState.formDirty = value;
}
@@ -60,35 +32,6 @@ 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 {
@@ -124,251 +67,25 @@ function applyWorkspaceRecord(record) {
setFormDirty(false);
}
function addMemberRow(container, membership) {
var row = document.createElement('div');
row.className = 'member-row';
var isCurrentUser = membership.user.id === workspaceFormState.currentUserId;
var canManage = membership.role !== 'owner';
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);
}
function renderMembers() {
var container = document.getElementById('members-list');
var label = document.getElementById('members-count-label');
if (!container || !label) {
return;
}
container.innerHTML = '';
workspaceFormState.memberships.forEach(function (membership) {
addMemberRow(container, membership);
});
label.textContent = window.tPlural(
'workspace_setup.member_count',
workspaceFormState.memberships.length,
{ count: workspaceFormState.memberships.length }
);
}
function renderInvitations() {
var pending = document.getElementById('pending-invites');
var count = document.getElementById('pending-count');
if (!pending || !count) {
return;
}
var items = workspaceFormState.invitations.filter(function (record) {
return record.invitation.status === 'pending';
});
count.textContent = String(items.length);
if (!items.length) {
pending.hidden = true;
return;
}
pending.hidden = false;
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';
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);
});
}
async function loadWorkspaceAccessData() {
if (workspaceFormState.isCreateMode || !window.CrankApi || !workspaceFormState.workspaceId) {
return;
}
if (window.CrankAuth && typeof window.CrankAuth.fetchSession === 'function') {
try {
var session = await window.CrankAuth.fetchSession(false);
workspaceFormState.currentUserId = session && session.user ? session.user.id : null;
} catch (_error) {}
}
var membershipsResponse = await window.CrankApi.listMemberships(workspaceFormState.workspaceId);
var invitationsResponse = await window.CrankApi.listInvitations(workspaceFormState.workspaceId);
workspaceFormState.memberships = membershipsResponse && membershipsResponse.items ? membershipsResponse.items : [];
workspaceFormState.invitations = invitationsResponse && invitationsResponse.items ? invitationsResponse.items : [];
renderMembers();
renderInvitations();
}
function updatePageMode() {
var params = new URLSearchParams(window.location.search);
workspaceFormState.isCreateMode = params.get('mode') === 'create';
if (workspaceFormState.isCreateMode) {
window.location.replace((window.CrankRoutes && window.CrankRoutes.workspaceSetup) || '/workspace-setup');
return;
}
var submit = document.getElementById('submit-btn');
if (workspaceFormState.isCreateMode) {
document.title = 'Crank — ' + tKey('workspace_setup.create.title');
document.getElementById('page-title').textContent = tKey('workspace_setup.create.title');
document.getElementById('page-subtitle').textContent = tKey('workspace_setup.create.subtitle');
document.getElementById('section-invite').hidden = false;
document.getElementById('footer-note').hidden = false;
submit.textContent = tKey('workspace_setup.actions.create');
submit.disabled = true;
submit.classList.add('is-disabled');
} else {
document.getElementById('section-members').hidden = false;
document.getElementById('section-danger').hidden = false;
submit.textContent = tKey('workspace_setup.actions.save');
submit.classList.remove('is-disabled');
var footerNote = document.getElementById('footer-note');
if (footerNote) {
footerNote.hidden = true;
}
var dangerSection = document.getElementById('section-danger');
if (dangerSection) {
dangerSection.hidden = false;
}
submit.textContent = tKey('workspace_setup.actions.save');
submit.classList.remove('is-disabled');
}
function onWsNameInput(value) {
@@ -410,17 +127,6 @@ function validateCreateForm() {
elements.submit.classList.toggle('is-disabled', !valid);
}
function inviteRows() {
return Array.from(document.querySelectorAll('#invite-rows .invite-row')).map(function (row) {
return {
email: row.querySelector('input').value.trim(),
role: uiRoleToApi(row.querySelector('select').value),
};
}).filter(function (row) {
return row.email;
});
}
async function submitForm() {
var payload = workspacePayload();
if (!payload.display_name || !payload.slug || !window.CrankApi) {
@@ -433,26 +139,11 @@ async function submitForm() {
submit.textContent = workspaceFormState.isCreateMode ? tKey('workspace_setup.saving_create') : tKey('workspace_setup.saving_update');
try {
var response;
if (workspaceFormState.isCreateMode) {
response = await window.CrankApi.createWorkspace(payload);
} else {
response = await window.CrankApi.updateWorkspace(workspaceFormState.workspaceId, payload);
}
var response = await window.CrankApi.updateWorkspace(workspaceFormState.workspaceId, payload);
var record = response && response.workspace ? response : { workspace: response.workspace || response };
var workspace = record.workspace;
if (workspaceFormState.isCreateMode) {
var invites = inviteRows();
for (var index = 0; index < invites.length; index += 1) {
await window.CrankApi.createInvitation(workspace.id, {
email: invites[index].email,
role: invites[index].role,
});
}
}
var workspaces = await window.refreshWorkspaces();
var mapped = workspaces.find(function (item) { return item.id === workspace.id; }) || {
id: workspace.id,
@@ -470,13 +161,7 @@ async function submitForm() {
workspaceFormState.workspaceRecord = { workspace: workspace };
setFormDirty(false);
if (workspaceFormState.isCreateMode) {
window.location.href = (window.CrankRoutes && window.CrankRoutes.workspaceSetup) || '/workspace-setup';
return;
}
applyWorkspaceRecord({ workspace: workspace });
await loadWorkspaceAccessData();
submit.textContent = tKey('workspace_setup.saved');
setTimeout(function () {
submit.textContent = originalLabel;
@@ -491,161 +176,6 @@ async function submitForm() {
}
}
function addInviteRow() {
var rows = document.getElementById('invite-rows');
var node = document.createElement('div');
node.className = 'invite-row';
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);
}
function removeInviteRow(button) {
button.closest('.invite-row').remove();
}
function toggleInviteForm() {
var form = document.getElementById('invite-form');
var success = document.getElementById('invite-success');
if (!form) {
return;
}
if (form.hidden) {
form.hidden = false;
if (success) {
success.hidden = true;
}
document.getElementById('invite-email').focus();
} else {
form.hidden = true;
}
}
async function sendInvite() {
if (!window.CrankApi || !workspaceFormState.workspaceId) {
return;
}
var email = document.getElementById('invite-email').value.trim();
var role = uiRoleToApi(document.getElementById('invite-role').value);
if (!email || email.indexOf('@') === -1) {
return;
}
try {
await window.CrankApi.createInvitation(workspaceFormState.workspaceId, { email: email, role: role });
document.getElementById('invite-email').value = '';
document.getElementById('invite-success').hidden = false;
await loadWorkspaceAccessData();
} catch (error) {
if (window.CrankUi) {
window.CrankUi.error(error.message || tKey('workspace_setup.invite_error'), tKey('workspace_setup.invite_error_title'));
}
}
}
async function updateRole(select, userId) {
if (!workspaceFormState.workspaceId || !window.CrankApi) {
return;
}
var previous = select.dataset.previous || select.value;
select.disabled = true;
try {
var response = await window.CrankApi.updateMembership(workspaceFormState.workspaceId, userId, {
role: select.value,
});
workspaceFormState.memberships = response && response.items ? response.items : workspaceFormState.memberships;
renderMembers();
} catch (error) {
select.value = previous;
if (window.CrankUi) {
window.CrankUi.error(error.message || tKey('workspace_setup.role_error'), tKey('workspace_setup.role_error_title'));
}
} finally {
select.disabled = false;
}
}
async function removeMember(userId, name) {
if (!workspaceFormState.workspaceId || !window.CrankApi) {
return;
}
if (!confirm(tfKey('workspace_setup.remove_confirm', { name: name }))) {
return;
}
try {
await window.CrankApi.deleteMembership(workspaceFormState.workspaceId, userId);
await loadWorkspaceAccessData();
} catch (error) {
if (window.CrankUi) {
window.CrankUi.error(error.message || tKey('workspace_setup.remove_error'), tKey('workspace_setup.remove_error_title'));
}
}
}
async function revokeInviteById(invitationId) {
if (!window.CrankApi || !workspaceFormState.workspaceId) {
return;
}
try {
await window.CrankApi.deleteInvitation(workspaceFormState.workspaceId, invitationId);
await loadWorkspaceAccessData();
} catch (error) {
if (window.CrankUi) {
window.CrankUi.error(error.message || tKey('workspace_setup.revoke_error'), tKey('workspace_setup.revoke_error_title'));
}
}
}
function revokeInvite(button) {
var row = button.closest('.member-row');
if (!row) {
return;
}
row.remove();
}
function downloadJsonFile(fileName, value) {
var blob = new Blob([JSON.stringify(value, null, 2)], { type: 'application/json;charset=utf-8' });
var url = URL.createObjectURL(blob);
@@ -674,32 +204,15 @@ async function exportWorkspaceSnapshot() {
}
}
async function deleteWorkspaceAction() {
if (!workspaceFormState.workspaceId || !window.CrankApi || !window.CrankAuth) {
return;
}
if (!confirm(tKey('workspace_setup.delete_confirm'))) {
return;
}
try {
await window.CrankApi.deleteWorkspace(workspaceFormState.workspaceId);
await window.CrankAuth.logout();
} catch (error) {
if (window.CrankUi) {
window.CrankUi.error(error.message || tKey('workspace_setup.delete_error'), tKey('workspace_setup.delete_error_title'));
}
}
}
async function initPage() {
updatePageMode();
document.getElementById('export-workspace-btn').addEventListener('click', exportWorkspaceSnapshot);
document.getElementById('delete-workspace-btn').addEventListener('click', deleteWorkspaceAction);
var exportButton = document.getElementById('export-workspace-btn');
if (exportButton) {
exportButton.addEventListener('click', exportWorkspaceSnapshot);
}
if (workspaceFormState.isCreateMode) {
document.getElementById('ws-name').focus();
return;
}
@@ -712,7 +225,6 @@ async function initPage() {
try {
var record = await window.CrankApi.getWorkspace(workspace.id);
applyWorkspaceRecord(record);
await loadWorkspaceAccessData();
} catch (error) {
if (window.CrankUi) {
window.CrankUi.error(error.message || 'Failed to load workspace', 'Workspace load failed');
+10 -23
View File
@@ -118,6 +118,10 @@ function renderWorkspaceList() {
var current = getCurrentWs();
updateWorkspaceHeader(current);
document.querySelectorAll('.ws-dropdown-footer').forEach(function(footer) {
footer.hidden = true;
});
var list = document.getElementById('ws-dropdown-list');
if (!list) return;
window.CrankDom.clear(list);
@@ -127,7 +131,8 @@ function renderWorkspaceList() {
var item = document.createElement('div');
item.className = 'ws-dropdown-item' + (active ? ' active' : '');
item.addEventListener('click', function() {
switchWorkspace(workspace.id);
var dd = document.getElementById('ws-dropdown');
if (dd) dd.hidden = true;
});
var dot = document.createElement('div');
@@ -232,28 +237,10 @@ function switchWorkspace(workspaceId) {
var dd = document.getElementById('ws-dropdown');
if (dd) dd.hidden = true;
if (!window.CrankApi || !window.CrankAuth || typeof window.CrankAuth.replaceSession !== 'function') {
cacheCurrentWorkspace(workspace);
renderWorkspaceList();
emitWorkspaceChange(workspace);
return;
}
return window.CrankApi
.setCurrentWorkspace(workspace.id)
.then(function(session) {
window.CrankAuth.replaceSession(session);
cacheCurrentWorkspace(workspace);
renderWorkspaceList();
emitWorkspaceChange(workspace);
return workspace;
})
.catch(function(error) {
if (window.CrankUi) {
window.CrankUi.error(error.message || tKey('workspace.switch_error'), tKey('workspace.switch_error_title'));
}
throw error;
});
cacheCurrentWorkspace(workspace);
renderWorkspaceList();
emitWorkspaceChange(workspace);
return Promise.resolve(workspace);
}
function setCurrentWorkspace(workspace) {
+3 -3
View File
@@ -8,9 +8,9 @@ test('workspace and settings pages show live session data', async ({ page }) =>
await expect(page.locator('[data-i18n="workspace_setup.defaults.title"]')).toHaveCount(0);
await expect(page.locator('#ws-protocol')).toHaveCount(0);
await expect(page.locator('#ws-timezone')).toHaveCount(0);
await expect(page.locator('#section-members')).toBeVisible();
await expect(page.locator('#members-count-label')).toBeVisible();
await expect(page.locator('#pending-invites')).toBeVisible();
await expect(page.locator('#section-members')).toHaveCount(0);
await expect(page.locator('#section-invite')).toHaveCount(0);
await expect(page.locator('#delete-workspace-btn')).toHaveCount(0);
await page.goto('/settings');
await expect(page.locator('.page-title')).toHaveText(localized('Account settings', 'Настройки аккаунта'));