From 400872f8eaae601f45c4de746a5109d3b1054cdc Mon Sep 17 00:00:00 2001 From: ScreenTinker Date: Fri, 5 Jun 2026 14:40:31 -0500 Subject: [PATCH] feat(admin): Add User from the platform Users page (workspace picker) Extends the shared add-user modal (workspace-members-add-user-modal.js) with an optional picker mode instead of forking a second form: - opened with a fixed workspace (members view) -> unchanged, no picker; - opened with null (platform Users admin page) -> shows an Org/Workspace picker (type-to-filter over /me's accessible_workspaces, labelled "org / workspace") plus the role select; email/name/password+generate/ must-change/error-mapping stay shared. Role options are rendered from a single WORKSPACE_ROLES constant that mirrors the set POST /api/admin/users accepts (routes/admin.js) - so we never offer a value the endpoint 400s (the platform_operator mismatch we already hit). org_admin is intentionally NOT offered: the endpoint accepts only the three workspace roles. admin.js: "Add user" button in the page header (page is already platform_admin-gated; the endpoint additionally enforces canAdminWorkspace, which platform_admin passes everywhere). On success -> toast + refresh the user list. Reuses workspace-members.js's mapMutationError. EN i18n only. Frontend only - no backend change. Verified headless (Playwright): button opens the modal, picker lists all 45 workspaces with working filter, role options = [viewer, editor, admin], and submit created + assigned a user into the chosen workspace (test row cleaned up afterward). npm test still 12/12. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../workspace-members-add-user-modal.js | 82 ++++++++++++++++--- frontend/js/i18n/en.js | 10 +++ frontend/js/views/admin.js | 20 +++++ 3 files changed, 101 insertions(+), 11 deletions(-) diff --git a/frontend/js/components/workspace-members-add-user-modal.js b/frontend/js/components/workspace-members-add-user-modal.js index af88004..f783259 100644 --- a/frontend/js/components/workspace-members-add-user-modal.js +++ b/frontend/js/components/workspace-members-add-user-modal.js @@ -1,10 +1,13 @@ -// Add-User modal (#10). Sits next to the invite modal in the members view, but -// instead of emailing an invite it creates a user account directly with an -// admin-set password and assigns them to this workspace + role. For -// admin-provisioning on instances with no outbound email (where invites never -// deliver). Mirrors workspace-members-invite-modal.js's structure. +// Add-User modal (#10). Creates a user account directly with an admin-set +// password and assigns them to a workspace + role (admin-provisioning for +// instances with no outbound email). Two open modes, ONE shared form: +// +// openAddUserModal({ id, name }, opts) -> fixed-workspace mode (members view). +// No picker; assigns into that workspace. +// openAddUserModal(null, opts) -> picker mode (platform Users admin page). +// Shows an Org/Workspace picker; the admin +// chooses the target workspace. // -// workspace: { id, name } // opts.onSuccess: (result) => void - fires on 201 (server response body) // opts.mapError: (err) => string - translates server error to display text import { api } from '../api.js'; @@ -12,6 +15,13 @@ import { t } from '../i18n.js'; const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; +// Roles the picker offers. This is the SET POST /api/admin/users accepts +// (server: routes/admin.js WORKSPACE_ROLES) - keep them in sync so we never +// offer a value the endpoint 400s (the platform_operator dropdown/endpoint +// mismatch we already hit). Order here is display order (least-privilege first +// = the default selection); the server validates set membership, not order. +const WORKSPACE_ROLES = ['workspace_viewer', 'workspace_editor', 'workspace_admin']; + // Crockford-ish readable random password: avoids ambiguous chars (0/O, 1/l/I). function generatePassword(len = 16) { const alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789'; @@ -22,14 +32,40 @@ function generatePassword(len = 16) { return out; } +function wsLabel(w) { + return `${w.organization_name || '—'} / ${w.name}`; +} + export function openAddUserModal(workspace, opts = {}) { const { onSuccess, mapError } = opts; + // Picker mode whenever no concrete target workspace was supplied. + const pickerMode = !(workspace && workspace.id); + + const title = pickerMode + ? t('members.modal.add_user_title_generic') + : t('members.modal.add_user_title', { workspace: esc(workspace.name) }); + + const roleOptions = WORKSPACE_ROLES + .map(r => ``) + .join(''); + + // Workspace picker block — only rendered in picker mode. A filter input above + // a + + ` : ''; + const overlay = document.createElement('div'); overlay.className = 'modal-overlay'; overlay.innerHTML = ` + ${workspaceGroup}