clay-server 5.2.1-beta.2 → 5.3.0-beta.1
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/lib/clay-control-presentation-binding.js +5 -3
- package/lib/clay-product-control-tools.js +3 -3
- package/lib/clay-product-control.js +4 -2
- package/lib/clay-screen-controls.js +18 -3
- package/lib/clay-split-view-awareness.js +35 -0
- package/lib/clay-split-view-controls.js +122 -0
- package/lib/clay-split-view-input.js +20 -0
- package/lib/clay-split-view-render-proof.js +22 -0
- package/lib/pane-asset-manifest.js +63 -0
- package/lib/project-connection.js +19 -10
- package/lib/project-session-actions.js +1 -0
- package/lib/project-session-delete.js +2 -2
- package/lib/project-session-folders.js +1 -0
- package/lib/project-sessions.js +6 -1
- package/lib/project-split-view-document.js +33 -0
- package/lib/project-split-view-names.js +67 -0
- package/lib/project-split-view-organization.js +23 -0
- package/lib/project-split-views.js +479 -0
- package/lib/project-worker-conversation.js +158 -0
- package/lib/project-worker-creation-proposal.js +3 -4
- package/lib/project-worker-proposal.js +1 -1
- package/lib/project-worker-removal.js +101 -0
- package/lib/project.js +28 -1
- package/lib/public/app.js +32 -13
- package/lib/public/css/base.css +9 -0
- package/lib/public/css/filebrowser.css +0 -1
- package/lib/public/css/pane-header-controls.css +100 -0
- package/lib/public/css/pane.css +24 -5
- package/lib/public/css/popup-chat.css +123 -0
- package/lib/public/css/right-workbench.css +50 -18
- package/lib/public/css/split-view-layout.css +460 -0
- package/lib/public/css/split-view-worker-activity.css +97 -0
- package/lib/public/css/sticky-notes.css +13 -0
- package/lib/public/index.html +3 -3
- package/lib/public/modules/app-connection.js +4 -1
- package/lib/public/modules/app-messages.js +25 -8
- package/lib/public/modules/chat-render-runtime.js +45 -0
- package/lib/public/modules/clay-control-copy.js +3 -0
- package/lib/public/modules/clay-navigation-render.js +1 -1
- package/lib/public/modules/clay-owner-navigation.js +15 -5
- package/lib/public/modules/clay-split-view-screen.js +74 -0
- package/lib/public/modules/clay-tool-activity.js +5 -0
- package/lib/public/modules/command-palette.js +30 -28
- package/lib/public/modules/contextual-clay-session.js +26 -2
- package/lib/public/modules/contextual-clay.js +5 -2
- package/lib/public/modules/contextual-mate-sessions.js +2 -0
- package/lib/public/modules/generation-stop.js +7 -1
- package/lib/public/modules/knowledge-workbench-entry.js +43 -0
- package/lib/public/modules/markdown.js +11 -0
- package/lib/public/modules/mate-knowledge-workbench.js +2 -7
- package/lib/public/modules/mate-sidebar.js +1 -1
- package/lib/public/modules/palette-clay-conversation.js +87 -0
- package/lib/public/modules/palette-render-helpers.js +25 -0
- package/lib/public/modules/pane-activation.js +46 -7
- package/lib/public/modules/pane-binding.js +12 -2
- package/lib/public/modules/pane-bridge.js +12 -3
- package/lib/public/modules/pane-deferred-bootstrap.js +32 -0
- package/lib/public/modules/pane-feature-assets.js +35 -0
- package/lib/public/modules/pane-search-receipt.js +12 -0
- package/lib/public/modules/permission-control.js +6 -6
- package/lib/public/modules/popup-chat-rendering.js +244 -0
- package/lib/public/modules/project-activation.js +2 -2
- package/lib/public/modules/qrcode.js +12 -1
- package/lib/public/modules/right-workbench.js +68 -0
- package/lib/public/modules/search-hints.js +79 -0
- package/lib/public/modules/session-context-menu.js +35 -2
- package/lib/public/modules/session-folder-context.js +24 -13
- package/lib/public/modules/session-folder-delete.js +18 -1
- package/lib/public/modules/session-folder-layout.js +7 -6
- package/lib/public/modules/session-search-pane-state.js +30 -0
- package/lib/public/modules/session-search.js +53 -13
- package/lib/public/modules/sidebar-mobile.js +3 -3
- package/lib/public/modules/sidebar-projects.js +1 -0
- package/lib/public/modules/sidebar-session-folders-ui.js +3 -3
- package/lib/public/modules/sidebar-sessions.js +57 -24
- package/lib/public/modules/sidebar.js +20 -3
- package/lib/public/modules/split-group-helpers.js +1 -1
- package/lib/public/modules/split-group-restore.js +8 -11
- package/lib/public/modules/split-pair-ui.js +18 -0
- package/lib/public/modules/split-pane-header-state.js +45 -0
- package/lib/public/modules/split-pane-reconciler.js +23 -4
- package/lib/public/modules/split-pane-renderer.js +330 -23
- package/lib/public/modules/split-pane-retention.js +157 -0
- package/lib/public/modules/split-session-boundary.js +21 -0
- package/lib/public/modules/split-view-create-action.js +15 -0
- package/lib/public/modules/split-view-create-menu.js +83 -0
- package/lib/public/modules/split-view-create-request.js +23 -0
- package/lib/public/modules/split-view-layout-client.js +198 -0
- package/lib/public/modules/split-view-layout-icons.js +37 -0
- package/lib/public/modules/split-view-layout-scope.js +38 -0
- package/lib/public/modules/split-view-organization.js +73 -0
- package/lib/public/modules/split-view-presentation-close.js +35 -0
- package/lib/public/modules/split-view-presentation-drag.js +254 -0
- package/lib/public/modules/split-view-presentation-drop.js +57 -0
- package/lib/public/modules/split-view-presentation-lookup.js +44 -0
- package/lib/public/modules/split-view-presentation-renderer.js +494 -0
- package/lib/public/modules/split-view-presentations.js +495 -0
- package/lib/public/modules/split-view-shell-controls.js +28 -0
- package/lib/public/modules/split-view-sidebar-menu.js +42 -0
- package/lib/public/modules/split-view-sidebar-renderer.js +30 -0
- package/lib/public/modules/split-view-singleton.js +21 -0
- package/lib/public/modules/split-view-worker-activity.js +94 -0
- package/lib/public/modules/split-view-worker-controls.js +30 -0
- package/lib/public/modules/split-view-worker-removal.js +137 -0
- package/lib/public/modules/split-view-worker-scroll.js +57 -0
- package/lib/public/modules/split-view-worker-transcript.js +46 -0
- package/lib/public/modules/split-view-workers.js +352 -0
- package/lib/public/modules/split-view.js +61 -22
- package/lib/public/modules/sticky-notes.js +9 -2
- package/lib/public/modules/tools.js +25 -5
- package/lib/public/modules/workbench-layout-preference.js +76 -0
- package/lib/public/pane-assets/app-OJEQXBBS.js +1 -0
- package/lib/public/pane-assets/chunk-4SMIVAB4.js +1 -0
- package/lib/public/pane-assets/chunk-623V5CEO.js +1 -0
- package/lib/public/pane-assets/chunk-6SM2WQJX.js +1 -0
- package/lib/public/pane-assets/chunk-7H36SUZA.js +2 -0
- package/lib/public/pane-assets/chunk-DGOFGEUK.js +1 -0
- package/lib/public/pane-assets/chunk-GML2R5JE.js +167 -0
- package/lib/public/pane-assets/chunk-I57URYSL.js +1 -0
- package/lib/public/pane-assets/chunk-I5P3MWQG.js +1 -0
- package/lib/public/pane-assets/chunk-UVGOPFEX.js +2 -0
- package/lib/public/pane-assets/manifest.json +551 -0
- package/lib/public/pane-assets/style-GN7R6XKV.css +1 -0
- package/lib/public/style.css +5 -1
- package/lib/server-owner-ui.js +6 -2
- package/lib/server-owner-view-observation.js +34 -0
- package/lib/server-palette.js +1 -0
- package/lib/server-pane-bootstrap.js +24 -0
- package/lib/server-settings.js +2 -0
- package/lib/server-static-assets.js +52 -0
- package/lib/server-workbench-layout-settings.js +56 -0
- package/lib/server.js +6 -45
- package/lib/session-activity-state.js +28 -0
- package/lib/session-list-delivery.js +17 -0
- package/lib/session-pair-turn-control.js +1 -6
- package/lib/session-search.js +3 -0
- package/lib/session-split-group-anchors.js +2 -2
- package/lib/session-split-group-roles.js +5 -8
- package/lib/session-split-group-v2-store.js +0 -3
- package/lib/sessions.js +16 -6
- package/lib/split-view-layout.js +132 -0
- package/lib/users-workbench-layout-preferences.js +33 -0
- package/lib/users.js +3 -0
- package/lib/workspace-query-service.js +1 -5
- package/lib/ws-schema.js +15 -0
- package/package.json +6 -3
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
var MAX_NAME_LENGTH = 80;
|
|
2
|
+
|
|
3
|
+
function mode(view) {
|
|
4
|
+
if (view && (view.nameMode === "automatic" || view.nameMode === "custom")) return view.nameMode;
|
|
5
|
+
return view && view.name === "Split view" ? "automatic" : "custom";
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function truncate(name) {
|
|
9
|
+
var chars = Array.from(name);
|
|
10
|
+
return chars.length <= MAX_NAME_LENGTH ? name : chars.slice(0, MAX_NAME_LENGTH - 1).join("") + "…";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function automaticName(sessions) {
|
|
14
|
+
var titles = (sessions || []).map(function (session) {
|
|
15
|
+
return session && typeof session.title === "string" && session.title.trim() || "Session";
|
|
16
|
+
});
|
|
17
|
+
return truncate(titles.length ? titles.join(" + ") : "Split view");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function project(view, sessions) {
|
|
21
|
+
var nameMode = mode(view), fullName = (sessions || []).map(function (session) {
|
|
22
|
+
return session && typeof session.title === "string" && session.title.trim() || "Session";
|
|
23
|
+
}).join(" + ");
|
|
24
|
+
return { id: view.id, name: nameMode === "automatic" ? truncate(fullName || "Split view") : view.name,
|
|
25
|
+
fullName: nameMode === "automatic" ? fullName || "Split view" : view.name, nameMode: nameMode,
|
|
26
|
+
folderId: view.folderId || null, favorite: view.favorite === true,
|
|
27
|
+
revision: view.revision, layout: view.layout, activeSessionOriginId: view.activeSessionOriginId,
|
|
28
|
+
tree: view.tree, sessions: sessions };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function stableOrder(existingRefs, refs, sessions, requiredOrigins) {
|
|
32
|
+
if (!Array.isArray(existingRefs) || !Array.isArray(refs) || !Array.isArray(sessions) || refs.length !== sessions.length) return null;
|
|
33
|
+
var priorOrigins = existingRefs.map(function (ref) { return ref && ref.originId; });
|
|
34
|
+
if (priorOrigins.some(function (origin, index) { return !origin || priorOrigins.indexOf(origin) !== index; })) return null;
|
|
35
|
+
var requested = refs.map(function (ref, index) {
|
|
36
|
+
var session = sessions[index];
|
|
37
|
+
return ref && session && ref.originId === session.sessionOriginId ? ref.originId : null;
|
|
38
|
+
});
|
|
39
|
+
if (requested.some(function (origin, index) { return !origin || requested.indexOf(origin) !== index; })) return null;
|
|
40
|
+
if (requiredOrigins && (requested.length !== requiredOrigins.length || requiredOrigins.some(function (origin) { return requested.indexOf(origin) === -1; }))) return null;
|
|
41
|
+
var origins = existingRefs.map(function (ref) { return ref && ref.originId; }).filter(function (origin) { return requested.indexOf(origin) !== -1; });
|
|
42
|
+
requested.forEach(function (origin) { if (origins.indexOf(origin) === -1) origins.push(origin); });
|
|
43
|
+
if (origins.length !== requested.length) return null;
|
|
44
|
+
return { refs: origins.map(function (id) { return refs.find(function (ref) { return ref.originId === id; }); }),
|
|
45
|
+
sessions: origins.map(function (id) { return sessions.find(function (session) { return session.sessionOriginId === id; }); }) };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function append(view, refs, sessions, origin) {
|
|
49
|
+
if (!view || !Array.isArray(view.sessionRefs) || view.sessionRefs.some(function (ref) { return !ref || ref.originId === origin; })) return null;
|
|
50
|
+
var required = view.sessionRefs.map(function (ref) { return ref.originId; });
|
|
51
|
+
required.push(origin);
|
|
52
|
+
return stableOrder(view.sessionRefs, refs, sessions, required);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function update(view, refs, sessions) { return view ? stableOrder(view.sessionRefs, refs, sessions) : null; }
|
|
56
|
+
|
|
57
|
+
function targetFirst(refs, sessions, targetOrigin, movedOrigin) {
|
|
58
|
+
if (!targetOrigin || !movedOrigin || targetOrigin === movedOrigin) return null;
|
|
59
|
+
var ordered = stableOrder([], refs, sessions, [targetOrigin, movedOrigin]);
|
|
60
|
+
if (!ordered || ordered.refs.length !== 2) return null;
|
|
61
|
+
var target = ordered.refs.find(function (ref) { return ref.originId === targetOrigin; });
|
|
62
|
+
var moved = ordered.refs.find(function (ref) { return ref.originId === movedOrigin; });
|
|
63
|
+
return target && moved ? { refs: [target, moved], sessions: [ordered.sessions.find(function (session) { return session.sessionOriginId === targetOrigin; }),
|
|
64
|
+
ordered.sessions.find(function (session) { return session.sessionOriginId === movedOrigin; })] } : null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = { mode: mode, truncate: truncate, automaticName: automaticName, project: project, append: append, update: update, targetFirst: targetFirst };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Saved presentation metadata never changes member-session organization.
|
|
2
|
+
function attachOrganization(deps) {
|
|
3
|
+
function organize(ownerId, linuxUser, input) {
|
|
4
|
+
var prepared = deps.prepare(ownerId, linuxUser, input.expectedRevision);
|
|
5
|
+
if (!prepared.ok) return prepared;
|
|
6
|
+
var view = prepared.record.views.find(function (item) { return item.id === input.id; });
|
|
7
|
+
if (!view || view.revision !== input.expectedViewRevision) return { ok: false, code: 'conflict', error: 'Split view changed. Reload and try again.' };
|
|
8
|
+
if (!(input.folderId === null || typeof input.folderId === 'string' && /^[A-Za-z0-9_-]{1,128}$/.test(input.folderId)) || typeof input.favorite !== 'boolean') return { ok: false, code: 'invalid', error: 'Invalid Split view organization.' };
|
|
9
|
+
view.folderId = input.folderId; view.favorite = input.favorite;
|
|
10
|
+
return deps.commit(ownerId, prepared.record, view);
|
|
11
|
+
}
|
|
12
|
+
function reconcileFolders(ownerId, linuxUser, folderIds) {
|
|
13
|
+
var current = deps.state(ownerId, linuxUser);
|
|
14
|
+
if (!current.ok || !current.views.some(function (view) { return view.folderId && folderIds.indexOf(view.folderId) < 0; })) return current;
|
|
15
|
+
var prepared = deps.prepare(ownerId, linuxUser, current.revision);
|
|
16
|
+
if (!prepared.ok) return prepared;
|
|
17
|
+
var changed = [];
|
|
18
|
+
prepared.record.views.forEach(function (view) { if (view.folderId && folderIds.indexOf(view.folderId) < 0) { view.folderId = null; changed.push(view); } });
|
|
19
|
+
return deps.commit(ownerId, prepared.record, changed);
|
|
20
|
+
}
|
|
21
|
+
return { organize: organize, reconcileFolders: reconcileFolders };
|
|
22
|
+
}
|
|
23
|
+
module.exports = { attachOrganization: attachOrganization };
|
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
var fs = require("fs");
|
|
2
|
+
var path = require("path");
|
|
3
|
+
var crypto = require("crypto");
|
|
4
|
+
var layout = require("./split-view-layout");
|
|
5
|
+
var names = require("./project-split-view-names");
|
|
6
|
+
var sessionProvenance = require("./session-provenance");
|
|
7
|
+
var validDocument = require("./project-split-view-document").validDocument;
|
|
8
|
+
function clone(value) { return JSON.parse(JSON.stringify(value)); }
|
|
9
|
+
function ownerKey(ownerId) { return ownerId == null ? "default" : "user:" + String(ownerId); }
|
|
10
|
+
function emptyDocument() { return { version: 1, projects: {} }; }
|
|
11
|
+
function emptyOwner(ownerId, linuxUser) { return { ownerId: ownerId == null ? null : String(ownerId), linuxUser: linuxUser || null, revision: 0, views: [] }; }
|
|
12
|
+
function createSplitViewsStore(opts) {
|
|
13
|
+
var slug = opts.slug;
|
|
14
|
+
var sessionsDir = opts.sessionsDir;
|
|
15
|
+
var filePath = opts.filePath || path.join(sessionsDir, "presentation-split-views.json");
|
|
16
|
+
var fsImpl = opts.fs || fs;
|
|
17
|
+
var persist = opts.persist || null;
|
|
18
|
+
var document = emptyDocument();
|
|
19
|
+
var loadError = null;
|
|
20
|
+
try {
|
|
21
|
+
var loaded = JSON.parse(fsImpl.readFileSync(filePath, "utf8"));
|
|
22
|
+
if (!validDocument(loaded)) throw new Error("Stored Split view data is invalid");
|
|
23
|
+
document = loaded;
|
|
24
|
+
} catch (error) {
|
|
25
|
+
if (!error || error.code !== "ENOENT") loadError = error;
|
|
26
|
+
}
|
|
27
|
+
function projectRecord(source, create) {
|
|
28
|
+
if (!source.projects[slug] && create) source.projects[slug] = { owners: {} };
|
|
29
|
+
return source.projects[slug] || null;
|
|
30
|
+
}
|
|
31
|
+
function ownerRecord(source, ownerId, linuxUser, create) {
|
|
32
|
+
var project = projectRecord(source, create);
|
|
33
|
+
if (!project) return null;
|
|
34
|
+
var key = ownerKey(ownerId), record = project.owners[key];
|
|
35
|
+
if (!record && create) { record = emptyOwner(ownerId, linuxUser); project.owners[key] = record; }
|
|
36
|
+
if (record && (record.ownerId !== (ownerId == null ? null : String(ownerId)) || record.linuxUser !== (linuxUser || null))) return { identityMismatch: true };
|
|
37
|
+
return record || null;
|
|
38
|
+
}
|
|
39
|
+
function save(next) {
|
|
40
|
+
if (!validDocument(next)) throw new Error("Split view state failed validation");
|
|
41
|
+
var serialized = JSON.stringify(next, null, 2) + "\n";
|
|
42
|
+
if (persist) {
|
|
43
|
+
if (persist(filePath, serialized) === false) throw new Error("Split view persistence returned false");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
fsImpl.mkdirSync(sessionsDir, { recursive: true });
|
|
47
|
+
var tempPath = filePath + ".tmp." + process.pid + "." + crypto.randomBytes(4).toString("hex");
|
|
48
|
+
try {
|
|
49
|
+
fsImpl.writeFileSync(tempPath, serialized, { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
50
|
+
if (process.platform !== "win32") try { fsImpl.chmodSync(tempPath, 0o600); } catch (e) {}
|
|
51
|
+
fsImpl.renameSync(tempPath, filePath);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
try { fsImpl.unlinkSync(tempPath); } catch (e) {}
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function state(ownerId, linuxUser) {
|
|
58
|
+
if (loadError) return { ok: false, error: "Stored Split view data could not be read; no changes were made", code: "storage", revision: 0, views: [] };
|
|
59
|
+
var record = ownerRecord(document, ownerId, linuxUser, false);
|
|
60
|
+
if (record && record.identityMismatch) return { ok: false, error: "Split view owner or OS identity changed", code: "identity", revision: 0, views: [] };
|
|
61
|
+
return record ? { ok: true, revision: record.revision, views: clone(record.views) } : { ok: true, revision: 0, views: [] };
|
|
62
|
+
}
|
|
63
|
+
function conflict(actual) { return { ok: false, error: "Split view state changed. Reload and try again.", code: "conflict", revision: actual }; }
|
|
64
|
+
function validateRevision(value) { return Number.isInteger(value) && value >= 0; }
|
|
65
|
+
function prepareRecord(ownerId, linuxUser, expectedRevision) {
|
|
66
|
+
var current = state(ownerId, linuxUser);
|
|
67
|
+
if (!current.ok) return current;
|
|
68
|
+
if (!validateRevision(expectedRevision) || expectedRevision !== current.revision) return conflict(current.revision);
|
|
69
|
+
var next = clone(document), record = ownerRecord(next, ownerId, linuxUser, true);
|
|
70
|
+
return { ok: true, next: next, record: record };
|
|
71
|
+
}
|
|
72
|
+
function commit(ownerId, record, changedView) {
|
|
73
|
+
record.revision++;
|
|
74
|
+
if (Array.isArray(changedView)) changedView.forEach(function (view) { if (view) view.revision = record.revision; });
|
|
75
|
+
else if (changedView) changedView.revision = record.revision;
|
|
76
|
+
try { save(documentForOwner(record, ownerId)); }
|
|
77
|
+
catch (error) { return { ok: false, error: error.message || String(error), code: "storage", revision: state(ownerId, record.linuxUser).revision }; }
|
|
78
|
+
document = documentForOwner(record, ownerId);
|
|
79
|
+
var resultView = Array.isArray(changedView) ? changedView[changedView.length - 1] : changedView;
|
|
80
|
+
return { ok: true, revision: record.revision, view: resultView ? clone(resultView) : null, views: clone(record.views) };
|
|
81
|
+
}
|
|
82
|
+
function documentForOwner(record, ownerId) {
|
|
83
|
+
var next = clone(document), project = projectRecord(next, true);
|
|
84
|
+
project.owners[ownerKey(ownerId)] = clone(record);
|
|
85
|
+
return next;
|
|
86
|
+
}
|
|
87
|
+
function validExpectedView(record, id) {
|
|
88
|
+
if (typeof id !== "string" || !id) return { error: "Split view not found" };
|
|
89
|
+
var index = record.views.findIndex(function (view) { return view.id === id; });
|
|
90
|
+
if (index === -1) return { error: "Split view not found" };
|
|
91
|
+
return { index: index, view: record.views[index] };
|
|
92
|
+
}
|
|
93
|
+
function refsFor(origins, resolvedRefs) {
|
|
94
|
+
if (!Array.isArray(origins) || origins.length < 2 || origins.length > layout.MAX_SESSIONS ||
|
|
95
|
+
origins.some(function (origin) { return !layout.validOrigin(origin); }) || new Set(origins).size !== origins.length) return { error: "A Split view requires two to eight unique session origins" };
|
|
96
|
+
if (!Array.isArray(resolvedRefs) || resolvedRefs.length !== origins.length) return { error: "One or more exact sessions could not be resolved" };
|
|
97
|
+
var refs = [];
|
|
98
|
+
for (var i = 0; i < origins.length; i++) {
|
|
99
|
+
var ref = resolvedRefs.find(function (item) { return item && item.originId === origins[i]; });
|
|
100
|
+
if (!ref) return { error: "One or more exact sessions could not be resolved" };
|
|
101
|
+
refs.push({ originId: ref.originId, ownerId: ref.ownerId == null ? null : String(ref.ownerId), linuxUser: ref.linuxUser || null });
|
|
102
|
+
}
|
|
103
|
+
return { refs: refs };
|
|
104
|
+
}
|
|
105
|
+
function validateComposition(input, refs) {
|
|
106
|
+
var origins = refs.map(function (ref) { return ref.originId; }), tree = layout.validateTree(input.tree, origins), preset = input.layout;
|
|
107
|
+
if (!tree.ok) return { error: tree.error };
|
|
108
|
+
if (!layout.validatePreset(preset)) return { error: "Unknown Split view layout" };
|
|
109
|
+
if (typeof input.activeSessionOriginId !== "string" || origins.indexOf(input.activeSessionOriginId) === -1) return { error: "Active session must belong to the Split view" };
|
|
110
|
+
return { origins: origins };
|
|
111
|
+
}
|
|
112
|
+
function uniqueMembership(views, refs, exceptId, secondExceptId) {
|
|
113
|
+
var used = [];
|
|
114
|
+
views.forEach(function (view) {
|
|
115
|
+
if (view.id === exceptId || view.id === secondExceptId) return;
|
|
116
|
+
view.sessionRefs.forEach(function (ref) { used.push(ref.originId); });
|
|
117
|
+
});
|
|
118
|
+
return refs.every(function (ref) { return used.indexOf(ref.originId) === -1; });
|
|
119
|
+
}
|
|
120
|
+
function create(ownerId, linuxUser, input, resolvedRefs) {
|
|
121
|
+
input = input || {};
|
|
122
|
+
var prepared = prepareRecord(ownerId, linuxUser, input.expectedRevision);
|
|
123
|
+
if (!prepared.ok) return prepared;
|
|
124
|
+
var name = layout.validateName(input.name), refs = refsFor(input.sessionOrigins, resolvedRefs);
|
|
125
|
+
if (!name.ok) return name;
|
|
126
|
+
if (refs.error) return { ok: false, error: refs.error };
|
|
127
|
+
var composition = validateComposition(input, refs.refs);
|
|
128
|
+
if (composition.error) return { ok: false, error: composition.error };
|
|
129
|
+
if (!uniqueMembership(prepared.record.views, refs.refs)) return { ok: false, error: "A session can belong to only one Split view" };
|
|
130
|
+
var nameMode = input.nameMode === "custom" ? "custom" : "automatic";
|
|
131
|
+
var view = { id: crypto.randomUUID(), name: nameMode === "automatic" ? names.automaticName(input.liveSessions) : name.name, nameMode: nameMode,
|
|
132
|
+
folderId: null, favorite: false, revision: prepared.record.revision + 1, layout: input.layout,
|
|
133
|
+
activeSessionOriginId: input.activeSessionOriginId, tree: clone(input.tree), sessionRefs: refs.refs };
|
|
134
|
+
prepared.record.views.push(view);
|
|
135
|
+
return commit(ownerId, prepared.record, view);
|
|
136
|
+
}
|
|
137
|
+
function update(ownerId, linuxUser, input, resolvedRefs) {
|
|
138
|
+
input = input || {};
|
|
139
|
+
var prepared = prepareRecord(ownerId, linuxUser, input.expectedRevision);
|
|
140
|
+
if (!prepared.ok) return prepared;
|
|
141
|
+
var found = validExpectedView(prepared.record, input.id, input.expectedRevision);
|
|
142
|
+
if (found.error) return { ok: false, error: found.error };
|
|
143
|
+
if (found.conflict) return conflict(found.revision);
|
|
144
|
+
var refs = refsFor(input.sessionOrigins, resolvedRefs);
|
|
145
|
+
if (refs.error) return { ok: false, error: refs.error };
|
|
146
|
+
var ordered = names.update(found.view, refs.refs, input.liveSessions);
|
|
147
|
+
if (!ordered) return { ok: false, error: "One or more exact sessions could not be resolved" };
|
|
148
|
+
refs.refs = ordered.refs; input.liveSessions = ordered.sessions;
|
|
149
|
+
var composition = validateComposition(input, refs.refs);
|
|
150
|
+
if (composition.error) return { ok: false, error: composition.error };
|
|
151
|
+
if (!uniqueMembership(prepared.record.views, refs.refs, input.id)) return { ok: false, error: "A session can belong to only one Split view" };
|
|
152
|
+
if (names.mode(found.view) === "automatic") found.view.name = names.automaticName(input.liveSessions);
|
|
153
|
+
found.view.layout = input.layout; found.view.activeSessionOriginId = input.activeSessionOriginId;
|
|
154
|
+
found.view.tree = clone(input.tree); found.view.sessionRefs = refs.refs;
|
|
155
|
+
return commit(ownerId, prepared.record, found.view);
|
|
156
|
+
}
|
|
157
|
+
function rename(ownerId, linuxUser, input) {
|
|
158
|
+
input = input || {};
|
|
159
|
+
var prepared = prepareRecord(ownerId, linuxUser, input.expectedRevision);
|
|
160
|
+
if (!prepared.ok) return prepared;
|
|
161
|
+
var found = validExpectedView(prepared.record, input.id, input.expectedRevision);
|
|
162
|
+
if (found.error) return { ok: false, error: found.error };
|
|
163
|
+
if (found.conflict) return conflict(found.revision);
|
|
164
|
+
var name = layout.validateName(input.name);
|
|
165
|
+
if (!name.ok) return name;
|
|
166
|
+
found.view.name = name.name;
|
|
167
|
+
found.view.nameMode = "custom";
|
|
168
|
+
return commit(ownerId, prepared.record, found.view);
|
|
169
|
+
}
|
|
170
|
+
function separate(ownerId, linuxUser, input) {
|
|
171
|
+
input = input || {};
|
|
172
|
+
var prepared = prepareRecord(ownerId, linuxUser, input.expectedRevision);
|
|
173
|
+
if (!prepared.ok) return prepared;
|
|
174
|
+
var found = validExpectedView(prepared.record, input.id, input.expectedRevision);
|
|
175
|
+
if (found.error) return { ok: false, error: found.error };
|
|
176
|
+
if (found.conflict) return conflict(found.revision);
|
|
177
|
+
var removed = prepared.record.views.splice(found.index, 1)[0];
|
|
178
|
+
return commit(ownerId, prepared.record, null);
|
|
179
|
+
}
|
|
180
|
+
function move(ownerId, linuxUser, input, resolvedSourceRefs, resolvedTargetRefs) {
|
|
181
|
+
input = input || {};
|
|
182
|
+
var prepared = prepareRecord(ownerId, linuxUser, input.expectedRevision);
|
|
183
|
+
if (!prepared.ok) return prepared;
|
|
184
|
+
if (typeof input.sourceSessionOriginId !== "string" || !layout.validOrigin(input.sourceSessionOriginId)) return { ok: false, error: "A valid source session is required", code: "invalid" };
|
|
185
|
+
var source = validExpectedView(prepared.record, input.sourceViewId);
|
|
186
|
+
if (source.error) return { ok: false, error: "Source Split view not found", code: "not_found", revision: prepared.record.revision };
|
|
187
|
+
var sourceOrigins = source.view.sessionRefs.map(function (ref) { return ref.originId; });
|
|
188
|
+
if (sourceOrigins.indexOf(input.sourceSessionOriginId) === -1) return { ok: false, error: "Source session does not belong to the source Split view", code: "session", revision: prepared.record.revision };
|
|
189
|
+
if (!Array.isArray(resolvedSourceRefs) || resolvedSourceRefs.length !== source.view.sessionRefs.length) return { ok: false, error: "Source sessions could not be resolved", code: "session", revision: prepared.record.revision };
|
|
190
|
+
for (var i = 0; i < source.view.sessionRefs.length; i++) {
|
|
191
|
+
var storedSource = source.view.sessionRefs[i], liveSource = resolvedSourceRefs.find(function (ref) { return ref && ref.originId === storedSource.originId; });
|
|
192
|
+
if (!liveSource || (liveSource.ownerId || null) !== (storedSource.ownerId || null) || (liveSource.linuxUser || null) !== (storedSource.linuxUser || null)) {
|
|
193
|
+
return { ok: false, error: "A source session was replaced or changed identity", code: "session", revision: prepared.record.revision };
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
var target = null, targetSessionOriginId = input.targetSessionOriginId;
|
|
197
|
+
var hasTargetView = typeof input.targetViewId === "string" && input.targetViewId.length > 0;
|
|
198
|
+
var hasStandaloneTarget = typeof targetSessionOriginId === "string" && layout.validOrigin(targetSessionOriginId);
|
|
199
|
+
if (hasTargetView === hasStandaloneTarget || (input.targetViewId != null && !hasTargetView)) {
|
|
200
|
+
return { ok: false, error: "Choose exactly one target Split view or standalone session", code: "invalid", revision: prepared.record.revision };
|
|
201
|
+
}
|
|
202
|
+
if (hasTargetView) {
|
|
203
|
+
target = validExpectedView(prepared.record, input.targetViewId);
|
|
204
|
+
if (target.error || target.view.id === source.view.id) return { ok: false, error: "Target Split view is unavailable", code: "not_found", revision: prepared.record.revision };
|
|
205
|
+
}
|
|
206
|
+
if (Array.isArray(input.targetSessionOrigins) && input.targetSessionOrigins.length > layout.MAX_SESSIONS) {
|
|
207
|
+
return { ok: false, error: "A Split view can contain at most eight sessions", code: "limit", revision: prepared.record.revision };
|
|
208
|
+
}
|
|
209
|
+
var targetRefs = refsFor(input.targetSessionOrigins, resolvedTargetRefs);
|
|
210
|
+
if (targetRefs.error) return { ok: false, error: targetRefs.error, code: "session", revision: prepared.record.revision };
|
|
211
|
+
var requestedTargetOrigins = targetRefs.refs.map(function (ref) { return ref.originId; });
|
|
212
|
+
if (requestedTargetOrigins.indexOf(input.sourceSessionOriginId) === -1) return { ok: false, error: "Target layout must include the moved source session", code: "invalid", revision: prepared.record.revision };
|
|
213
|
+
if (target) {
|
|
214
|
+
var requestedPriorOrigins = target.view.sessionRefs.map(function (ref) { return ref.originId; });
|
|
215
|
+
if (requestedTargetOrigins.length !== requestedPriorOrigins.length + 1 || requestedPriorOrigins.some(function (origin) { return requestedTargetOrigins.indexOf(origin) === -1; })) {
|
|
216
|
+
return { ok: false, error: "Target layout must preserve every existing target session", code: "invalid", revision: prepared.record.revision };
|
|
217
|
+
}
|
|
218
|
+
if (!uniqueMembership(prepared.record.views, targetRefs.refs, target.view.id, source.view.id)) return { ok: false, error: "A target session belongs to another Split view", code: "membership", revision: prepared.record.revision };
|
|
219
|
+
} else if (targetSessionOriginId === input.sourceSessionOriginId || requestedTargetOrigins.length !== 2 || requestedTargetOrigins.indexOf(targetSessionOriginId) === -1) {
|
|
220
|
+
return { ok: false, error: "A standalone target transfer requires exactly the target and moved sessions", code: "invalid", revision: prepared.record.revision };
|
|
221
|
+
} else if (!uniqueMembership(prepared.record.views, targetRefs.refs, source.view.id)) {
|
|
222
|
+
return { ok: false, error: "A standalone target session already belongs to a Split view", code: "membership", revision: prepared.record.revision };
|
|
223
|
+
}
|
|
224
|
+
if (target) { var orderedTarget = names.append(target.view, targetRefs.refs, input.liveSessions, input.sourceSessionOriginId);
|
|
225
|
+
if (!orderedTarget) return { ok: false, error: "Target layout must preserve existing sessions and include the moved source exactly once", code: "invalid", revision: prepared.record.revision };
|
|
226
|
+
targetRefs.refs = orderedTarget.refs; input.liveSessions = orderedTarget.sessions; }
|
|
227
|
+
else { var orderedStandalone = names.targetFirst(targetRefs.refs, input.liveSessions, targetSessionOriginId, input.sourceSessionOriginId);
|
|
228
|
+
if (!orderedStandalone) return { ok: false, error: "A standalone target transfer requires exactly the target and moved sessions", code: "invalid", revision: prepared.record.revision };
|
|
229
|
+
targetRefs.refs = orderedStandalone.refs; input.liveSessions = orderedStandalone.sessions; }
|
|
230
|
+
var targetOrigins = targetRefs.refs.map(function (ref) { return ref.originId; });
|
|
231
|
+
if (!layout.validatePreset(input.layout) || !layout.validateTree(input.tree, targetOrigins).ok || targetOrigins.length > layout.MAX_SESSIONS) {
|
|
232
|
+
return { ok: false, error: "Target layout is invalid or exceeds the eight-session limit", code: targetOrigins.length > layout.MAX_SESSIONS ? "limit" : "invalid", revision: prepared.record.revision };
|
|
233
|
+
}
|
|
234
|
+
if (typeof input.activeSessionOriginId !== "string" || targetOrigins.indexOf(input.activeSessionOriginId) === -1) {
|
|
235
|
+
return { ok: false, error: "Active session must belong to the target Split view", code: "invalid", revision: prepared.record.revision };
|
|
236
|
+
}
|
|
237
|
+
if (!target) {
|
|
238
|
+
var name = layout.validateName(input.name);
|
|
239
|
+
if (!name.ok) return { ok: false, error: name.error, code: "invalid", revision: prepared.record.revision };
|
|
240
|
+
}
|
|
241
|
+
var nextSourceOrigins = sourceOrigins.filter(function (origin) { return origin !== input.sourceSessionOriginId; });
|
|
242
|
+
source.view.tree = layout.removeLeaf(source.view.tree, input.sourceSessionOriginId);
|
|
243
|
+
source.view.sessionRefs = source.view.sessionRefs.filter(function (ref) { return ref.originId !== input.sourceSessionOriginId; });
|
|
244
|
+
if (source.view.activeSessionOriginId === input.sourceSessionOriginId && nextSourceOrigins.length) source.view.activeSessionOriginId = nextSourceOrigins[0];
|
|
245
|
+
if (nextSourceOrigins.length < 2) prepared.record.views.splice(source.index, 1);
|
|
246
|
+
if (nextSourceOrigins.length >= 2 && names.mode(source.view) === "automatic") source.view.name = names.automaticName(input.sourceLiveSessions.filter(function (session) { return session.sessionOriginId !== input.sourceSessionOriginId; }));
|
|
247
|
+
if (target) {
|
|
248
|
+
target.view.tree = clone(input.tree); target.view.layout = input.layout;
|
|
249
|
+
target.view.activeSessionOriginId = input.activeSessionOriginId; target.view.sessionRefs = targetRefs.refs;
|
|
250
|
+
if (names.mode(target.view) === "automatic") target.view.name = names.automaticName(input.liveSessions);
|
|
251
|
+
} else {
|
|
252
|
+
target = { index: -1, view: { id: crypto.randomUUID(), folderId: null, favorite: false, name: names.automaticName(input.liveSessions), nameMode: "automatic", revision: prepared.record.revision + 1,
|
|
253
|
+
layout: input.layout, activeSessionOriginId: input.activeSessionOriginId, tree: clone(input.tree), sessionRefs: targetRefs.refs } };
|
|
254
|
+
prepared.record.views.push(target.view);
|
|
255
|
+
}
|
|
256
|
+
var changed = [target.view];
|
|
257
|
+
if (nextSourceOrigins.length >= 2) changed.unshift(source.view);
|
|
258
|
+
return commit(ownerId, prepared.record, changed);
|
|
259
|
+
}
|
|
260
|
+
function removeOrigin(originId) {
|
|
261
|
+
var project = projectRecord(document, false), changedOwners = [], failures = [];
|
|
262
|
+
if (!project) return { owners: changedOwners, failures: failures };
|
|
263
|
+
var keys = Object.keys(project.owners);
|
|
264
|
+
for (var i = 0; i < keys.length; i++) {
|
|
265
|
+
var source = project.owners[keys[i]], next = clone(source), changed = false, retained = [];
|
|
266
|
+
for (var j = 0; j < next.views.length; j++) {
|
|
267
|
+
var view = next.views[j], member = view.sessionRefs.some(function (ref) { return ref.originId === originId; });
|
|
268
|
+
if (!member) { retained.push(view); continue; }
|
|
269
|
+
changed = true;
|
|
270
|
+
var tree = layout.removeLeaf(view.tree, originId), origins = view.sessionRefs.filter(function (ref) { return ref.originId !== originId; });
|
|
271
|
+
if (origins.length < 2) continue;
|
|
272
|
+
view.tree = tree;
|
|
273
|
+
view.sessionRefs = origins;
|
|
274
|
+
if (view.activeSessionOriginId === originId) view.activeSessionOriginId = origins[0].originId;
|
|
275
|
+
retained.push(view);
|
|
276
|
+
}
|
|
277
|
+
if (!changed) continue;
|
|
278
|
+
next.views = retained;
|
|
279
|
+
next.revision++;
|
|
280
|
+
next.views.forEach(function (view) { view.revision = next.revision; });
|
|
281
|
+
try {
|
|
282
|
+
var replacement = documentForOwner(next, source.ownerId);
|
|
283
|
+
save(replacement);
|
|
284
|
+
document = replacement;
|
|
285
|
+
changedOwners.push(source.ownerId);
|
|
286
|
+
} catch (error) { failures.push({ ownerId: source.ownerId, error: error.message || String(error) }); }
|
|
287
|
+
}
|
|
288
|
+
return { owners: changedOwners, failures: failures };
|
|
289
|
+
}
|
|
290
|
+
var organization = require('./project-split-view-organization').attachOrganization({ prepare: prepareRecord, commit: commit, state: state });
|
|
291
|
+
return { organize: organization.organize, reconcileFolders: organization.reconcileFolders, state: state, create: create, update: update, rename: rename, separate: separate, move: move, removeOrigin: removeOrigin,
|
|
292
|
+
get filePath() { return filePath; }, get loadError() { return loadError; } };
|
|
293
|
+
}
|
|
294
|
+
function attachProjectSplitViews(ctx) {
|
|
295
|
+
var store = createSplitViewsStore({ sessionsDir: ctx.sm.sessionsDir, slug: ctx.slug, persist: ctx.persist });
|
|
296
|
+
var users = ctx.usersModule;
|
|
297
|
+
function actor(ws) {
|
|
298
|
+
if (ctx.isMate || !ws || !ctx.clients.has(ws) || ws._clayPaneActivation || !ctx.canAccessProject(ws)) return null;
|
|
299
|
+
if (users.isMultiUser()) {
|
|
300
|
+
var user = ws._clayUser && users.findUserById(ws._clayUser.id);
|
|
301
|
+
if (!user || !ctx.canAccessProject(ws)) return null;
|
|
302
|
+
if (ctx.osUsers && (!user.linuxUser || !ctx.getOsUserInfoForWs({ _clayUser: user }))) return null;
|
|
303
|
+
return { id: user.id, linuxUser: ctx.osUsers ? user.linuxUser : null };
|
|
304
|
+
}
|
|
305
|
+
var current = ws._clayUser && users.findUserById(ws._clayUser.id);
|
|
306
|
+
if (ctx.osUsers && (!current || !current.linuxUser || !ctx.getOsUserInfoForWs({ _clayUser: current }))) return null;
|
|
307
|
+
return { id: current && current.id || null, linuxUser: ctx.osUsers && current ? current.linuxUser : null };
|
|
308
|
+
}
|
|
309
|
+
function resolveOrigin(ws, originId) {
|
|
310
|
+
if (!layout.validOrigin(originId)) return null;
|
|
311
|
+
var found = null, ambiguous = false;
|
|
312
|
+
ctx.sm.sessions.forEach(function (session) {
|
|
313
|
+
if (session.sessionOriginId !== originId) return;
|
|
314
|
+
if (found && found !== session) { ambiguous = true; return; }
|
|
315
|
+
found = session;
|
|
316
|
+
});
|
|
317
|
+
if (!found || ambiguous || ctx.sm.sessions.get(found.localId) !== found || sessionProvenance.isWorker(found) || !ctx.canAccessSession(ws, found)) return null;
|
|
318
|
+
var linuxUser = ctx.osUsers ? ctx.getLinuxUserForSession(found) : null;
|
|
319
|
+
if (ctx.osUsers && !linuxUser) return null;
|
|
320
|
+
return { session: found, ref: { originId: found.sessionOriginId, ownerId: found.ownerId || null, linuxUser: linuxUser || null } };
|
|
321
|
+
}
|
|
322
|
+
function resolvedRefs(ws, origins) {
|
|
323
|
+
if (!Array.isArray(origins) || origins.length < 2) return { error: "A Split view requires two to eight sessions", code: "invalid" };
|
|
324
|
+
if (origins.length > layout.MAX_SESSIONS) return { error: "A Split view can contain at most eight sessions", code: "limit" };
|
|
325
|
+
if (new Set(origins).size !== origins.length || origins.some(function (origin) { return !layout.validOrigin(origin); })) return { error: "Split view session origins must be unique valid values", code: "invalid" };
|
|
326
|
+
var refs = [], sessions = [];
|
|
327
|
+
for (var i = 0; i < origins.length; i++) {
|
|
328
|
+
var resolved = resolveOrigin(ws, origins[i]);
|
|
329
|
+
if (!resolved) return { error: "One or more exact project sessions are unavailable", code: "session" };
|
|
330
|
+
refs.push(resolved.ref); sessions.push(resolved.session);
|
|
331
|
+
}
|
|
332
|
+
return { refs: refs, sessions: sessions };
|
|
333
|
+
}
|
|
334
|
+
function verifyStoredView(ws, owner, id) {
|
|
335
|
+
var state = store.state(owner.id, owner.linuxUser);
|
|
336
|
+
if (!state.ok) return { error: state.error, code: state.code };
|
|
337
|
+
var view = state.views.find(function (item) { return item.id === id; });
|
|
338
|
+
if (!view) return { error: "Split view not found", code: "not_found" };
|
|
339
|
+
for (var i = 0; i < view.sessionRefs.length; i++) {
|
|
340
|
+
var stored = view.sessionRefs[i], current = resolveOrigin(ws, stored.originId);
|
|
341
|
+
if (!current || (current.ref.ownerId || null) !== (stored.ownerId || null) ||
|
|
342
|
+
(current.ref.linuxUser || null) !== (stored.linuxUser || null)) {
|
|
343
|
+
return { error: "A referenced session was deleted, replaced, or is no longer accessible", code: "session" };
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return { ok: true };
|
|
347
|
+
}
|
|
348
|
+
function visibleView(ws, view) {
|
|
349
|
+
var refs = [];
|
|
350
|
+
for (var i = 0; i < view.sessionRefs.length; i++) {
|
|
351
|
+
var stored = view.sessionRefs[i], resolved = resolveOrigin(ws, stored.originId);
|
|
352
|
+
if (!resolved || (resolved.ref.ownerId || null) !== (stored.ownerId || null) ||
|
|
353
|
+
(resolved.ref.linuxUser || null) !== (stored.linuxUser || null)) return null;
|
|
354
|
+
refs.push({ id: resolved.session.localId, sessionOriginId: resolved.session.sessionOriginId, title: resolved.session.title || "" });
|
|
355
|
+
}
|
|
356
|
+
var projected = names.project(view, refs); projected.tree = clone(view.tree); return projected;
|
|
357
|
+
}
|
|
358
|
+
function folderIds(ownerId) {
|
|
359
|
+
var accountId = users.isMultiUser() ? ownerId : 'default';
|
|
360
|
+
return typeof users.getSessionFolders === 'function' ? users.getSessionFolders(accountId, ctx.slug).folders.map(function (folder) { return folder.id; }) : [];
|
|
361
|
+
}
|
|
362
|
+
function stateFor(ws) {
|
|
363
|
+
var current = actor(ws);
|
|
364
|
+
if (!current) return null;
|
|
365
|
+
var state = store.reconcileFolders(current.id, current.linuxUser, folderIds(current.id));
|
|
366
|
+
if (!state.ok) return { owner: current, revision: state.revision, views: [], error: state.error };
|
|
367
|
+
var views = state.views.map(function (view) { return visibleView(ws, view); }).filter(Boolean);
|
|
368
|
+
return { owner: current, revision: state.revision, views: views };
|
|
369
|
+
}
|
|
370
|
+
function sendState(ws) {
|
|
371
|
+
var state = stateFor(ws);
|
|
372
|
+
if (!state) return false;
|
|
373
|
+
ctx.sendTo(ws, { type: "project_split_views", projectSlug: ctx.slug, revision: state.revision, views: state.views, error: state.error || null });
|
|
374
|
+
return true;
|
|
375
|
+
}
|
|
376
|
+
function broadcastOwner(ownerId) {
|
|
377
|
+
ctx.clients.forEach(function (ws) {
|
|
378
|
+
if (ws.readyState !== 1) return;
|
|
379
|
+
var current = actor(ws);
|
|
380
|
+
if (current && current.id === ownerId) sendState(ws);
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
function sendOwnerError(ownerId, error) {
|
|
384
|
+
ctx.clients.forEach(function (ws) {
|
|
385
|
+
if (ws.readyState !== 1) return;
|
|
386
|
+
var current = actor(ws);
|
|
387
|
+
if (!current || current.id !== ownerId) return;
|
|
388
|
+
var state = stateFor(ws);
|
|
389
|
+
if (state) ctx.sendTo(ws, { type: "project_split_views", projectSlug: ctx.slug, revision: state.revision,
|
|
390
|
+
views: state.views, error: error });
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
function serviceForConnection(ws) {
|
|
394
|
+
var original = actor(ws);
|
|
395
|
+
if (!original) return null;
|
|
396
|
+
function currentActor() {
|
|
397
|
+
var latest = actor(ws);
|
|
398
|
+
if (!latest || latest.id !== original.id || latest.linuxUser !== original.linuxUser) return null;
|
|
399
|
+
return latest;
|
|
400
|
+
}
|
|
401
|
+
function state() {
|
|
402
|
+
if (!currentActor()) return { ok: false, error: "Split view access is no longer permitted", code: "forbidden", views: [] };
|
|
403
|
+
return stateFor(ws);
|
|
404
|
+
}
|
|
405
|
+
function execute(action, input) {
|
|
406
|
+
var current = currentActor(), result;
|
|
407
|
+
if (!current) return { ok: false, error: "Split view access is no longer permitted", code: "forbidden" };
|
|
408
|
+
input = input || {};
|
|
409
|
+
var currentState = store.state(current.id, current.linuxUser);
|
|
410
|
+
if (!currentState.ok) return currentState;
|
|
411
|
+
if (!Number.isInteger(input.expectedRevision) || input.expectedRevision !== currentState.revision) {
|
|
412
|
+
return { ok: false, error: "Split view state changed. Reload and try again.", code: "conflict", revision: currentState.revision };
|
|
413
|
+
}
|
|
414
|
+
if (action === "create" || action === "update") {
|
|
415
|
+
var refs = resolvedRefs(ws, input.sessionOrigins);
|
|
416
|
+
if (!refs.refs) return refs;
|
|
417
|
+
input.liveSessions = refs.sessions;
|
|
418
|
+
if (action === "update") {
|
|
419
|
+
var exact = verifyStoredView(ws, current, input.id);
|
|
420
|
+
return exact.ok ? store.update(current.id, current.linuxUser, input, refs.refs) : exact;
|
|
421
|
+
}
|
|
422
|
+
input.nameMode = "automatic";
|
|
423
|
+
return store.create(current.id, current.linuxUser, input, refs.refs);
|
|
424
|
+
}
|
|
425
|
+
if (action === "move") {
|
|
426
|
+
var sourceState = store.state(current.id, current.linuxUser);
|
|
427
|
+
if (!sourceState.ok) return sourceState;
|
|
428
|
+
var sourceView = sourceState.views.find(function (view) { return view.id === input.sourceViewId; });
|
|
429
|
+
var sourceCheck = verifyStoredView(ws, current, input.sourceViewId);
|
|
430
|
+
if (!sourceCheck.ok || !sourceView) return sourceCheck.ok ? { error: "Source Split view not found", code: "not_found" } : sourceCheck;
|
|
431
|
+
var sourceResolved = resolvedRefs(ws, sourceView.sessionRefs.map(function (ref) { return ref.originId; }));
|
|
432
|
+
if (!sourceResolved.refs) return sourceResolved;
|
|
433
|
+
input.sourceLiveSessions = sourceResolved.sessions;
|
|
434
|
+
if (input.targetViewId) {
|
|
435
|
+
var targetCheck = verifyStoredView(ws, current, input.targetViewId);
|
|
436
|
+
if (!targetCheck.ok) return targetCheck;
|
|
437
|
+
}
|
|
438
|
+
var targetResolved = resolvedRefs(ws, input.targetSessionOrigins);
|
|
439
|
+
if (!targetResolved.refs) return targetResolved;
|
|
440
|
+
input.liveSessions = targetResolved.sessions;
|
|
441
|
+
return store.move(current.id, current.linuxUser, input, sourceResolved.refs, targetResolved.refs);
|
|
442
|
+
}
|
|
443
|
+
if (action === 'organize' && (input.projectSlug !== ctx.slug || input.folderId !== null && folderIds(current.id).indexOf(input.folderId) < 0)) return { ok: false, code: 'folder', error: 'That project folder is no longer available.' };
|
|
444
|
+
var exactView = verifyStoredView(ws, current, input.id);
|
|
445
|
+
return exactView.ok ? store[action](current.id, current.linuxUser, input) : exactView;
|
|
446
|
+
}
|
|
447
|
+
return { publish: function () { var current = currentActor(); if (!current) throw new Error("Split view access changed"); broadcastOwner(current.id); }, state: state, folders: function () { var current = currentActor(); if (!current) throw new Error("Split view access changed"); return users.getSessionFolders ? users.getSessionFolders(users.isMultiUser() ? current.id : "default", ctx.slug).folders : []; }, create: function (input) { return execute("create", input); },
|
|
448
|
+
update: function (input) { return execute("update", input); },
|
|
449
|
+
organize: function (input) { return execute('organize', input); },
|
|
450
|
+
rename: function (input) { return execute("rename", input); },
|
|
451
|
+
separate: function (input) { return execute("separate", input); },
|
|
452
|
+
move: function (input) { return execute("move", input); } };
|
|
453
|
+
}
|
|
454
|
+
function onSessionDeleted(localId, session) {
|
|
455
|
+
if (!session || !layout.validOrigin(session.sessionOriginId)) return;
|
|
456
|
+
var result = store.removeOrigin(session.sessionOriginId);
|
|
457
|
+
result.owners.forEach(broadcastOwner);
|
|
458
|
+
result.failures.forEach(function (failure) {
|
|
459
|
+
sendOwnerError(failure.ownerId, "A deleted session's Split view cleanup could not be persisted; affected layouts are unavailable until repaired.");
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
function handleMessage(ws, msg) {
|
|
463
|
+
var action = msg.type === "project_split_view_create" ? "create" : msg.type === "project_split_view_update" ? "update" :
|
|
464
|
+
msg.type === "project_split_view_rename" ? "rename" : msg.type === "project_split_view_separate" ? "separate" :
|
|
465
|
+
msg.type === "project_split_view_move" ? "move" : msg.type === "project_split_view_organize" ? "organize" : null;
|
|
466
|
+
if (msg.type === "project_split_views_request") { sendState(ws); return true; }
|
|
467
|
+
if (!action) return false;
|
|
468
|
+
var service = serviceForConnection(ws), result = service ? service[action](msg) : { ok: false, error: "Split view access is not permitted", code: "forbidden" };
|
|
469
|
+
ctx.sendTo(ws, { type: "project_split_view_result", action: action, requestId: typeof msg.requestId === "string" ? msg.requestId : null,
|
|
470
|
+
ok: !!result.ok, error: result.error || null, code: result.code || null, revision: result.revision,
|
|
471
|
+
view: result.ok && result.view ? visibleView(ws, result.view) : null });
|
|
472
|
+
var current = actor(ws);
|
|
473
|
+
if (result.ok && current) broadcastOwner(current.id);
|
|
474
|
+
return true;
|
|
475
|
+
}
|
|
476
|
+
if (ctx.sm && typeof ctx.sm.addOnSessionDeleted === "function") ctx.sm.addOnSessionDeleted(onSessionDeleted);
|
|
477
|
+
return { onFoldersChanged: function (accountId) { if (users.isMultiUser()) broadcastOwner(accountId); else ctx.clients.forEach(function (ws) { if (ws.readyState === 1) sendState(ws); }); }, handleMessage: handleMessage, sendConnectionState: sendState, forConnection: serviceForConnection, store: store };
|
|
478
|
+
}
|
|
479
|
+
module.exports = { createSplitViewsStore: createSplitViewsStore, attachProjectSplitViews: attachProjectSplitViews, validDocument: validDocument };
|