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