// "Manage workspaces" modal for the platform Users admin page. Lets a platform // admin see/manage ALL of a user's workspace memberships: list each with an // inline role dropdown + Remove, and add the user to more workspaces via a // type-to-filter picker. Backed by /api/admin/users/:id/workspaces. import { api } from '../api.js'; import { t } from '../i18n.js'; import { showToast } from '../components/toast.js'; // Display order = least-privilege first (the default for the add row). The SET // must match the server's accepted WORKSPACE_ROLES (routes/admin.js). const WORKSPACE_ROLES = ['workspace_viewer', 'workspace_editor', 'workspace_admin']; const STAFF_ROLES = ['platform_admin', 'superadmin', 'platform_operator']; function esc(s) { return String(s ?? '').replace(/[&<>"']/g, c => ({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[c])); } function roleOptions(selected) { return WORKSPACE_ROLES.map(r => ``).join(''); } const wsLabel = w => `${w.organization_name || '—'} / ${w.name}`; // user: { id, name, email, role }; opts.onClose fires (once) if anything changed. export function openManageWorkspacesModal(user, opts = {}) { const { onClose } = opts; const isStaff = STAFF_ROLES.includes(user.role); const overlay = document.createElement('div'); overlay.className = 'modal-overlay'; overlay.innerHTML = ` `; document.body.appendChild(overlay); const listEl = overlay.querySelector('#mwsList'); const filterEl = overlay.querySelector('#mwsFilter'); const addWsEl = overlay.querySelector('#mwsAddWs'); const addRoleEl = overlay.querySelector('#mwsAddRole'); const addBtn = overlay.querySelector('#mwsAddBtn'); const errorEl = overlay.querySelector('#mwsError'); let allWs = []; // assignable workspaces (from /me) let memberships = []; // current memberships let changed = false; // refresh the table on close only if something changed function close() { overlay.remove(); document.removeEventListener('keydown', onKey); if (changed && typeof onClose === 'function') { try { onClose(); } catch (e) { console.error(e); } } } function onKey(e) { if (e.key === 'Escape') close(); } document.addEventListener('keydown', onKey); overlay.addEventListener('click', e => { if (e.target === overlay) close(); }); overlay.querySelectorAll('[data-mws-close]').forEach(b => b.addEventListener('click', close)); const showError = m => { errorEl.textContent = m; errorEl.style.display = 'block'; }; const clearError = () => { errorEl.style.display = 'none'; }; function renderAddOptions() { const memberIds = new Set(memberships.map(m => m.workspace_id)); const f = (filterEl.value || '').trim().toLowerCase(); const avail = allWs.filter(w => !memberIds.has(w.id) && (!f || wsLabel(w).toLowerCase().includes(f))); let html = ``; let curOrg = null; for (const w of avail) { const org = w.organization_name || '—'; if (org !== curOrg) { if (curOrg !== null) html += ''; html += ``; curOrg = org; } html += ``; } if (curOrg !== null) html += ''; addWsEl.innerHTML = html; } function renderList() { if (!memberships.length) { listEl.innerHTML = `

${t('manage_ws.empty')}

`; return; } listEl.innerHTML = memberships.map(m => `
${esc(m.workspace_name)}
${esc(m.organization_name || '')}
`).join(''); listEl.querySelectorAll('[data-mws-role]').forEach(sel => { sel.onchange = async () => { clearError(); try { await api.adminSetUserWorkspaceRole(user.id, sel.dataset.mwsRole, sel.value); changed = true; showToast(t('manage_ws.toast.role'), 'success'); await reload(); } catch (e) { showError(e.message); await reload(); } }; }); listEl.querySelectorAll('[data-mws-remove]').forEach(btn => { btn.onclick = async () => { clearError(); try { await api.adminRemoveUserWorkspace(user.id, btn.dataset.mwsRemove); changed = true; showToast(t('manage_ws.toast.removed'), 'success'); await reload(); } catch (e) { showError(e.message); await reload(); } }; }); } async function reload() { memberships = await api.adminGetUserWorkspaces(user.id).catch(() => memberships); renderList(); renderAddOptions(); } filterEl.addEventListener('input', renderAddOptions); addBtn.addEventListener('click', async () => { clearError(); const wsId = addWsEl.value; const role = addRoleEl.value; if (!wsId) { showError(t('manage_ws.pick_required')); return; } addBtn.disabled = true; try { await api.adminAddUserWorkspace(user.id, wsId, role); changed = true; showToast(t('manage_ws.toast.added'), 'success'); filterEl.value = ''; await reload(); } catch (e) { showError(e.message); } finally { addBtn.disabled = false; } }); // initial load (async () => { try { const [mem, me] = await Promise.all([api.adminGetUserWorkspaces(user.id), api.getMe().catch(() => ({}))]); memberships = Array.isArray(mem) ? mem : []; allWs = Array.isArray(me?.accessible_workspaces) ? me.accessible_workspaces.slice() : []; renderList(); renderAddOptions(); } catch (e) { listEl.innerHTML = `

${esc(e.message || 'Failed to load')}

`; } })(); }