Files
crank/apps/ui/js/workspace-setup.js
T
2026-04-03 00:24:46 +03:00

576 lines
20 KiB
JavaScript

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 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';
row.innerHTML =
'<div class="member-avatar" style="background:linear-gradient(135deg,' + workspaceFormState.selectedColor + ',#6366f1);">' +
membership.user.display_name.slice(0, 2).toUpperCase() +
'</div>' +
'<div class="member-info">' +
'<div class="member-name">' + membership.user.display_name +
(isCurrentUser ? ' <span style="font-size:11px;font-weight:400;color:var(--text-muted);">(you)</span>' : '') +
'</div>' +
'<div class="member-email">' + membership.user.email + '</div>' +
'</div>' +
'<div class="member-last-active">—</div>' +
(
canManage
? '<select class="member-role-select" data-previous="' + membership.role + '" onchange="updateRole(this, \'' + membership.user.id + '\')">' +
'<option value="owner"' + (membership.role === 'owner' ? ' selected' : '') + '>' + tKey('workspace_setup.role.owner') + '</option>' +
'<option value="admin"' + (membership.role === 'admin' ? ' selected' : '') + '>' + tKey('workspace_setup.role.admin') + '</option>' +
'<option value="operator"' + (membership.role === 'operator' ? ' selected' : '') + '>' + tKey('workspace_setup.role.operator') + '</option>' +
'<option value="viewer"' + (membership.role === 'viewer' ? ' selected' : '') + '>' + tKey('workspace_setup.role.viewer') + '</option>' +
'</select>' +
'<button class="member-remove-btn" type="button" onclick="removeMember(\'' + membership.user.id + '\', \'' + membership.user.display_name.replace(/'/g, "\\'") + '\')" title="' + tKey('workspace_setup.members.remove') + '">' +
'<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2 4h12M5 4V3a1 1 0 011-1h4a1 1 0 011 1v1M10 8v4M6 8v4"/><path d="M3 4l1 9a1 1 0 001 1h6a1 1 0 001-1l1-9"/></svg>' +
'</button>'
: '<span class="member-role-badge ' + roleClass(membership.role) + '">' + roleLabel(membership.role) + '</span>'
);
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 === 1
? tKey('workspace_setup.member_count_one')
: tfKey('workspace_setup.member_count', { 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.style.display = 'none';
return;
}
pending.style.display = '';
pending.innerHTML =
'<div style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.06em;color:var(--text-muted);margin-bottom:10px;">' +
tKey('workspace_setup.members.pending') + ' <span id="pending-count" style="background:rgba(139,148,158,0.15);border-radius:10px;padding:1px 7px;font-size:10px;">' + items.length + '</span>' +
'</div>';
items.forEach(function (record) {
var invitation = record.invitation;
var row = document.createElement('div');
row.className = 'member-row';
row.innerHTML =
'<div class="member-avatar" style="background:var(--bg-canvas);border:1.5px dashed var(--border);color:var(--text-muted);font-size:16px;">?</div>' +
'<div class="member-info">' +
'<div class="member-name">' + invitation.email + '</div>' +
'<div class="member-email">' + tfKey('workspace_setup.members.invited_pending', { date: invitation.created_at.slice(0, 10) }) + '</div>' +
'</div>' +
'<div class="member-last-active">—</div>' +
'<span class="member-role-badge" style="background:rgba(139,148,158,0.12);color:var(--text-muted);border:1px solid var(--border);">' + roleLabel(invitation.role) + '</span>' +
'<button class="member-remove-btn" type="button" onclick="revokeInviteById(\'' + invitation.id + '\')" title="' + tKey('workspace_setup.members.revoke') + '">' +
'<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><line x1="2" y1="2" x2="14" y2="14"/><line x1="14" y1="2" x2="2" y2="14"/></svg>' +
'</button>';
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').style.display = '';
document.getElementById('footer-note').style.display = '';
submit.textContent = tKey('workspace_setup.actions.create');
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 = tKey('workspace_setup.actions.save');
}
}
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 ? 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';
node.innerHTML =
'<input class="form-input" type="email" placeholder="colleague@company.com" autocomplete="off" style="flex:1;margin-bottom:0;">' +
'<select class="form-input" style="width:130px;margin-bottom:0;">' +
'<option value="admin">' + tKey('workspace_setup.role.admin') + '</option>' +
'<option value="developer" selected>' + tKey('workspace_setup.role.operator') + '</option>' +
'<option value="viewer">' + tKey('workspace_setup.role.viewer') + '</option>' +
'</select>' +
'<button class="invite-row-remove" type="button" onclick="removeInviteRow(this)" title="' + tKey('workspace_setup.members.remove') + '">' +
'<svg width="12" height="12"><use href="' + (window.APP_BASE || '') + 'icons/general/close.svg#icon"/></svg>' +
'</button>';
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) {
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 = '';
}
});