var workspaceFormState = { selectedColor: '#0d9488', slugManual: false, isCreateMode: false, formDirty: false, workspaceId: null, workspaceRecord: null, memberships: [], invitations: [], }; function normalizeSlug(value) { return value .toLowerCase() .replace(/[^a-z0-9-]/g, '') .replace(/-+/g, '-') .replace(/^-|-$/g, ''); } function uiRoleToApi(value) { if (value === 'developer') { return 'operator'; } return value; } function roleLabel(value) { if (value === 'operator') { return 'Developer'; } return value.charAt(0).toUpperCase() + value.slice(1); } function roleClass(value) { if (value === 'operator') { return 'role-developer'; } return 'role-' + value; } function currentLanguage() { return localStorage.getItem('crank_lang') || 'en'; } function setFormDirty(value) { workspaceFormState.formDirty = value; } function formElements() { return { name: document.getElementById('ws-name'), slug: document.getElementById('ws-slug'), desc: document.getElementById('ws-desc'), protocol: document.getElementById('ws-protocol'), timezone: document.getElementById('ws-timezone'), submit: document.getElementById('submit-btn'), avatar: document.getElementById('ws-avatar-preview'), }; } function workspacePayload() { var elements = formElements(); return { slug: elements.slug.value.trim(), display_name: elements.name.value.trim(), settings: { description: elements.desc.value.trim(), default_protocol: elements.protocol.value, timezone: elements.timezone.value, language: currentLanguage(), color: workspaceFormState.selectedColor, }, }; } function applyWorkspaceRecord(record) { var workspace = record.workspace; var settings = workspace.settings || {}; var elements = formElements(); workspaceFormState.workspaceId = workspace.id; workspaceFormState.workspaceRecord = record; workspaceFormState.selectedColor = settings.color || '#0d9488'; workspaceFormState.slugManual = true; elements.name.value = workspace.display_name || ''; elements.slug.value = workspace.slug || ''; elements.desc.value = settings.description || ''; elements.protocol.value = settings.default_protocol || 'rest'; elements.timezone.value = settings.timezone || 'UTC'; elements.avatar.textContent = (workspace.display_name || workspace.slug || '?').charAt(0).toUpperCase(); elements.avatar.style.background = workspaceFormState.selectedColor; document.querySelectorAll('.ws-color-swatch').forEach(function (swatch) { swatch.classList.toggle('active', swatch.dataset.color === workspaceFormState.selectedColor); }); setFormDirty(false); } function addMemberRow(container, membership) { var row = document.createElement('div'); row.className = 'member-row'; row.innerHTML = '
' + membership.user.display_name.slice(0, 2).toUpperCase() + '
' + '
' + '
' + membership.user.display_name + '
' + '
' + membership.user.email + '
' + '
' + '
' + '' + 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 = workspaceFormState.memberships.length + ' member' + (workspaceFormState.memberships.length === 1 ? '' : 's'); } 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.style.display = 'none'; return; } pending.style.display = ''; pending.innerHTML = '
' + 'Pending invites ' + items.length + '' + '
'; items.forEach(function (record) { var invitation = record.invitation; var row = document.createElement('div'); row.className = 'member-row'; row.innerHTML = '
?
' + '
' + '
' + invitation.email + '
' + '
Invited ' + invitation.created_at.slice(0, 10) + ' · pending
' + '
' + '
' + '' + roleLabel(invitation.role) + '' + ''; pending.appendChild(row); }); } async function loadWorkspaceAccessData() { if (workspaceFormState.isCreateMode || !window.CrankApi || !workspaceFormState.workspaceId) { return; } 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'; var submit = document.getElementById('submit-btn'); if (workspaceFormState.isCreateMode) { document.title = 'Crank — Create workspace'; document.getElementById('page-title').textContent = 'Create a new workspace'; document.getElementById('page-subtitle').textContent = 'A workspace is a shared environment for your team\'s MCP operations and agents. Each workspace gets its own MCP endpoint namespace.'; document.getElementById('section-invite').style.display = ''; document.getElementById('footer-note').style.display = ''; submit.textContent = 'Create workspace'; submit.disabled = true; submit.style.opacity = '0.5'; submit.style.cursor = 'not-allowed'; } else { document.getElementById('section-members').style.display = ''; document.getElementById('section-danger').style.display = ''; submit.textContent = 'Save changes'; } } function onWsNameInput(value) { var elements = formElements(); var normalized = value.trim(); elements.avatar.textContent = normalized ? normalized.charAt(0).toUpperCase() : '?'; if (!workspaceFormState.slugManual) { elements.slug.value = normalizeSlug(value); } if (workspaceFormState.isCreateMode) { validateCreateForm(); } setFormDirty(true); } function onWsSlugInput(value) { formElements().slug.value = normalizeSlug(value); workspaceFormState.slugManual = true; if (workspaceFormState.isCreateMode) { validateCreateForm(); } setFormDirty(true); } function pickColor(element) { workspaceFormState.selectedColor = element.dataset.color; document.querySelectorAll('.ws-color-swatch').forEach(function (swatch) { swatch.classList.remove('active'); }); element.classList.add('active'); formElements().avatar.style.background = workspaceFormState.selectedColor; setFormDirty(true); } function validateCreateForm() { var elements = formElements(); var valid = Boolean(elements.name.value.trim() && elements.slug.value.trim()); elements.submit.disabled = !valid; elements.submit.style.opacity = valid ? '1' : '0.5'; elements.submit.style.cursor = valid ? 'pointer' : 'not-allowed'; } 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) { return; } var submit = document.getElementById('submit-btn'); var originalLabel = submit.textContent; submit.disabled = true; submit.textContent = workspaceFormState.isCreateMode ? 'Creating…' : 'Saving…'; try { var response; if (workspaceFormState.isCreateMode) { response = await window.CrankApi.createWorkspace(payload); } else { 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, slug: workspace.slug, name: workspace.display_name, role: 'Owner', letter: (workspace.display_name || workspace.slug).charAt(0).toUpperCase(), color: payload.settings.color || '#0d9488', status: workspace.status, settings: payload.settings, }; window.setCurrentWorkspace(mapped); workspaceFormState.workspaceId = workspace.id; workspaceFormState.workspaceRecord = { workspace: workspace }; setFormDirty(false); if (workspaceFormState.isCreateMode) { window.location.href = (window.APP_BASE || '') + 'html/workspace-setup.html'; return; } applyWorkspaceRecord({ workspace: workspace }); await loadWorkspaceAccessData(); submit.textContent = 'Saved ✓'; setTimeout(function () { submit.textContent = originalLabel; submit.disabled = false; }, 1400); } catch (error) { alert(error.message || 'Failed to save workspace'); submit.disabled = false; submit.textContent = originalLabel; } } function addInviteRow() { var rows = document.getElementById('invite-rows'); var node = document.createElement('div'); node.className = 'invite-row'; node.innerHTML = '' + '' + ''; 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.style.display === 'none') { form.style.display = ''; if (success) { success.style.display = 'none'; } document.getElementById('invite-email').focus(); } else { form.style.display = 'none'; } } 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').style.display = ''; await loadWorkspaceAccessData(); } catch (error) { alert(error.message || 'Failed to send invite'); } } function updateRole() { alert('Role management is not available yet in the backend.'); } function removeMember() { alert('Member removal is not available yet in the backend.'); } async function revokeInviteById(invitationId) { if (!window.CrankApi || !workspaceFormState.workspaceId) { return; } try { await window.CrankApi.deleteInvitation(workspaceFormState.workspaceId, invitationId); await loadWorkspaceAccessData(); } catch (error) { alert(error.message || 'Failed to revoke invite'); } } function revokeInvite(button) { var row = button.closest('.member-row'); if (!row) { return; } row.remove(); } async function initPage() { updatePageMode(); var language = currentLanguage(); document.querySelectorAll('.lang-btn').forEach(function (button) { button.classList.toggle('active', button.dataset.lang === language); }); document.getElementById('export-workspace-btn').addEventListener('click', function () { alert('Workspace export is not available yet in the backend.'); }); document.getElementById('delete-workspace-btn').addEventListener('click', function () { alert('Workspace deletion is not available yet in the backend.'); }); if (workspaceFormState.isCreateMode) { document.getElementById('ws-name').focus(); return; } await window.whenWorkspacesReady(); var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null; if (!workspace || !window.CrankApi) { return; } try { var record = await window.CrankApi.getWorkspace(workspace.id); applyWorkspaceRecord(record); await loadWorkspaceAccessData(); } catch (error) { alert(error.message || 'Failed to load workspace'); } } document.addEventListener('DOMContentLoaded', initPage); window.addEventListener('beforeunload', function (event) { if (workspaceFormState.formDirty) { event.preventDefault(); event.returnValue = ''; } });