clay-server 5.2.0-beta.10 → 5.2.0-beta.11
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/project-session-notes.js +33 -8
- package/lib/project-sessions.js +63 -24
- package/lib/project-start-protocol.js +29 -0
- package/lib/project-vendor-login.js +2 -0
- package/lib/project.js +3 -0
- package/lib/public/app.js +25 -17
- package/lib/public/css/contextual-clay.css +1 -1
- package/lib/public/css/filebrowser.css +1 -1
- 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/title-bar.css +79 -0
- package/lib/public/index.html +50 -63
- 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-home-hub.js +2 -1
- package/lib/public/modules/app-messages.js +10 -2
- package/lib/public/modules/app-projects.js +1 -407
- package/lib/public/modules/app-rate-limit.js +12 -1
- package/lib/public/modules/clay-tool-presentation.js +1 -0
- package/lib/public/modules/contextual-clay.js +3 -1
- package/lib/public/modules/github-star.js +66 -0
- package/lib/public/modules/mate-loading-scene.js +17 -0
- 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.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 +33 -14
- package/lib/public/modules/projects-hub.js +10 -6
- package/lib/public/modules/server-settings.js +36 -91
- 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/theme.js +10 -2
- package/lib/public/modules/user-settings-layout.js +8 -33
- 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 +312 -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 +4 -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 +2 -0
- 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 +2 -1
- package/lib/public/modules/server-settings-navigation.js +0 -39
|
@@ -27,12 +27,10 @@ function setSelectState(select, value, disabled) {
|
|
|
27
27
|
select.disabled = !!disabled;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
|
|
30
|
+
// Editable for any authorised selected project: the server re-authorises the
|
|
31
|
+
// exact slug (owner, permission, access, worktree) and echoes it back.
|
|
34
32
|
function isReadOnly(active) {
|
|
35
|
-
return !active || !active.authorized || active.isWorktree
|
|
33
|
+
return !active || !active.authorized || active.isWorktree;
|
|
36
34
|
}
|
|
37
35
|
|
|
38
36
|
function renderActiveState() {
|
|
@@ -40,23 +38,26 @@ function renderActiveState() {
|
|
|
40
38
|
var pending = pendingState();
|
|
41
39
|
var select = selectElement();
|
|
42
40
|
var inheritedHint = document.getElementById("ps-session-visibility-default-inherited-hint");
|
|
43
|
-
var nonCurrentHint = document.getElementById("ps-session-visibility-default-non-current-hint");
|
|
44
41
|
if (inheritedHint) inheritedHint.classList.toggle("hidden", !(active && active.isWorktree));
|
|
45
|
-
if (nonCurrentHint) nonCurrentHint.classList.toggle("hidden", !(active && active.authorized && !active.isWorktree && !isCurrentProject(active)));
|
|
46
42
|
if (!select || !active) return;
|
|
47
|
-
|
|
43
|
+
// One request at a time: a pending change for any project holds the control.
|
|
44
|
+
setSelectState(select, active.effectiveValue, isReadOnly(active) || !!pending);
|
|
48
45
|
}
|
|
49
46
|
|
|
50
|
-
|
|
47
|
+
// The server requires the project owner plus the projectSettings permission.
|
|
48
|
+
// An administrator who is not the owner may see the value but not change it.
|
|
49
|
+
function visibilityAccess(project) {
|
|
51
50
|
var users = store.get('cachedAllUsers') || [];
|
|
52
51
|
var userId = store.get('myUserId');
|
|
53
52
|
var currentUser = null;
|
|
54
53
|
for (var i = 0; i < users.length; i++) {
|
|
55
54
|
if (users[i].id === userId) { currentUser = users[i]; break; }
|
|
56
55
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
var permitted = !store.get('permissions') || store.get('permissions').projectSettings !== false;
|
|
57
|
+
var owner = !!(project && project.projectOwnerId && project.projectOwnerId === userId);
|
|
58
|
+
var admin = !!(currentUser && currentUser.role === "admin");
|
|
59
|
+
var multi = !!(store.get('isMultiUserMode') && project && permitted);
|
|
60
|
+
return { visible: multi && (owner || admin), editable: multi && owner };
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
export function initProjectSessionVisibilitySettings() {
|
|
@@ -80,7 +81,6 @@ export function initProjectSessionVisibilitySettings() {
|
|
|
80
81
|
});
|
|
81
82
|
|
|
82
83
|
store.subscribe(function (state, previous) {
|
|
83
|
-
if (state.currentSlug !== previous.currentSlug) renderActiveState();
|
|
84
84
|
if (state.connected || previous.connected !== true) return;
|
|
85
85
|
var pending = pendingState();
|
|
86
86
|
if (!pending) return;
|
|
@@ -95,10 +95,13 @@ export function initProjectSessionVisibilitySettings() {
|
|
|
95
95
|
export function populateProjectSessionVisibilitySettings(slug, project) {
|
|
96
96
|
var field = document.getElementById("ps-session-visibility-default-field");
|
|
97
97
|
var select = selectElement();
|
|
98
|
-
|
|
98
|
+
var access = visibilityAccess(project);
|
|
99
|
+
if (field) field.style.display = access.visible ? "" : "none";
|
|
100
|
+
var ownerHint = document.getElementById("ps-session-visibility-default-owner-hint");
|
|
101
|
+
if (ownerHint) ownerHint.classList.toggle("hidden", !(access.visible && !access.editable));
|
|
99
102
|
if (!select) return;
|
|
100
103
|
var value = project && project.sessionVisibilityDefault === "shared" ? "shared" : "private";
|
|
101
|
-
var active = { slug: slug || "", effectiveValue: value, isWorktree: !!(project && project.isWorktree), authorized:
|
|
104
|
+
var active = { slug: slug || "", effectiveValue: value, isWorktree: !!(project && project.isWorktree), authorized: access.editable };
|
|
102
105
|
var pending = pendingState();
|
|
103
106
|
setVisibilityState(active, pending);
|
|
104
107
|
renderActiveState();
|
|
@@ -124,7 +127,7 @@ export function handleProjectSessionVisibilityDefault(msg) {
|
|
|
124
127
|
var active = activeState();
|
|
125
128
|
setVisibilityState(active, null);
|
|
126
129
|
var select = selectElement();
|
|
127
|
-
if (!select || !active || active.slug !== msg.slug) return;
|
|
130
|
+
if (!select || !active || active.slug !== msg.slug) { renderActiveState(); return; }
|
|
128
131
|
if (!msg.ok) {
|
|
129
132
|
active = Object.assign({}, active, { effectiveValue: pending.previous });
|
|
130
133
|
setVisibilityState(active, null);
|
|
@@ -1,76 +1,64 @@
|
|
|
1
1
|
import { store } from './store.js';
|
|
2
2
|
import { getWs } from './ws-ref.js';
|
|
3
|
-
import { trapSettingsFocus, isolateSettingsDialog, confirmSettingsAction } from './server-settings-state.js';
|
|
4
3
|
import { initProjectEnvironment, openProjectEnvironment, projectEnvironmentDirty, discardProjectEnvironment } from './project-settings-environment.js';
|
|
5
4
|
import { enhanceSettingsSelects } from './settings-select.js';
|
|
6
|
-
// Project identity and
|
|
5
|
+
// Project identity, access and environment: the Project pane of the unified
|
|
6
|
+
// settings shell. The shell owns the selected target; this pane only renders
|
|
7
|
+
// and edits the project it was last bound to.
|
|
7
8
|
import { refreshIcons } from './icons.js';
|
|
8
9
|
import { showToast } from './utils.js';
|
|
9
10
|
import { parseEmojis } from './markdown.js';
|
|
10
|
-
import {
|
|
11
|
-
import { openNotesBrowser, closeNotesBrowser, isNotesBrowserOpen } from './sticky-notes-browser.js';
|
|
11
|
+
import { registerSettingsScope, openSettings, closeSettings, isSettingsShellOpen, activeSettingsScope } from './settings-shell.js';
|
|
12
12
|
import { initProjectSessionVisibilitySettings, populateProjectSessionVisibilitySettings } from './project-session-visibility-settings.js';
|
|
13
13
|
|
|
14
14
|
var panelEl = null;
|
|
15
|
-
var navItems = null;
|
|
16
15
|
var sections = null;
|
|
17
|
-
var notesBrowserReopen = false;
|
|
18
|
-
var restoreBackground = null;
|
|
19
|
-
var returnFocus = null;
|
|
20
16
|
function settingsSlug() { return store.get("projectSettingsSlug"); }
|
|
21
17
|
function settingsProject() { return store.get("projectSettingsProject"); }
|
|
22
18
|
|
|
23
19
|
// Emoji categories (reuse from sidebar)
|
|
24
20
|
var EMOJI_CATEGORIES = null;
|
|
25
21
|
|
|
22
|
+
function renameDirty() {
|
|
23
|
+
var rename = document.getElementById("ps-rename-form");
|
|
24
|
+
var project = settingsProject();
|
|
25
|
+
return !!(rename && project && !rename.classList.contains("hidden") && document.getElementById("ps-rename-input").value.trim() !== (project.name || ""));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function discardDrafts() {
|
|
29
|
+
discardProjectEnvironment();
|
|
30
|
+
cancelRename();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
var controller = {
|
|
34
|
+
enter: function () {},
|
|
35
|
+
activate: function (page) { switchSection(page); },
|
|
36
|
+
leave: function () { closePsEmojiPicker(); },
|
|
37
|
+
busy: function (kind) { return kind !== "page" && (store.get("projectEnvironmentEditor") || {}).saving ? "Wait for the environment save to finish." : ""; },
|
|
38
|
+
leavePrompt: function (kind) {
|
|
39
|
+
var env = projectEnvironmentDirty();
|
|
40
|
+
var rename = renameDirty();
|
|
41
|
+
if (!env && !rename) return null;
|
|
42
|
+
if (kind === "close") return { message: "Your changes have not been saved. Discard them and close?", action: "Discard changes", apply: discardDrafts };
|
|
43
|
+
if (kind === "page") return null;
|
|
44
|
+
if (kind === "project" && rename) return { message: "Your unsaved project name change will be discarded when you switch projects. An unsaved environment draft stays in this tab.", action: "Discard name and switch", apply: cancelRename };
|
|
45
|
+
return { message: "Your unsaved project changes will remain in this tab. Leave this editor and keep the draft?", action: "Keep draft and leave" };
|
|
46
|
+
},
|
|
47
|
+
retarget: bindProjectSettings,
|
|
48
|
+
refreshTarget: refreshProjectSettings,
|
|
49
|
+
clearTarget: clearProjectSettings,
|
|
50
|
+
setEmpty: setPaneEmpty
|
|
51
|
+
};
|
|
26
52
|
|
|
27
53
|
export function initProjectSettings(emojiCategories) {
|
|
28
54
|
EMOJI_CATEGORIES = emojiCategories;
|
|
29
55
|
panelEl = document.getElementById("project-settings");
|
|
30
56
|
if (!panelEl) return;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
var destinations = [{id: 'profile', label: 'General'}, {id: 'access', label: 'Access'}, {id: 'environment', label: 'Environment'}];
|
|
34
|
-
var nav = panelEl.querySelector('.server-settings-nav-items');
|
|
35
|
-
var dropdown = document.getElementById('ps-nav-dropdown');
|
|
36
|
-
destinations.forEach(function (page) {
|
|
37
|
-
var button = document.createElement('button');
|
|
38
|
-
button.className = 'settings-nav-item'; button.dataset.section = page.id; button.textContent = page.label;
|
|
39
|
-
nav.appendChild(button);
|
|
40
|
-
var option = document.createElement('option'); option.value = page.id; option.textContent = page.label; dropdown.appendChild(option);
|
|
41
|
-
});
|
|
57
|
+
controller.pane = panelEl;
|
|
58
|
+
registerSettingsScope("project", controller);
|
|
42
59
|
initProjectEnvironment();
|
|
43
|
-
panelEl.addEventListener('click', function (event) { if (event.target === panelEl) closeProjectSettings(); });
|
|
44
|
-
panelEl.addEventListener('keydown', function (event) {
|
|
45
|
-
if (event.key === 'Escape' && !event.defaultPrevented) { event.preventDefault(); event.stopPropagation(); closeProjectSettings(); }
|
|
46
|
-
trapSettingsFocus(event, panelEl);
|
|
47
|
-
});
|
|
48
|
-
navItems = panelEl.querySelectorAll(".settings-nav-item");
|
|
49
60
|
sections = panelEl.querySelectorAll(".ps-section");
|
|
50
61
|
|
|
51
|
-
// Nav clicks
|
|
52
|
-
for (var i = 0; i < navItems.length; i++) {
|
|
53
|
-
navItems[i].addEventListener("click", function () {
|
|
54
|
-
switchSection(this.dataset.section);
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Mobile dropdown nav
|
|
59
|
-
var psNavDropdown = document.getElementById("ps-nav-dropdown");
|
|
60
|
-
if (psNavDropdown) {
|
|
61
|
-
psNavDropdown.addEventListener("change", function () {
|
|
62
|
-
switchSection(psNavDropdown.value);
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Close button
|
|
67
|
-
var closeBtn = document.getElementById("project-settings-close");
|
|
68
|
-
if (closeBtn) {
|
|
69
|
-
closeBtn.addEventListener("click", function () {
|
|
70
|
-
closeProjectSettings();
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
|
|
74
62
|
// Profile: rename
|
|
75
63
|
var renameBtn = document.getElementById("ps-rename-btn");
|
|
76
64
|
var renameForm = document.getElementById("ps-rename-form");
|
|
@@ -111,98 +99,105 @@ export function initProjectSettings(emojiCategories) {
|
|
|
111
99
|
}
|
|
112
100
|
if (iconRemoveBtn) {
|
|
113
101
|
iconRemoveBtn.addEventListener("click", function () {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
updateIconPreview(null);
|
|
102
|
+
var slug = settingsSlug();
|
|
103
|
+
var previous = { icon: settingsProject() ? settingsProject().icon || null : null };
|
|
104
|
+
if (slug && sendEdit("set_project_icon", { slug: slug, icon: null }, previous)) updateIconPreview(null);
|
|
118
105
|
});
|
|
119
106
|
}
|
|
120
107
|
|
|
121
108
|
initProjectSessionVisibilitySettings();
|
|
122
|
-
store.subscribe(function (next, previous) { if (next.myUserId !== previous.myUserId) closeProjectSettings({ cleanup: true }); });
|
|
123
109
|
}
|
|
124
110
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
111
|
+
// Entry for the project context menu and mobile sheet: show this exact
|
|
112
|
+
// project's settings without changing the open workspace.
|
|
113
|
+
export function openProjectSettings(slug) {
|
|
114
|
+
return openSettings({ scope: "project", page: "profile", slug: slug });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Bind the pane to one project. Called by the shell for every target change.
|
|
118
|
+
function bindProjectSettings(slug, project) {
|
|
119
|
+
var same = settingsSlug() === slug;
|
|
120
|
+
store.set({ projectSettingsSlug: slug, projectSettingsProject: Object.assign({}, project, { name: project.title || project.project || project.name || slug }) });
|
|
130
121
|
var env = store.get("projectEnvironmentEditor");
|
|
131
122
|
if (env && !env.dirty && !env.saving) store.set({ projectEnvironmentEditor: Object.assign({}, env, { loaded: false, loading: false }) });
|
|
123
|
+
if (same) refreshProjectSettings(slug, project);
|
|
124
|
+
else { closePsEmojiPicker(); populateProfile(); }
|
|
125
|
+
var active = panelEl.querySelector(".ps-section.active");
|
|
126
|
+
if (active && active.dataset.section === "environment") openProjectEnvironment(slug);
|
|
127
|
+
}
|
|
132
128
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
129
|
+
function refreshProjectSettings(slug, project) {
|
|
130
|
+
if (settingsSlug() !== slug) return;
|
|
131
|
+
var name = project.title || project.project || project.name || slug;
|
|
132
|
+
store.set({ projectSettingsProject: Object.assign({}, project, { name: name }) });
|
|
133
|
+
var nameEl = document.getElementById("ps-project-name");
|
|
134
|
+
if (nameEl) nameEl.textContent = name || "-";
|
|
135
|
+
updateIconPreview(project.icon || null);
|
|
136
|
+
populateProjectSessionVisibilitySettings(slug, settingsProject());
|
|
137
|
+
document.getElementById('ps-project-path').textContent = project.path || (slug === store.get('currentSlug') ? store.get('cwd') : '') || 'Folder information unavailable';
|
|
138
|
+
}
|
|
142
139
|
|
|
140
|
+
// The selected project is gone or no longer authorised.
|
|
141
|
+
function clearProjectSettings(slug) {
|
|
142
|
+
var env = store.get("projectEnvironmentEditor") || {};
|
|
143
|
+
var drafts = Object.assign({}, store.get("projectEnvironmentDrafts") || {});
|
|
144
|
+
delete drafts[slug];
|
|
145
|
+
store.set({ projectSettingsSlug: null, projectSettingsProject: null, projectEnvironmentDrafts: drafts });
|
|
146
|
+
if (env.slug === slug) { store.set({ projectEnvironmentEditor: {} }); }
|
|
147
|
+
closePsEmojiPicker();
|
|
148
|
+
cancelRename();
|
|
149
|
+
}
|
|
143
150
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
151
|
+
var EMPTY_HEADINGS = { loading: "Loading projects", empty: "No project selected", error: "Projects unavailable", lost: "Project unavailable" };
|
|
152
|
+
function setPaneEmpty(text, kind) {
|
|
153
|
+
var empty = document.getElementById("ps-empty");
|
|
154
|
+
if (empty) {
|
|
155
|
+
empty.hidden = !text;
|
|
156
|
+
document.getElementById("ps-empty-text").textContent = text || "";
|
|
157
|
+
empty.querySelector("h2").textContent = EMPTY_HEADINGS[kind] || EMPTY_HEADINGS.empty;
|
|
149
158
|
}
|
|
159
|
+
panelEl.classList.toggle("ps-no-target", !!text);
|
|
160
|
+
}
|
|
150
161
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
162
|
+
// Rename and icon edits are bound to the exact project, request and (for the
|
|
163
|
+
// reply) the slug the server echoes; a reply for another project never
|
|
164
|
+
// changes the view.
|
|
165
|
+
var pendingEdits = {};
|
|
166
|
+
var editSequence = 0;
|
|
167
|
+
function sendEdit(type, fields, previous) {
|
|
168
|
+
var ws = getWs();
|
|
169
|
+
if (!ws || !store.get("connected")) return false;
|
|
170
|
+
editSequence += 1;
|
|
171
|
+
var requestId = "project-settings-" + Date.now() + "-" + editSequence;
|
|
172
|
+
pendingEdits[requestId] = { slug: fields.slug, type: type, previous: previous };
|
|
173
|
+
ws.send(JSON.stringify(Object.assign({ type: type, requestId: requestId }, fields)));
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function handleProjectSettingsEditResult(msg) {
|
|
178
|
+
var pending = msg && msg.requestId && pendingEdits[msg.requestId];
|
|
179
|
+
if (!pending) return;
|
|
180
|
+
delete pendingEdits[msg.requestId];
|
|
181
|
+
if (msg.slug !== pending.slug || msg.ok || settingsSlug() !== pending.slug) return;
|
|
182
|
+
if (pending.type === "set_project_title") updateProjectSettingsName(pending.previous.name);
|
|
183
|
+
else updateProjectSettingsIcon(pending.previous.icon);
|
|
157
184
|
}
|
|
158
185
|
|
|
159
|
-
// `cleanup` is for teardown while leaving the project: the notes workbench is
|
|
160
|
-
// not reopened over whatever screen comes next.
|
|
161
186
|
export function closeProjectSettings(opts) {
|
|
162
|
-
|
|
163
|
-
if (!(opts && opts.cleanup) && (store.get("projectEnvironmentEditor") || {}).saving) { showToast("Wait for the environment save to finish."); return; }
|
|
164
|
-
var rename = document.getElementById("ps-rename-form");
|
|
165
|
-
var renameDirty = rename && !rename.classList.contains("hidden") && document.getElementById("ps-rename-input").value.trim() !== (settingsProject().name || "");
|
|
166
|
-
if (!(opts && (opts.cleanup || opts.confirmed)) && (projectEnvironmentDirty() || renameDirty)) {
|
|
167
|
-
confirmSettingsAction('Your changes have not been saved. Discard them and close?', 'Discard changes', function () {
|
|
168
|
-
discardProjectEnvironment(); closeProjectSettings({ confirmed: true });
|
|
169
|
-
});
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
var wasOpen = !panelEl.classList.contains("hidden");
|
|
173
|
-
panelEl.classList.add("hidden");
|
|
174
|
-
closePsEmojiPicker();
|
|
175
|
-
if (restoreBackground) { restoreBackground(); restoreBackground = null; }
|
|
176
|
-
if (returnFocus && returnFocus.isConnected && !(opts && opts.cleanup)) returnFocus.focus();
|
|
177
|
-
if (wasOpen) {
|
|
178
|
-
releaseNotesForSettings();
|
|
179
|
-
if (notesBrowserReopen && !(opts && opts.cleanup)) openNotesBrowser();
|
|
180
|
-
notesBrowserReopen = false;
|
|
181
|
-
}
|
|
187
|
+
closeSettings({ cleanup: !!(opts && opts.cleanup), onlyScope: "project" });
|
|
182
188
|
}
|
|
183
189
|
|
|
184
190
|
export function isProjectSettingsOpen() {
|
|
185
|
-
return
|
|
191
|
+
return isSettingsShellOpen() && activeSettingsScope() === "project";
|
|
186
192
|
}
|
|
187
193
|
|
|
188
194
|
function switchSection(name) {
|
|
189
|
-
for (var i = 0; i < navItems.length; i++) {
|
|
190
|
-
var active = navItems[i].dataset.section === name;
|
|
191
|
-
navItems[i].classList.toggle("active", active);
|
|
192
|
-
navItems[i].setAttribute("aria-current", active ? "page" : "false");
|
|
193
|
-
}
|
|
194
195
|
for (var j = 0; j < sections.length; j++) {
|
|
195
|
-
|
|
196
|
-
sections[j].classList.toggle("active", active2);
|
|
196
|
+
sections[j].classList.toggle("active", sections[j].dataset.section === name);
|
|
197
197
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
if (psNavDropdown && psNavDropdown.value !== name) {
|
|
202
|
-
psNavDropdown.value = name;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
if (name === "environment") openProjectEnvironment(settingsSlug());
|
|
198
|
+
if (name === "environment" && settingsSlug()) openProjectEnvironment(settingsSlug());
|
|
199
|
+
enhanceSettingsSelects(panelEl);
|
|
200
|
+
refreshIcons();
|
|
206
201
|
}
|
|
207
202
|
|
|
208
203
|
function populateProfile() {
|
|
@@ -229,15 +224,13 @@ function commitRename() {
|
|
|
229
224
|
var renameInput = document.getElementById("ps-rename-input");
|
|
230
225
|
var nameEl = document.getElementById("ps-project-name");
|
|
231
226
|
var newName = renameInput ? renameInput.value.trim() : "";
|
|
232
|
-
|
|
233
|
-
|
|
227
|
+
var slug = settingsSlug();
|
|
228
|
+
var previous = { name: settingsProject() ? settingsProject().name : "" };
|
|
229
|
+
if (newName && slug && sendEdit("set_project_title", { slug: slug, title: newName }, previous)) {
|
|
234
230
|
if (nameEl) nameEl.textContent = newName;
|
|
235
231
|
if (settingsProject()) settingsProject().name = newName;
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
}
|
|
239
|
-
if (newName && getWs() && store.get("connected")) cancelRename();
|
|
240
|
-
else showToast("Enter a name and connect to the server before saving.");
|
|
232
|
+
cancelRename();
|
|
233
|
+
} else showToast("Enter a name and connect to the server before saving.");
|
|
241
234
|
}
|
|
242
235
|
|
|
243
236
|
function cancelRename() {
|
|
@@ -330,10 +323,9 @@ function showPsEmojiPicker() {
|
|
|
330
323
|
btn.addEventListener("click", function (e) {
|
|
331
324
|
e.stopPropagation();
|
|
332
325
|
closePsEmojiPicker();
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
}
|
|
336
|
-
updateIconPreview(emoji);
|
|
326
|
+
var slug = settingsSlug();
|
|
327
|
+
var previous = { icon: settingsProject() ? settingsProject().icon || null : null };
|
|
328
|
+
if (slug && sendEdit("set_project_icon", { slug: slug, icon: emoji }, previous)) updateIconPreview(emoji);
|
|
337
329
|
});
|
|
338
330
|
grid.appendChild(btn);
|
|
339
331
|
})(emojis[i]);
|
|
@@ -365,8 +357,6 @@ export function updateProjectSettingsName(name) {
|
|
|
365
357
|
if (settingsProject()) settingsProject().name = name;
|
|
366
358
|
var nameEl = document.getElementById("ps-project-name");
|
|
367
359
|
if (nameEl) nameEl.textContent = name || "-";
|
|
368
|
-
var navTitle = document.getElementById("ps-nav-title");
|
|
369
|
-
if (navTitle) navTitle.textContent = name || "-";
|
|
370
360
|
}
|
|
371
361
|
|
|
372
362
|
export function handleProjectOwnerChanged(msg) {
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// project-start-dialog.js - One fixed-size dialog for choosing how to start a project
|
|
2
|
+
|
|
3
|
+
import { store } from './store.js';
|
|
4
|
+
import { iconHtml, refreshIcons } from './icons.js';
|
|
5
|
+
import { openState, closeState, selectView } from './project-start-state.js';
|
|
6
|
+
import { folderViewHtml, bindFolderView, renderFolderView, startFolderBrowse, resetFolderState, handleBrowseDirResult } from './project-start-folder-view.js';
|
|
7
|
+
import { formViewHtml, bindFormView, renderFormView } from './project-start-forms.js';
|
|
8
|
+
import { submitFolder, handleAddProjectResult, handleCloneProgress } from './project-start-submit.js';
|
|
9
|
+
|
|
10
|
+
export { handleBrowseDirResult, handleAddProjectResult, handleCloneProgress };
|
|
11
|
+
|
|
12
|
+
var TITLES = { choices: 'Start a project', open: 'Open a folder', create: 'Create a project', clone: 'Clone a repository' };
|
|
13
|
+
var CHOICES = [
|
|
14
|
+
{ view: 'open', icon: 'folder-open', title: 'Open an existing folder', text: 'Use a directory on the machine running Clay.' },
|
|
15
|
+
{ view: 'create', icon: 'plus', title: 'Create a new project', text: 'Start in a new, empty project directory.' },
|
|
16
|
+
{ view: 'clone', icon: 'git-branch', title: 'Clone a repository', text: 'Bring an existing Git repository into Clay.' },
|
|
17
|
+
];
|
|
18
|
+
var shownGen = -1;
|
|
19
|
+
var opener = null;
|
|
20
|
+
|
|
21
|
+
function modal() { return document.getElementById('project-start-modal'); }
|
|
22
|
+
function body() { return document.getElementById('project-start-body'); }
|
|
23
|
+
function current() { return store.get('projectStart'); }
|
|
24
|
+
|
|
25
|
+
function choicesHtml() {
|
|
26
|
+
var html = '<div class="project-start-view" data-view="choices"><div class="project-start-scroll"><ul class="project-start-rows project-start-choices" role="list">';
|
|
27
|
+
for (var i = 0; i < CHOICES.length; i++) {
|
|
28
|
+
var c = CHOICES[i];
|
|
29
|
+
html += '<li><button type="button" class="project-start-row project-start-choice" data-choice="' + c.view + '">' +
|
|
30
|
+
iconHtml(c.icon, 'project-start-choice-icon') + '<span class="project-start-choice-text"><strong>' + c.title + '</strong><span>' + c.text + '</span></span>' +
|
|
31
|
+
iconHtml('chevron-right') + '</button></li>';
|
|
32
|
+
}
|
|
33
|
+
return html + '</ul></div></div>';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function focusFirst(view) {
|
|
37
|
+
var target = null;
|
|
38
|
+
if (view === 'choices') target = body().querySelector('[data-choice]');
|
|
39
|
+
else if (view === 'open') target = body().querySelector('[data-ps="list"]');
|
|
40
|
+
else target = body().querySelector('[data-ps="field"]');
|
|
41
|
+
if (!target) target = modal().querySelector('.project-start-dialog');
|
|
42
|
+
target.focus({ preventScroll: true });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function renderShell(view) {
|
|
46
|
+
var container = body();
|
|
47
|
+
document.getElementById('project-start-title').textContent = TITLES[view];
|
|
48
|
+
document.getElementById('project-start-back').hidden = view === 'choices';
|
|
49
|
+
document.getElementById('project-start-subtitle').hidden = view !== 'choices';
|
|
50
|
+
if (view === 'choices') container.innerHTML = choicesHtml();
|
|
51
|
+
else if (view === 'open') { container.innerHTML = folderViewHtml(); bindFolderView(submitFolder); }
|
|
52
|
+
else { container.innerHTML = formViewHtml(view); bindFormView(view); }
|
|
53
|
+
refreshIcons();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function render(state, previous) {
|
|
57
|
+
var overlay = modal();
|
|
58
|
+
if (!overlay) return;
|
|
59
|
+
if (state.view === null) {
|
|
60
|
+
if (!overlay.classList.contains('hidden')) hide();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (state.gen !== shownGen || previous.view === null) {
|
|
64
|
+
shownGen = state.gen;
|
|
65
|
+
overlay.classList.remove('hidden');
|
|
66
|
+
renderShell(state.view);
|
|
67
|
+
if (state.view === 'open') { resetFolderState(); startFolderBrowse(); }
|
|
68
|
+
if (state.view === 'create' || state.view === 'clone') renderFormView(state.view);
|
|
69
|
+
focusFirst(state.view);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (state.view === 'open') renderFolderView();
|
|
73
|
+
else if (state.view === 'create' || state.view === 'clone') renderFormView(state.view);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function hide() {
|
|
77
|
+
modal().classList.add('hidden');
|
|
78
|
+
body().innerHTML = '';
|
|
79
|
+
shownGen = -1;
|
|
80
|
+
resetFolderState();
|
|
81
|
+
var target = opener;
|
|
82
|
+
opener = null;
|
|
83
|
+
if (target && document.contains(target) && typeof target.focus === 'function') target.focus({ preventScroll: true });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function openAddProjectModal(mode) {
|
|
87
|
+
if (current().view === null) opener = document.activeElement;
|
|
88
|
+
store.set({ projectStart: openState(current(), mode) });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function closeAddProjectModal() {
|
|
92
|
+
if (current().view === null) return;
|
|
93
|
+
store.set({ projectStart: closeState(current()) });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function trapTab(event, dialog) {
|
|
97
|
+
var items = dialog.querySelectorAll('button:not([disabled]):not([hidden]), input:not([disabled]):not([hidden]), [tabindex="-1"][data-ps="list"]');
|
|
98
|
+
var focusable = [];
|
|
99
|
+
for (var i = 0; i < items.length; i++) if (items[i].offsetParent !== null && items[i].tabIndex >= 0) focusable.push(items[i]);
|
|
100
|
+
if (!focusable.length) { event.preventDefault(); dialog.focus(); return; }
|
|
101
|
+
var first = focusable[0];
|
|
102
|
+
var last = focusable[focusable.length - 1];
|
|
103
|
+
var active = document.activeElement;
|
|
104
|
+
if (event.shiftKey && (active === first || active === dialog)) { event.preventDefault(); last.focus(); }
|
|
105
|
+
else if (!event.shiftKey && active === last) { event.preventDefault(); first.focus(); }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function initProjectStart() {
|
|
109
|
+
var overlay = modal();
|
|
110
|
+
var dialog = overlay.querySelector('.project-start-dialog');
|
|
111
|
+
store.subscribe(function (state, previous) {
|
|
112
|
+
if (state.projectStart !== previous.projectStart) render(state.projectStart, previous.projectStart);
|
|
113
|
+
else if (state.projectStartFolder !== previous.projectStartFolder && state.projectStart.view === 'open') renderFolderView();
|
|
114
|
+
});
|
|
115
|
+
overlay.querySelector('.project-start-backdrop').addEventListener('click', closeAddProjectModal);
|
|
116
|
+
document.getElementById('project-start-close').addEventListener('click', closeAddProjectModal);
|
|
117
|
+
document.getElementById('project-start-back').addEventListener('click', function () {
|
|
118
|
+
store.set({ projectStart: selectView(current(), 'choices') });
|
|
119
|
+
});
|
|
120
|
+
body().addEventListener('click', function (event) {
|
|
121
|
+
var choice = event.target.closest('[data-choice]');
|
|
122
|
+
if (choice) store.set({ projectStart: selectView(current(), choice.dataset.choice) });
|
|
123
|
+
});
|
|
124
|
+
dialog.addEventListener('keydown', function (event) {
|
|
125
|
+
if (event.key === 'Escape') { event.preventDefault(); closeAddProjectModal(); }
|
|
126
|
+
else if (event.key === 'Tab') trapTab(event, dialog);
|
|
127
|
+
});
|
|
128
|
+
var addButton = document.getElementById('project-list-add');
|
|
129
|
+
if (addButton) addButton.addEventListener('click', function () { openAddProjectModal(); });
|
|
130
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// project-start-folder-state.js - Pure state transitions for the open-folder browser.
|
|
2
|
+
// A reply is applied only when its requestId matches the single in-flight request.
|
|
3
|
+
|
|
4
|
+
export function emptyFolderState() {
|
|
5
|
+
return {
|
|
6
|
+
status: "idle", dir: null, parent: null, entries: [], error: null, errorKind: null,
|
|
7
|
+
requestId: null, requestedPath: null, candidates: [], candidateIdx: 0, initial: false, pathOpen: false,
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function beginInitial(candidates, requestId) {
|
|
12
|
+
var next = emptyFolderState();
|
|
13
|
+
next.status = "loading";
|
|
14
|
+
next.candidates = candidates.slice();
|
|
15
|
+
next.initial = true;
|
|
16
|
+
next.requestId = requestId;
|
|
17
|
+
next.requestedPath = candidates[0] || "~";
|
|
18
|
+
return next;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function beginNavigate(prev, path, requestId) {
|
|
22
|
+
return Object.assign({}, prev, {
|
|
23
|
+
status: "loading", requestId: requestId, requestedPath: path, error: null, errorKind: null, initial: false,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function errorKindOf(message) {
|
|
28
|
+
return /not permitted|permission|EACCES|EPERM/i.test(message || "") ? "denied" : "failed";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function cleanEntries(entries) {
|
|
32
|
+
var out = [];
|
|
33
|
+
for (var i = 0; i < (entries || []).length; i++) {
|
|
34
|
+
var e = entries[i];
|
|
35
|
+
if (e && typeof e.name === "string" && typeof e.path === "string") out.push({ name: e.name, path: e.path });
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Returns { state, retryPath }. retryPath is set when the initial candidate failed and another remains.
|
|
41
|
+
export function applyBrowseResult(prev, msg, nextRequestId) {
|
|
42
|
+
if (!msg || prev.status !== "loading" || !prev.requestId || msg.requestId !== prev.requestId) {
|
|
43
|
+
return { state: prev, retryPath: null };
|
|
44
|
+
}
|
|
45
|
+
var failed = !!msg.error;
|
|
46
|
+
var wrongTarget = !failed && (typeof msg.dir !== "string" || (msg.prefix || "") !== "");
|
|
47
|
+
if (failed || wrongTarget) {
|
|
48
|
+
var idx = prev.candidateIdx + 1;
|
|
49
|
+
if (prev.initial && idx < prev.candidates.length) {
|
|
50
|
+
var retry = Object.assign({}, prev, { candidateIdx: idx, requestId: nextRequestId, requestedPath: prev.candidates[idx] });
|
|
51
|
+
return { state: retry, retryPath: prev.candidates[idx] };
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
state: Object.assign({}, prev, {
|
|
55
|
+
status: "error",
|
|
56
|
+
error: failed ? msg.error : "That folder was not found.",
|
|
57
|
+
errorKind: failed ? errorKindOf(msg.error) : "missing",
|
|
58
|
+
}),
|
|
59
|
+
retryPath: null,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
state: Object.assign({}, prev, {
|
|
64
|
+
status: "ready", dir: msg.dir, parent: typeof msg.parent === "string" ? msg.parent : null,
|
|
65
|
+
entries: cleanEntries(msg.entries), error: null, errorKind: null, initial: false,
|
|
66
|
+
}),
|
|
67
|
+
retryPath: null,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function failLocally(prev, message) {
|
|
72
|
+
if (prev.status !== "loading") return prev;
|
|
73
|
+
return Object.assign({}, prev, { status: "error", error: message, errorKind: "failed" });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function canOpenFolder(state) {
|
|
77
|
+
return state.status === "ready" && typeof state.dir === "string" && state.dir.length > 0;
|
|
78
|
+
}
|