var workspaceFormState = { selectedColor: '#0d9488', slugManual: false, isCreateMode: false, 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 .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 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 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 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 { 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'; 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'; 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'); } } 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.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) { return; } var submit = document.getElementById('submit-btn'); var originalLabel = submit.textContent; submit.disabled = true; 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 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: tKey('workspace_setup.role.owner'), letter: (workspace.display_name || workspace.slug).charAt(0).toUpperCase(), color: payload.settings.color || '#0d9488', status: workspace.status, settings: payload.settings, }; await window.setCurrentWorkspace(mapped); workspaceFormState.workspaceId = workspace.id; 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; submit.disabled = false; }, 1400); } catch (error) { if (window.CrankUi) { window.CrankUi.error(error.message || tKey('workspace_setup.save_error'), tKey('workspace_setup.save_error_title')); } submit.disabled = false; submit.textContent = originalLabel; } } 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); var link = document.createElement('a'); link.href = url; link.download = fileName; link.click(); URL.revokeObjectURL(url); } async function exportWorkspaceSnapshot() { if (!workspaceFormState.workspaceId || !window.CrankApi) { return; } try { var snapshot = await window.CrankApi.exportWorkspace(workspaceFormState.workspaceId); var slug = workspaceFormState.workspaceRecord && workspaceFormState.workspaceRecord.workspace ? workspaceFormState.workspaceRecord.workspace.slug : tKey('settings.nav.workspace'); downloadJsonFile(slug + '-snapshot.json', snapshot); } catch (error) { if (window.CrankUi) { window.CrankUi.error(error.message || tKey('workspace_setup.export_error'), tKey('workspace_setup.export_error_title')); } } } 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(); 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', exportWorkspaceSnapshot); document.getElementById('delete-workspace-btn').addEventListener('click', deleteWorkspaceAction); 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) { if (window.CrankUi) { window.CrankUi.error(error.message || 'Failed to load workspace', 'Workspace load failed'); } } } document.addEventListener('DOMContentLoaded', initPage); window.addEventListener('beforeunload', function (event) { if (workspaceFormState.formDirty) { event.preventDefault(); event.returnValue = ''; } });