460 lines
15 KiB
JavaScript
460 lines
15 KiB
JavaScript
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 =
|
|
'<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 + '</div>' +
|
|
'<div class="member-email">' + membership.user.email + '</div>' +
|
|
'</div>' +
|
|
'<div class="member-last-active">—</div>' +
|
|
'<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 + ' 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 =
|
|
'<div style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.06em;color:var(--text-muted);margin-bottom:10px;">' +
|
|
'Pending invites <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">Invited ' + invitation.created_at.slice(0, 10) + ' · pending</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="Revoke invite">' +
|
|
'<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;
|
|
}
|
|
|
|
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 =
|
|
'<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">Admin</option>' +
|
|
'<option value="developer" selected>Developer</option>' +
|
|
'<option value="viewer">Viewer</option>' +
|
|
'</select>' +
|
|
'<button class="invite-row-remove" type="button" onclick="removeInviteRow(this)" title="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) {
|
|
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 = '';
|
|
}
|
|
});
|