clay-server 5.2.0-beta.8 → 5.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -4
- package/lib/clay-product-control-tools.js +1 -0
- package/lib/clay-product-control.js +14 -11
- package/lib/clay-vendor-usage.js +44 -0
- package/lib/github-star-count.js +147 -0
- package/lib/notes-checklist.js +43 -1
- package/lib/notes.js +2 -1
- package/lib/pane-pending-interactions.js +20 -0
- package/lib/pane-session-activation.js +114 -0
- package/lib/pane-session-transport.js +26 -0
- package/lib/project-session-notes.js +33 -8
- package/lib/project-sessions.js +78 -29
- package/lib/project-start-protocol.js +29 -0
- package/lib/project-vendor-login.js +2 -0
- package/lib/project.js +16 -4
- package/lib/public/app.js +41 -18
- package/lib/public/css/contextual-clay.css +2 -2
- package/lib/public/css/filebrowser.css +1 -1
- package/lib/public/css/input.css +6 -0
- package/lib/public/css/mate-loading-scene.css +21 -0
- package/lib/public/css/overlays.css +64 -256
- package/lib/public/css/project-mate-creation.css +5 -6
- package/lib/public/css/project-start.css +108 -0
- package/lib/public/css/projects-hub.css +53 -14
- package/lib/public/css/settings-shell.css +103 -0
- package/lib/public/css/sidebar.css +7 -0
- package/lib/public/css/sticky-notes.css +37 -11
- package/lib/public/css/title-bar.css +83 -0
- package/lib/public/index.html +50 -63
- package/lib/public/modules/account-transport-settings.js +8 -2
- package/lib/public/modules/admin-settings-email.js +7 -2
- package/lib/public/modules/admin-settings-invitations.js +2 -2
- package/lib/public/modules/admin-settings-projects.js +2 -2
- package/lib/public/modules/admin-settings-ui.js +20 -4
- package/lib/public/modules/admin-settings-users.js +4 -4
- package/lib/public/modules/app-connection.js +3 -0
- package/lib/public/modules/app-home-hub.js +2 -1
- package/lib/public/modules/app-message-router.js +13 -0
- package/lib/public/modules/app-messages.js +35 -7
- package/lib/public/modules/app-misc.js +3 -1
- package/lib/public/modules/app-projects.js +6 -407
- package/lib/public/modules/app-rate-limit.js +12 -1
- package/lib/public/modules/clay-owner-navigation.js +0 -3
- package/lib/public/modules/clay-studio-render.js +3 -1
- package/lib/public/modules/clay-tool-presentation.js +1 -0
- package/lib/public/modules/command-palette.js +52 -14
- package/lib/public/modules/contextual-clay-workspace.js +14 -2
- package/lib/public/modules/contextual-clay.js +19 -4
- package/lib/public/modules/contextual-mate-context.js +6 -4
- package/lib/public/modules/elicitation-form.js +5 -0
- package/lib/public/modules/github-star.js +66 -0
- package/lib/public/modules/history-replay-batch.js +8 -0
- package/lib/public/modules/home-surface-boot.js +0 -1
- package/lib/public/modules/input.js +55 -11
- package/lib/public/modules/mate-loading-scene.js +17 -0
- package/lib/public/modules/message-delivery.js +10 -2
- package/lib/public/modules/model-picker.js +2 -0
- package/lib/public/modules/pane-activation.js +214 -0
- package/lib/public/modules/pane-appearance-bridge.js +59 -0
- package/lib/public/modules/pane-binding.js +82 -0
- package/lib/public/modules/pane-bridge.js +5 -2
- package/lib/public/modules/pane-browser-bridge.js +5 -3
- package/lib/public/modules/pane-file-bridge.js +4 -0
- package/lib/public/modules/pane-issue-bridge.js +4 -1
- package/lib/public/modules/profile.js +11 -3
- package/lib/public/modules/project-mate-creation-workspace.js +13 -4
- package/lib/public/modules/project-session-visibility-settings.js +19 -16
- package/lib/public/modules/project-settings-environment.js +12 -1
- package/lib/public/modules/project-settings.js +119 -129
- package/lib/public/modules/project-start-dialog.js +130 -0
- package/lib/public/modules/project-start-folder-state.js +78 -0
- package/lib/public/modules/project-start-folder-view.js +190 -0
- package/lib/public/modules/project-start-forms.js +66 -0
- package/lib/public/modules/project-start-path.js +60 -0
- package/lib/public/modules/project-start-state.js +54 -0
- package/lib/public/modules/project-start-submit.js +67 -0
- package/lib/public/modules/projects-hub-view.js +32 -44
- package/lib/public/modules/projects-hub.js +39 -28
- package/lib/public/modules/rewind.js +5 -0
- package/lib/public/modules/server-settings.js +36 -91
- package/lib/public/modules/session-actions.js +5 -2
- package/lib/public/modules/session-context-menu.js +5 -0
- package/lib/public/modules/session-rich-drafts.js +21 -0
- package/lib/public/modules/session-switch-defer.js +67 -0
- package/lib/public/modules/settings-project-target.js +56 -0
- package/lib/public/modules/settings-select.js +1 -1
- package/lib/public/modules/settings-shell-registry.js +88 -0
- package/lib/public/modules/settings-shell-view.js +104 -0
- package/lib/public/modules/settings-shell.js +342 -0
- package/lib/public/modules/sidebar-mobile.js +8 -30
- package/lib/public/modules/sidebar-projects.js +2 -1
- package/lib/public/modules/split-group-restore.js +68 -0
- package/lib/public/modules/split-pair-ui.js +1 -0
- package/lib/public/modules/split-pane-reconciler.js +15 -0
- package/lib/public/modules/split-pane-renderer.js +87 -6
- package/lib/public/modules/split-session-boundary.js +1 -1
- package/lib/public/modules/split-view.js +51 -52
- package/lib/public/modules/sticky-notes-checklist.js +51 -6
- package/lib/public/modules/stt-controller.js +4 -1
- package/lib/public/modules/stt-recording.js +1 -1
- package/lib/public/modules/theme.js +31 -3
- package/lib/public/modules/tools.js +18 -2
- package/lib/public/modules/user-settings-integrations.js +2 -1
- package/lib/public/modules/user-settings-layout.js +12 -34
- package/lib/public/modules/user-settings-profile.js +1 -1
- package/lib/public/modules/user-settings.js +18 -85
- package/lib/public/modules/vendor-usage-format.js +93 -0
- package/lib/public/modules/vendor-usage-list.js +45 -0
- package/lib/public/modules/vendor-usage-model.js +215 -0
- package/lib/public/modules/vendor-usage-overview.js +323 -0
- package/lib/public/style.css +10 -7
- package/lib/public/welcome-art/conversation-desk.png +0 -0
- package/lib/public/welcome-art/mate-clay.png +0 -0
- package/lib/public/welcome-art/project-clay.png +0 -0
- package/lib/sdk-bridge.js +6 -1
- package/lib/sdk-message-processor.js +9 -1
- package/lib/sdk-quota-snapshot.js +20 -2
- package/lib/server-github-stars.js +39 -0
- package/lib/server-global-ws.js +21 -17
- package/lib/server.js +11 -0
- package/lib/session-notes-mcp-server.js +26 -4
- package/lib/session-notes-writes.js +88 -0
- package/lib/session-visibility.js +20 -8
- package/lib/sessions.js +27 -10
- package/lib/vendor-usage-acp-readers.js +97 -0
- package/lib/vendor-usage-capabilities.js +88 -0
- package/lib/vendor-usage-http-readers.js +214 -0
- package/lib/vendor-usage-metrics.js +47 -0
- package/lib/vendor-usage-native-readers.js +106 -0
- package/lib/vendor-usage-overview.js +406 -0
- package/lib/vendor-usage-service.js +281 -0
- package/lib/ws-schema.js +4 -2
- package/lib/yoke/adapters/claude.js +12 -0
- package/lib/yoke/adapters/codex-query-contract.js +1 -0
- package/lib/yoke/adapters/codex.js +45 -12
- package/lib/yoke/claude-hook-installer.js +1 -0
- package/lib/yoke/codex-dynamic-tool-names.js +80 -0
- package/lib/yoke/codex-model-catalog.js +37 -3
- package/lib/yoke/muse-services.js +16 -3
- package/lib/yoke/providers/claude-model-descriptions.js +22 -2
- package/lib/yoke/vendor-registry.js +6 -2
- package/package.json +5 -3
- package/lib/public/modules/server-settings-navigation.js +0 -39
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Project selection for settings. The settings target is independent of the
|
|
2
|
+
// open workspace: choosing a project here never changes currentSlug or the
|
|
3
|
+
// workspace WebSocket. Pure helpers plus a generation guard for async replies.
|
|
4
|
+
|
|
5
|
+
export function settingsProjectLabel(project) {
|
|
6
|
+
return (project && (project.title || project.project || project.name || project.slug)) || '';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Mates and the Clay project have their own settings surfaces.
|
|
10
|
+
export function isSettingsProject(project, permissions) {
|
|
11
|
+
if (!project || !project.slug || project.isMate || /^mate-/.test(project.slug)) return false;
|
|
12
|
+
return !permissions || permissions.projectSettings !== false;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Authorised projects for the signed-in user (the server-filtered project
|
|
16
|
+
// list), the open workspace first, the rest in their existing order.
|
|
17
|
+
export function listSettingsProjects(projects, currentSlug, permissions) {
|
|
18
|
+
var eligible = (Array.isArray(projects) ? projects : []).filter(function (project) { return isSettingsProject(project, permissions); });
|
|
19
|
+
var current = eligible.filter(function (project) { return project.slug === currentSlug; });
|
|
20
|
+
var others = eligible.filter(function (project) { return project.slug !== currentSlug; });
|
|
21
|
+
return current.concat(others).map(function (project, index) {
|
|
22
|
+
return { slug: project.slug, label: settingsProjectLabel(project), isCurrent: project.slug === currentSlug, project: project, first: index === 0 };
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// A requested slug must still be authorised; otherwise use the current
|
|
27
|
+
// workspace, then the first choice, then nothing.
|
|
28
|
+
export function resolveTargetSlug(requested, choices) {
|
|
29
|
+
var list = choices || [];
|
|
30
|
+
for (var i = 0; i < list.length; i++) if (list[i].slug === requested) return requested;
|
|
31
|
+
return list.length ? list[0].slug : null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function findChoice(choices, slug) {
|
|
35
|
+
var list = choices || [];
|
|
36
|
+
for (var i = 0; i < list.length; i++) if (list[i].slug === slug) return list[i];
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function optionLabel(choice) {
|
|
41
|
+
return choice.isCurrent ? choice.label + ' (open workspace)' : choice.label;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Every settings open, scope change and project switch takes a new token.
|
|
45
|
+
// Async work captures the token and ignores its result once it is stale.
|
|
46
|
+
export function createTargetBinding() {
|
|
47
|
+
var generation = 0;
|
|
48
|
+
var slug = null;
|
|
49
|
+
return {
|
|
50
|
+
bind: function (next) { generation += 1; slug = next || null; return generation; },
|
|
51
|
+
invalidate: function () { generation += 1; slug = null; return generation; },
|
|
52
|
+
isCurrent: function (token, expectedSlug) { return token === generation && (expectedSlug === undefined || expectedSlug === slug); },
|
|
53
|
+
slug: function () { return slug; },
|
|
54
|
+
generation: function () { return generation; }
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -72,7 +72,7 @@ export function enhanceSettingsSelects(root) {
|
|
|
72
72
|
button.setAttribute('aria-expanded', 'true');
|
|
73
73
|
document.addEventListener('pointerdown', outside);
|
|
74
74
|
var bounds = button.getBoundingClientRect();
|
|
75
|
-
var boundary = wrap.closest('.server-settings-content, .us-modal-content, .us-content, .ps-content, .admin-modal');
|
|
75
|
+
var boundary = wrap.closest('.settings-shell-content, .server-settings-content, .us-modal-content, .us-content, .ps-content, .admin-modal');
|
|
76
76
|
var bottom = Math.min(window.innerHeight - 8, boundary ? boundary.getBoundingClientRect().bottom - 8 : window.innerHeight - 8);
|
|
77
77
|
var above = bounds.top - Math.max(8, boundary ? boundary.getBoundingClientRect().top + 8 : 8);
|
|
78
78
|
var below = bottom - bounds.bottom - 4;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Pure routing model for the unified settings shell: scopes, pages, legacy
|
|
2
|
+
// aliases and access-based fallbacks. No DOM or store access, so every entry
|
|
3
|
+
// point resolves to one exact { scope, page, slug } destination.
|
|
4
|
+
|
|
5
|
+
export var SETTINGS_SCOPES = [
|
|
6
|
+
{ id: 'personal', label: 'Personal', hint: 'Applies to your account on every device' },
|
|
7
|
+
{ id: 'project', label: 'Project', hint: 'Applies to the selected project' },
|
|
8
|
+
{ id: 'server', label: 'Server', hint: 'Applies to this Clay server for everyone' }
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
export var PERSONAL_PAGES = [
|
|
12
|
+
{ id: 'us-account', label: 'Account' }, { id: 'us-appearance', label: 'Appearance' },
|
|
13
|
+
{ id: 'us-chat', label: 'Conversation' }, { id: 'us-notifications', label: 'Notifications' },
|
|
14
|
+
{ id: 'us-ai', label: 'AI preferences' }, { id: 'us-integrations', label: 'Integrations' }
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
export var PROJECT_PAGES = [
|
|
18
|
+
{ id: 'profile', label: 'General' }, { id: 'access', label: 'Access' }, { id: 'environment', label: 'Environment' }
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
export var SERVER_PAGES = [
|
|
22
|
+
{ id: 'overview', category: 'Service', label: 'Overview' },
|
|
23
|
+
{ id: 'network', category: 'Service', label: 'Network' },
|
|
24
|
+
{ id: 'storage', category: 'Service', label: 'Storage' },
|
|
25
|
+
{ id: 'security', category: 'Administration', label: 'Access & security' },
|
|
26
|
+
{ id: 'admin-users', category: 'Administration', label: 'Users', multiUser: true },
|
|
27
|
+
{ id: 'admin-projects', category: 'Administration', label: 'Project access', multiUser: true },
|
|
28
|
+
{ id: 'admin-smtp', category: 'Administration', label: 'Email delivery', multiUser: true },
|
|
29
|
+
{ id: 'environment', category: 'Agent configuration', label: 'Environment' },
|
|
30
|
+
{ id: 'operations', category: 'Maintenance', label: 'Operations' }
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
var PERSONAL_ALIASES = {
|
|
34
|
+
'us-mates': 'us-chat', 'us-experimental': 'us-chat', 'us-connections': 'us-integrations',
|
|
35
|
+
'us-email': 'us-integrations', 'us-logout': 'us-account'
|
|
36
|
+
};
|
|
37
|
+
var SERVER_ALIASES = { 'admin-invites': 'admin-users' };
|
|
38
|
+
|
|
39
|
+
export var DEFAULT_PERSONAL_PAGE = 'us-account';
|
|
40
|
+
|
|
41
|
+
// access: { server: boolean, multiUser: boolean, project: boolean }
|
|
42
|
+
export function defaultAccess() { return { server: false, multiUser: false, project: true }; }
|
|
43
|
+
|
|
44
|
+
// The authoritative role rule: a multi-user server needs an admin; a solo
|
|
45
|
+
// (single-user) server's owner has server access.
|
|
46
|
+
export function accessFromMe(me, permissions) {
|
|
47
|
+
var multiUser = !!(me && me.multiUser);
|
|
48
|
+
var admin = !!(me && me.user && me.user.role === 'admin');
|
|
49
|
+
return { server: !!me && (!multiUser || admin), multiUser: multiUser, project: !permissions || permissions.projectSettings !== false };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function pagesForScope(scope, access) {
|
|
53
|
+
access = access || defaultAccess();
|
|
54
|
+
if (scope === 'personal') return PERSONAL_PAGES.slice();
|
|
55
|
+
if (scope === 'project') return PROJECT_PAGES.slice();
|
|
56
|
+
if (scope === 'server') return SERVER_PAGES.filter(function (page) { return !page.multiUser || access.multiUser; });
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function scopeAvailable(scope, access) {
|
|
61
|
+
access = access || defaultAccess();
|
|
62
|
+
if (scope === 'personal') return true;
|
|
63
|
+
if (scope === 'project') return !!access.project;
|
|
64
|
+
if (scope === 'server') return !!access.server;
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function resolvePage(scope, page, access) {
|
|
69
|
+
var alias = scope === 'personal' ? PERSONAL_ALIASES[page] : scope === 'server' ? SERVER_ALIASES[page] : null;
|
|
70
|
+
var id = alias || page;
|
|
71
|
+
var pages = pagesForScope(scope, access);
|
|
72
|
+
for (var i = 0; i < pages.length; i++) if (pages[i].id === id) return id;
|
|
73
|
+
return pages.length ? pages[0].id : null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Unknown or unavailable scopes fall back to Personal, which is always allowed.
|
|
77
|
+
export function resolveRoute(route, access) {
|
|
78
|
+
route = route || {};
|
|
79
|
+
var scope = scopeAvailable(route.scope, access) ? route.scope : 'personal';
|
|
80
|
+
var requested = scope === route.scope ? route.page : null;
|
|
81
|
+
return {
|
|
82
|
+
scope: scope,
|
|
83
|
+
page: resolvePage(scope, requested, access),
|
|
84
|
+
slug: scope === 'project' ? (route.slug || null) : null,
|
|
85
|
+
requestedScope: route.scope || 'personal',
|
|
86
|
+
redirected: scope !== (route.scope || 'personal')
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// DOM rendering for the unified settings shell chrome: scope tabs, page
|
|
2
|
+
// navigation (desktop list + narrow select) and the project selector.
|
|
3
|
+
import { SETTINGS_SCOPES } from './settings-shell-registry.js';
|
|
4
|
+
import { optionLabel } from './settings-project-target.js';
|
|
5
|
+
import { enhanceSettingsSelects } from './settings-select.js';
|
|
6
|
+
|
|
7
|
+
function el(id) { return document.getElementById(id); }
|
|
8
|
+
|
|
9
|
+
export function setShellStatus(text, state) {
|
|
10
|
+
var status = el('settings-shell-status');
|
|
11
|
+
status.textContent = text || '';
|
|
12
|
+
status.hidden = !text;
|
|
13
|
+
if (state) status.dataset.state = state; else delete status.dataset.state;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function renderScopeTabs(access, activeScope, onSelect) {
|
|
17
|
+
var tabs = el('settings-scope-tabs');
|
|
18
|
+
tabs.replaceChildren();
|
|
19
|
+
SETTINGS_SCOPES.forEach(function (scope) {
|
|
20
|
+
if (scope.id === 'server' && !access.server) return;
|
|
21
|
+
if (scope.id === 'project' && !access.project) return;
|
|
22
|
+
var button = document.createElement('button');
|
|
23
|
+
button.type = 'button'; button.className = 'settings-scope-tab'; button.dataset.scope = scope.id;
|
|
24
|
+
button.textContent = scope.label; button.title = scope.hint;
|
|
25
|
+
if (scope.id === activeScope) button.setAttribute('aria-current', 'page');
|
|
26
|
+
button.classList.toggle('active', scope.id === activeScope);
|
|
27
|
+
button.onclick = function () { onSelect(scope.id); };
|
|
28
|
+
tabs.appendChild(button);
|
|
29
|
+
});
|
|
30
|
+
var active = SETTINGS_SCOPES.filter(function (scope) { return scope.id === activeScope; })[0];
|
|
31
|
+
el('settings-scope-title').textContent = active ? active.label + ' settings' : 'Settings';
|
|
32
|
+
el('settings-scope-hint').textContent = active ? active.hint : '';
|
|
33
|
+
el('settings-server-name').hidden = activeScope !== 'server';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function renderPageNav(pages, activePage, onSelect) {
|
|
37
|
+
var nav = el('settings-page-nav');
|
|
38
|
+
var select = el('settings-page-select');
|
|
39
|
+
nav.replaceChildren(); select.replaceChildren();
|
|
40
|
+
var category; var group = select;
|
|
41
|
+
pages.forEach(function (page) {
|
|
42
|
+
if (page.category && page.category !== category) {
|
|
43
|
+
category = page.category;
|
|
44
|
+
var heading = document.createElement('div'); heading.className = 'settings-nav-category'; heading.textContent = category; nav.appendChild(heading);
|
|
45
|
+
group = document.createElement('optgroup'); group.label = category; select.appendChild(group);
|
|
46
|
+
}
|
|
47
|
+
var button = document.createElement('button');
|
|
48
|
+
button.type = 'button'; button.className = 'settings-nav-item'; button.dataset.section = page.id; button.textContent = page.label;
|
|
49
|
+
button.onclick = function () { onSelect(page.id); };
|
|
50
|
+
nav.appendChild(button);
|
|
51
|
+
var option = document.createElement('option'); option.value = page.id; option.textContent = page.label; group.appendChild(option);
|
|
52
|
+
});
|
|
53
|
+
select.onchange = function () { onSelect(this.value); };
|
|
54
|
+
nav.onkeydown = function (event) {
|
|
55
|
+
if (['ArrowUp', 'ArrowDown', 'Home', 'End'].indexOf(event.key) < 0) return;
|
|
56
|
+
var items = Array.from(nav.querySelectorAll('button'));
|
|
57
|
+
var index = items.indexOf(document.activeElement);
|
|
58
|
+
if (index < 0) return;
|
|
59
|
+
event.preventDefault();
|
|
60
|
+
index = event.key === 'Home' ? 0 : event.key === 'End' ? items.length - 1 : (index + (event.key === 'ArrowDown' ? 1 : -1) + items.length) % items.length;
|
|
61
|
+
items[index].focus();
|
|
62
|
+
};
|
|
63
|
+
markActivePage(activePage);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function markActivePage(activePage) {
|
|
67
|
+
Array.from(el('settings-page-nav').querySelectorAll('button')).forEach(function (button) {
|
|
68
|
+
var active = button.dataset.section === activePage;
|
|
69
|
+
button.classList.toggle('active', active);
|
|
70
|
+
if (active) button.setAttribute('aria-current', 'page'); else button.removeAttribute('aria-current');
|
|
71
|
+
});
|
|
72
|
+
var select = el('settings-page-select');
|
|
73
|
+
if (activePage && select.value !== activePage) select.value = activePage;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// The selector is a plain select; settings-select upgrades it to the shared
|
|
77
|
+
// desktop picker and leaves the native control on narrow screens.
|
|
78
|
+
export function renderProjectSelect(choices, selectedSlug, onChange, placeholder) {
|
|
79
|
+
var wrap = el('settings-project-picker');
|
|
80
|
+
var select = el('settings-project-select');
|
|
81
|
+
select.replaceChildren();
|
|
82
|
+
choices.forEach(function (choice) {
|
|
83
|
+
var option = document.createElement('option');
|
|
84
|
+
option.value = choice.slug; option.textContent = optionLabel(choice);
|
|
85
|
+
select.appendChild(option);
|
|
86
|
+
});
|
|
87
|
+
if (!choices.length) {
|
|
88
|
+
var none = document.createElement('option'); none.value = ''; none.textContent = placeholder || 'No projects available';
|
|
89
|
+
select.appendChild(none);
|
|
90
|
+
}
|
|
91
|
+
select.disabled = !choices.length;
|
|
92
|
+
select.value = selectedSlug || (choices[0] && choices[0].slug) || '';
|
|
93
|
+
select.onchange = function () { onChange(this.value); };
|
|
94
|
+
enhanceSettingsSelects(wrap);
|
|
95
|
+
select.dispatchEvent(new Event('settings-sync'));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function showProjectPicker(visible) { el('settings-project-picker').hidden = !visible; }
|
|
99
|
+
export function selectedProjectValue() { return el('settings-project-select').value; }
|
|
100
|
+
export function syncProjectSelect(slug) {
|
|
101
|
+
var select = el('settings-project-select');
|
|
102
|
+
select.value = slug || '';
|
|
103
|
+
select.dispatchEvent(new Event('settings-sync'));
|
|
104
|
+
}
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
// Unified full-screen settings: one shell for Personal, Project and Server
|
|
2
|
+
// settings. Scope modules register a controller; the shell owns routing,
|
|
3
|
+
// access, project target binding, focus, close restoration and draft guards.
|
|
4
|
+
import { store } from './store.js';
|
|
5
|
+
import { refreshIcons } from './icons.js';
|
|
6
|
+
import { closeFileViewer } from './filebrowser.js';
|
|
7
|
+
import { holdNotesForSettings, releaseNotesForSettings } from './sticky-notes.js';
|
|
8
|
+
import { openNotesBrowser, closeNotesBrowser, isNotesBrowserOpen } from './sticky-notes-browser.js';
|
|
9
|
+
import { confirmSettingsAction, isolateSettingsDialog, trapSettingsFocus } from './server-settings-state.js';
|
|
10
|
+
import { showToast } from './utils.js';
|
|
11
|
+
import { enhanceSettingsSelects } from './settings-select.js';
|
|
12
|
+
import { accessFromMe, defaultAccess, pagesForScope, resolveRoute, scopeAvailable } from './settings-shell-registry.js';
|
|
13
|
+
import { listSettingsProjects, resolveTargetSlug, findChoice, createTargetBinding } from './settings-project-target.js';
|
|
14
|
+
import { setShellStatus, renderScopeTabs, renderPageNav, markActivePage, renderProjectSelect, showProjectPicker, syncProjectSelect } from './settings-shell-view.js';
|
|
15
|
+
|
|
16
|
+
var controllers = {};
|
|
17
|
+
var binding = createTargetBinding();
|
|
18
|
+
var root = null;
|
|
19
|
+
var lastChoices = '';
|
|
20
|
+
var OFFLINE = 'Offline. The project list and your project access may be out of date until the connection returns.';
|
|
21
|
+
var shell = { verified: false, pending: null, waiting: null, offline: false, open: false, scope: null, pages: {}, slug: null, access: defaultAccess(), returnFocus: null, restoreInert: null, notesReopen: false, token: 0, pending: null };
|
|
22
|
+
var OVERLAYS = '.admin-modal-overlay, #settings-confirm, .confirm-above-settings, .avatar-positioner-overlay, .email-setup-overlay, .settings-subview';
|
|
23
|
+
|
|
24
|
+
export function registerSettingsScope(id, controller) { controllers[id] = controller; }
|
|
25
|
+
export function isSettingsShellOpen() { return !!shell.open; }
|
|
26
|
+
export function activeSettingsScope() { return shell.scope; }
|
|
27
|
+
export function settingsTargetSlug() { return shell.slug; }
|
|
28
|
+
// Async replies compare their captured token with the live target.
|
|
29
|
+
export function settingsTargetToken() { return binding.generation(); }
|
|
30
|
+
export function isSettingsTargetCurrent(token, slug) { return shell.open && binding.isCurrent(token, slug); }
|
|
31
|
+
|
|
32
|
+
// Fail closed: until a fresh server answer is bound to this open, no
|
|
33
|
+
// administrator scope is offered, whatever an earlier session learned.
|
|
34
|
+
function provisionalAccess() {
|
|
35
|
+
var perms = store.get('permissions');
|
|
36
|
+
return Object.assign(defaultAccess(), { project: !perms || perms.projectSettings !== false });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Reads only; the caller applies the answer after validating that it still
|
|
40
|
+
// belongs to the current open and signed-in user.
|
|
41
|
+
function fetchMe() {
|
|
42
|
+
return fetch('/api/me', { credentials: 'same-origin' }).then(function (response) {
|
|
43
|
+
if (!response.ok) throw new Error('Could not verify access.');
|
|
44
|
+
return response.json();
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function setGears(open) {
|
|
49
|
+
['server-settings-btn', 'user-settings-btn'].forEach(function (id) {
|
|
50
|
+
var button = document.getElementById(id);
|
|
51
|
+
if (!button) return;
|
|
52
|
+
button.classList.toggle('active', open);
|
|
53
|
+
button.setAttribute('aria-expanded', String(open));
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function projectChoices() {
|
|
58
|
+
return listSettingsProjects(store.get('projectsHubList'), store.get('currentSlug'), store.get('permissions'));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function focusHeading(ctrl) {
|
|
62
|
+
var heading = ctrl && ctrl.pane.querySelector('.active h2');
|
|
63
|
+
if (heading) { heading.tabIndex = -1; heading.focus(); }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function setPaneVisibility(scope) {
|
|
67
|
+
Object.keys(controllers).forEach(function (id) { controllers[id].pane.classList.toggle('hidden', id !== scope); });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function controller() { return controllers[shell.scope]; }
|
|
71
|
+
|
|
72
|
+
// Ask before leaving drafts behind. kind: page | scope | project | close.
|
|
73
|
+
function guard(kind, proceed, pageId) {
|
|
74
|
+
var ctrl = controller();
|
|
75
|
+
var busy = ctrl && ctrl.busy && ctrl.busy(kind);
|
|
76
|
+
if (busy) { showToast(busy); return; }
|
|
77
|
+
var prompt = ctrl && ctrl.leavePrompt && ctrl.leavePrompt(kind, pageId);
|
|
78
|
+
if (!prompt) { proceed(); return; }
|
|
79
|
+
confirmSettingsAction(prompt.message, prompt.action, function () { if (prompt.apply) prompt.apply(); proceed(); });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function choicesSignature(choices, slug) {
|
|
83
|
+
return choices.map(function (choice) { return choice.slug + '|' + choice.label + '|' + choice.isCurrent; }).join('\n') + '#' + (slug || '');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function applyProject(ctrl, route, page) {
|
|
87
|
+
var requested = route.slug || shell.slug;
|
|
88
|
+
lastChoices = '';
|
|
89
|
+
// An unloaded list is never "no projects": wait for the authoritative list.
|
|
90
|
+
if (store.get('projectListLoaded') !== true) {
|
|
91
|
+
shell.waiting = { slug: requested };
|
|
92
|
+
binding.bind(null); shell.slug = null;
|
|
93
|
+
renderProjectSelect([], '', onProjectSelect, 'Loading projects…'); showProjectPicker(true);
|
|
94
|
+
renderPageNav(pagesForScope('project', shell.access), page, onPageSelect);
|
|
95
|
+
ctrl.setEmpty('Loading your projects…', 'loading');
|
|
96
|
+
var token = shell.token;
|
|
97
|
+
setTimeout(function () {
|
|
98
|
+
if (shell.open && token === shell.token && shell.waiting) {
|
|
99
|
+
ctrl.setEmpty('Your projects could not be loaded. Check the connection and reopen Settings to try again.', 'error');
|
|
100
|
+
setShellStatus('Your projects could not be loaded.', 'error');
|
|
101
|
+
}
|
|
102
|
+
}, 10000);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
shell.waiting = null;
|
|
106
|
+
var choices = projectChoices();
|
|
107
|
+
var slug = resolveTargetSlug(requested, choices);
|
|
108
|
+
var lost = !!requested && slug !== requested;
|
|
109
|
+
renderProjectSelect(choices, slug, onProjectSelect);
|
|
110
|
+
lastChoices = choicesSignature(choices, slug);
|
|
111
|
+
showProjectPicker(true);
|
|
112
|
+
binding.bind(slug);
|
|
113
|
+
shell.slug = slug;
|
|
114
|
+
if (!slug) {
|
|
115
|
+
ctrl.setEmpty('You do not have a project to configure yet. Open or create a project, then return here.', 'empty');
|
|
116
|
+
renderPageNav(pagesForScope('project', shell.access), page, onPageSelect);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
ctrl.setEmpty(null);
|
|
120
|
+
ctrl.retarget(slug, findChoice(choices, slug).project, binding.generation());
|
|
121
|
+
setShellStatus(lost ? 'That project is not available for your account. Showing ' + findChoice(choices, slug).label + '.' : '', lost ? 'error' : undefined);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function redirectNote(resolved) {
|
|
125
|
+
if (!resolved.redirected) return '';
|
|
126
|
+
return resolved.requestedScope === 'server' ? 'Server settings require an administrator. Your personal settings are shown.' : 'That settings area is not available for your account.';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function currentRoute() {
|
|
130
|
+
return { scope: shell.scope, page: shell.pages[shell.scope], slug: shell.slug || (shell.waiting && shell.waiting.slug) || null };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function applyRoute(route, focus) {
|
|
134
|
+
var resolved = resolveRoute(route, shell.access);
|
|
135
|
+
var ctrl = controllers[resolved.scope];
|
|
136
|
+
if (!ctrl) return;
|
|
137
|
+
if (shell.scope && shell.scope !== resolved.scope && controllers[shell.scope].leave) controllers[shell.scope].leave();
|
|
138
|
+
var entering = shell.scope !== resolved.scope;
|
|
139
|
+
shell.scope = resolved.scope;
|
|
140
|
+
binding.bind(null);
|
|
141
|
+
setPaneVisibility(resolved.scope);
|
|
142
|
+
renderScopeTabs(shell.access, resolved.scope, onScopeSelect);
|
|
143
|
+
setShellStatus(redirectNote(resolved), resolved.redirected ? 'error' : undefined);
|
|
144
|
+
var ctx = { access: shell.access, scope: resolved.scope };
|
|
145
|
+
if (entering && ctrl.enter) ctrl.enter(ctx);
|
|
146
|
+
if (resolved.scope === 'project') applyProject(ctrl, resolved, resolved.page);
|
|
147
|
+
else { showProjectPicker(false); shell.slug = null; shell.waiting = null; }
|
|
148
|
+
var page = resolved.page;
|
|
149
|
+
if (resolved.scope !== 'project' || shell.slug) renderPageNav(pagesForScope(resolved.scope, shell.access), page, onPageSelect);
|
|
150
|
+
shell.pages[resolved.scope] = page;
|
|
151
|
+
if (resolved.scope !== 'project' || shell.slug) ctrl.activate(page, ctx);
|
|
152
|
+
markActivePage(page);
|
|
153
|
+
store.set({ settingsLastRoute: { scope: resolved.scope, page: page, slug: shell.slug } });
|
|
154
|
+
enhanceSettingsSelects(root); refreshIcons(root);
|
|
155
|
+
if (focus) focusHeading(ctrl);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function onScopeSelect(scope) {
|
|
159
|
+
if (scope === shell.scope) return;
|
|
160
|
+
guard('scope', function () { applyRoute({ scope: scope, page: shell.pages[scope], slug: scope === 'project' ? currentRoute().slug : null }, true); });
|
|
161
|
+
}
|
|
162
|
+
function onPageSelect(page) {
|
|
163
|
+
if (page === shell.pages[shell.scope]) { focusHeading(controller()); return; }
|
|
164
|
+
guard('page', function () {
|
|
165
|
+
var ctrl = controller();
|
|
166
|
+
shell.pages[shell.scope] = page; ctrl.activate(page, { access: shell.access, scope: shell.scope });
|
|
167
|
+
markActivePage(page); store.set({ settingsLastRoute: { scope: shell.scope, page: page, slug: shell.slug } });
|
|
168
|
+
focusHeading(ctrl);
|
|
169
|
+
}, shell.pages[shell.scope]);
|
|
170
|
+
}
|
|
171
|
+
// A project switch retargets only the settings view, never the workspace.
|
|
172
|
+
function onProjectSelect(slug) {
|
|
173
|
+
syncProjectSelect(shell.slug);
|
|
174
|
+
if (!slug || slug === shell.slug) return;
|
|
175
|
+
guard('project', function () { applyRoute({ scope: 'project', page: shell.pages.project, slug: slug }, false); });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function navigateSettings(route) {
|
|
179
|
+
// Until access is verified, remember the latest request and apply it afterwards.
|
|
180
|
+
if (!shell.verified) { shell.pending = route; return; }
|
|
181
|
+
var resolved = resolveRoute(route, shell.access);
|
|
182
|
+
if (resolved.scope === shell.scope && resolved.page === shell.pages[shell.scope] && (resolved.scope !== 'project' || !route.slug || route.slug === shell.slug)) {
|
|
183
|
+
setShellStatus(redirectNote(resolved), resolved.redirected ? 'error' : undefined);
|
|
184
|
+
focusHeading(controller()); return;
|
|
185
|
+
}
|
|
186
|
+
guard(resolved.scope === shell.scope ? (route.slug && route.slug !== shell.slug ? 'project' : 'page') : 'scope', function () { applyRoute(route, true); }, shell.pages[shell.scope]);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Entry for every legacy opener. Returns once the exact destination is shown.
|
|
190
|
+
export function openSettings(route) {
|
|
191
|
+
if (!root) return Promise.resolve();
|
|
192
|
+
route = route || store.get('settingsLastRoute') || { scope: 'personal' };
|
|
193
|
+
if (shell.open) { navigateSettings(route); return Promise.resolve(); }
|
|
194
|
+
shell.open = true;
|
|
195
|
+
shell.token += 1;
|
|
196
|
+
var token = shell.token;
|
|
197
|
+
shell.returnFocus = document.activeElement;
|
|
198
|
+
shell.scope = null; shell.pages = {}; shell.slug = null;
|
|
199
|
+
closeFileViewer();
|
|
200
|
+
shell.notesReopen = isNotesBrowserOpen();
|
|
201
|
+
if (shell.notesReopen) closeNotesBrowser();
|
|
202
|
+
holdNotesForSettings();
|
|
203
|
+
root.classList.remove('hidden');
|
|
204
|
+
shell.restoreInert = isolateSettingsDialog(root);
|
|
205
|
+
setGears(true);
|
|
206
|
+
store.set({ settingsOpen: true });
|
|
207
|
+
var userAtOpen = store.get('myUserId');
|
|
208
|
+
shell.verified = false; shell.pending = null;
|
|
209
|
+
shell.access = provisionalAccess();
|
|
210
|
+
setShellStatus('');
|
|
211
|
+
if (route.scope !== 'server') applyRoute(route, true);
|
|
212
|
+
else { renderScopeTabs(shell.access, 'personal', onScopeSelect); setShellStatus('Checking administrator access…'); }
|
|
213
|
+
function current(me) {
|
|
214
|
+
// Stale after close/reopen, or a different signed-in user: ignore entirely.
|
|
215
|
+
return shell.open && token === shell.token && store.get('myUserId') === userAtOpen && (!me || !me.user || !userAtOpen || me.user.id === userAtOpen);
|
|
216
|
+
}
|
|
217
|
+
return fetchMe().then(function (me) {
|
|
218
|
+
if (!current(me)) return;
|
|
219
|
+
var access = accessFromMe(me, me.permissions || null);
|
|
220
|
+
store.set({ adminMe: me, permissions: me.permissions || null, projectsAccessLoaded: true });
|
|
221
|
+
var needsRoute = !shell.scope;
|
|
222
|
+
var changed = access.server !== shell.access.server || access.multiUser !== shell.access.multiUser || access.project !== shell.access.project;
|
|
223
|
+
shell.access = access;
|
|
224
|
+
shell.verified = true;
|
|
225
|
+
var pending = shell.pending; shell.pending = null;
|
|
226
|
+
if (pending) applyRoute(pending, true);
|
|
227
|
+
else if (needsRoute) applyRoute(route, true);
|
|
228
|
+
else if (changed) applyRoute(scopeAvailable(shell.scope, access) ? currentRoute() : { scope: 'personal' }, false);
|
|
229
|
+
}, function () {
|
|
230
|
+
if (!current()) return;
|
|
231
|
+
// Verification failed: Server stays unavailable and the view is rebuilt without it.
|
|
232
|
+
shell.access = Object.assign({}, shell.access, { server: false });
|
|
233
|
+
shell.verified = true;
|
|
234
|
+
var queued = shell.pending; shell.pending = null;
|
|
235
|
+
applyRoute(queued || (shell.scope && shell.scope !== 'server' ? currentRoute() : { scope: 'personal' }), false);
|
|
236
|
+
setShellStatus('Could not verify administrator access, so Server settings are unavailable. Close and reopen Settings to retry.', 'error');
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function closePrompt() {
|
|
241
|
+
var withDrafts = Object.keys(controllers).filter(function (id) { return controllers[id].leavePrompt && controllers[id].leavePrompt('close'); });
|
|
242
|
+
if (!withDrafts.length) return null;
|
|
243
|
+
if (withDrafts.length === 1 && withDrafts[0] === shell.scope) return controllers[shell.scope].leavePrompt('close');
|
|
244
|
+
return { message: 'You have unsaved changes in another settings area. Closing keeps these drafts in this tab, but reloading the page will lose them.', action: 'Close with drafts' };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function closeSettings(opts) {
|
|
248
|
+
opts = opts || {};
|
|
249
|
+
if (!shell.open) return;
|
|
250
|
+
if (opts.onlyScope && shell.scope !== opts.onlyScope) return;
|
|
251
|
+
function finish() {
|
|
252
|
+
shell.token += 1; binding.invalidate();
|
|
253
|
+
var ctrl = controller();
|
|
254
|
+
if (ctrl && ctrl.leave) ctrl.leave();
|
|
255
|
+
Object.keys(controllers).forEach(function (id) { if (controllers[id].close) controllers[id].close(); });
|
|
256
|
+
root.classList.add('hidden');
|
|
257
|
+
if (shell.restoreInert) { shell.restoreInert(); shell.restoreInert = null; }
|
|
258
|
+
setGears(false);
|
|
259
|
+
shell.open = false; shell.scope = null; shell.slug = null; shell.waiting = null; shell.offline = false; shell.verified = false; shell.pending = null;
|
|
260
|
+
store.set({ settingsOpen: false });
|
|
261
|
+
releaseNotesForSettings();
|
|
262
|
+
if (!opts.cleanup) {
|
|
263
|
+
if (shell.notesReopen) openNotesBrowser();
|
|
264
|
+
var focus = shell.returnFocus;
|
|
265
|
+
if (!focus || !focus.isConnected || focus === document.body) focus = document.getElementById('server-settings-btn');
|
|
266
|
+
if (focus) focus.focus();
|
|
267
|
+
}
|
|
268
|
+
shell.notesReopen = false; shell.returnFocus = null;
|
|
269
|
+
}
|
|
270
|
+
if (opts.cleanup || opts.force) { finish(); return; }
|
|
271
|
+
var ctrl = controller();
|
|
272
|
+
var busy = ctrl && ctrl.busy && ctrl.busy('close');
|
|
273
|
+
if (busy) { showToast(busy); return; }
|
|
274
|
+
var prompt = closePrompt();
|
|
275
|
+
if (!prompt) { finish(); return; }
|
|
276
|
+
confirmSettingsAction(prompt.message, prompt.action, function () { if (prompt.apply) prompt.apply(); finish(); });
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Authorisation, ordering and the open project can change while settings
|
|
280
|
+
// are open; an unavailable target is cleared rather than silently replaced.
|
|
281
|
+
function refreshLive() {
|
|
282
|
+
if (!shell.open || !shell.scope) return;
|
|
283
|
+
var perms = store.get('permissions');
|
|
284
|
+
var project = !perms || perms.projectSettings !== false;
|
|
285
|
+
if (project !== shell.access.project) {
|
|
286
|
+
shell.access = Object.assign({}, shell.access, { project: project });
|
|
287
|
+
if (shell.scope === 'project' && !project) { applyRoute({ scope: 'personal' }, false); setShellStatus('Project settings are no longer permitted for your account.', 'error'); return; }
|
|
288
|
+
renderScopeTabs(shell.access, shell.scope, onScopeSelect);
|
|
289
|
+
}
|
|
290
|
+
if (shell.scope !== 'project') return;
|
|
291
|
+
syncConnectivity();
|
|
292
|
+
if (shell.waiting && store.get('projectListLoaded') === true) {
|
|
293
|
+
var waiting = shell.waiting; shell.waiting = null;
|
|
294
|
+
applyRoute({ scope: 'project', page: shell.pages.project, slug: waiting.slug }, false);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
if (shell.waiting) return;
|
|
298
|
+
var choices = projectChoices();
|
|
299
|
+
var ctrl = controller();
|
|
300
|
+
var target = findChoice(choices, shell.slug);
|
|
301
|
+
var signature = choicesSignature(choices, target ? shell.slug : '');
|
|
302
|
+
if (signature !== lastChoices) { lastChoices = signature; renderProjectSelect(choices, target ? shell.slug : '', onProjectSelect); }
|
|
303
|
+
if (shell.slug && !target) {
|
|
304
|
+
var lostSlug = shell.slug;
|
|
305
|
+
binding.invalidate(); shell.slug = null;
|
|
306
|
+
ctrl.clearTarget(lostSlug);
|
|
307
|
+
ctrl.setEmpty('That project is no longer available for your account. Choose another project.', 'lost');
|
|
308
|
+
syncProjectSelect('');
|
|
309
|
+
setShellStatus('The selected project is no longer available. Unsaved edits for it were not applied.', 'error');
|
|
310
|
+
} else if (target) ctrl.refreshTarget(shell.slug, target.project);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function syncConnectivity() {
|
|
314
|
+
var offline = store.get('connected') === false;
|
|
315
|
+
if (offline && !shell.offline) { shell.offline = true; setShellStatus(OFFLINE, 'error'); }
|
|
316
|
+
else if (!offline && shell.offline) { shell.offline = false; setShellStatus(''); }
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function initSettingsShell() {
|
|
320
|
+
root = document.getElementById('settings-shell');
|
|
321
|
+
if (!root) return;
|
|
322
|
+
var content = document.getElementById('settings-shell-content');
|
|
323
|
+
['user-settings', 'project-settings', 'server-settings'].forEach(function (id) {
|
|
324
|
+
var pane = document.getElementById(id);
|
|
325
|
+
if (pane) { pane.classList.add('settings-pane', 'hidden'); content.appendChild(pane); }
|
|
326
|
+
});
|
|
327
|
+
document.getElementById('settings-shell-close').onclick = function () { closeSettings(); };
|
|
328
|
+
var server = document.getElementById('server-settings-btn');
|
|
329
|
+
if (server) server.addEventListener('click', function () { if (shell.open) closeSettings(); else openSettings(); });
|
|
330
|
+
var personal = document.getElementById('user-settings-btn');
|
|
331
|
+
if (personal) personal.addEventListener('click', function () { openSettings({ scope: 'personal' }); });
|
|
332
|
+
document.addEventListener('keydown', function (event) {
|
|
333
|
+
if (shell.open && event.key === 'Tab' && !document.querySelector('#settings-confirm, .admin-modal-overlay')) trapSettingsFocus(event, root);
|
|
334
|
+
if (!shell.open || event.key !== 'Escape' || event.defaultPrevented) return;
|
|
335
|
+
if (store.get('speechPopup') || document.querySelector(OVERLAYS)) return;
|
|
336
|
+
event.preventDefault(); closeSettings();
|
|
337
|
+
});
|
|
338
|
+
store.subscribe(function (state, previous) {
|
|
339
|
+
if (state.myUserId !== previous.myUserId) { closeSettings({ cleanup: true }); return; }
|
|
340
|
+
if (state.projectsHubList !== previous.projectsHubList || state.currentSlug !== previous.currentSlug || state.permissions !== previous.permissions || state.projectListLoaded !== previous.projectListLoaded || state.connected !== previous.connected) refreshLive();
|
|
341
|
+
});
|
|
342
|
+
}
|
|
@@ -10,7 +10,7 @@ import { iconHtml, refreshIcons } from './icons.js';
|
|
|
10
10
|
import { parseEmojis } from './markdown.js';
|
|
11
11
|
import { getCurrentTheme, getChatLayout, setChatLayout } from './theme.js';
|
|
12
12
|
import { openCommandPalette } from './command-palette.js';
|
|
13
|
-
import {
|
|
13
|
+
import { openSettings } from './settings-shell.js';
|
|
14
14
|
import { getCachedSessions, openSessionContextMenu } from './sidebar-sessions.js';
|
|
15
15
|
import { renderSessionPresence } from './session-presence-ui.js';
|
|
16
16
|
import {
|
|
@@ -1033,6 +1033,7 @@ export function renderSheetTools(listEl) {
|
|
|
1033
1033
|
|
|
1034
1034
|
function renderSheetSettings(listEl) {
|
|
1035
1035
|
var items = [
|
|
1036
|
+
{ icon: "user-cog", label: "Personal Settings", action: "personal-settings" },
|
|
1036
1037
|
{ icon: "folder-cog", label: "Project Settings", action: "project-settings" },
|
|
1037
1038
|
{ icon: "settings", label: "Server Settings", action: "server-settings" }
|
|
1038
1039
|
];
|
|
@@ -1044,35 +1045,12 @@ function renderSheetSettings(listEl) {
|
|
|
1044
1045
|
btn.innerHTML = '<i data-lucide="' + item.icon + '"></i><span class="mobile-more-item-label">' + item.label + '</span>';
|
|
1045
1046
|
btn.addEventListener("click", function () {
|
|
1046
1047
|
closeMobileSheet();
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
proj = getCachedProjectList()[pi];
|
|
1054
|
-
break;
|
|
1055
|
-
}
|
|
1056
|
-
}
|
|
1057
|
-
// For mate projects, use mate display name instead of slug
|
|
1058
|
-
if (proj && proj.isMate && getCachedMates().length > 0) {
|
|
1059
|
-
var mateId = getCachedCurrentSlug().replace("mate-", "");
|
|
1060
|
-
var _mates = getCachedMates();
|
|
1061
|
-
for (var mi = 0; mi < _mates.length; mi++) {
|
|
1062
|
-
var mp = _mates[mi].profile || {};
|
|
1063
|
-
if (_mates[mi].id === mateId) {
|
|
1064
|
-
proj = Object.assign({}, proj, { name: mp.displayName || _mates[mi].name || proj.name });
|
|
1065
|
-
break;
|
|
1066
|
-
}
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1069
|
-
if (proj && store.get('ownerLocked')) proj = Object.assign({}, proj, { ownerLocked: true });
|
|
1070
|
-
openProjectSettings(getCachedCurrentSlug(), proj);
|
|
1071
|
-
}, 250);
|
|
1072
|
-
} else if (item.action === "server-settings") {
|
|
1073
|
-
var settingsBtn = document.getElementById("server-settings-btn");
|
|
1074
|
-
if (settingsBtn) setTimeout(function () { settingsBtn.click(); }, 250);
|
|
1075
|
-
}
|
|
1048
|
+
// The shell resolves each scope to its exact page and falls back for
|
|
1049
|
+
// accounts that may not use it. Project settings target the open
|
|
1050
|
+
// workspace first.
|
|
1051
|
+
var route = item.action === "project-settings" ? { scope: "project", slug: getCachedCurrentSlug() }
|
|
1052
|
+
: item.action === "server-settings" ? { scope: "server" } : { scope: "personal" };
|
|
1053
|
+
setTimeout(function () { openSettings(route); }, 250);
|
|
1076
1054
|
});
|
|
1077
1055
|
listEl.appendChild(btn);
|
|
1078
1056
|
})(items[i]);
|