Limit community to single-user workspace
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 & 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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
@@ -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': 'Сейчас не удается войти. Попробуйте еще раз.',
|
||||
|
||||
@@ -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
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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', 'Настройки аккаунта'));
|
||||
|
||||
Reference in New Issue
Block a user