@qnote/q-ai-note 1.0.5 → 1.0.7
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 +41 -0
- package/dist/cli.js +55 -18
- package/dist/cli.js.map +1 -1
- package/dist/server/accessControl.d.ts +29 -0
- package/dist/server/accessControl.d.ts.map +1 -0
- package/dist/server/accessControl.js +161 -0
- package/dist/server/accessControl.js.map +1 -0
- package/dist/server/api/accessHelpers.d.ts +11 -0
- package/dist/server/api/accessHelpers.d.ts.map +1 -0
- package/dist/server/api/accessHelpers.js +45 -0
- package/dist/server/api/accessHelpers.js.map +1 -0
- package/dist/server/api/chat.d.ts.map +1 -1
- package/dist/server/api/chat.js +31 -0
- package/dist/server/api/chat.js.map +1 -1
- package/dist/server/api/diary.d.ts.map +1 -1
- package/dist/server/api/diary.js +61 -1
- package/dist/server/api/diary.js.map +1 -1
- package/dist/server/api/nodeEntities.d.ts.map +1 -1
- package/dist/server/api/nodeEntities.js +31 -0
- package/dist/server/api/nodeEntities.js.map +1 -1
- package/dist/server/api/projectSettings.d.ts +3 -0
- package/dist/server/api/projectSettings.d.ts.map +1 -0
- package/dist/server/api/projectSettings.js +29 -0
- package/dist/server/api/projectSettings.js.map +1 -0
- package/dist/server/api/sandbox.d.ts.map +1 -1
- package/dist/server/api/sandbox.js +35 -1
- package/dist/server/api/sandbox.js.map +1 -1
- package/dist/server/api/settings.d.ts.map +1 -1
- package/dist/server/api/settings.js +25 -1
- package/dist/server/api/settings.js.map +1 -1
- package/dist/server/api/workItem.d.ts.map +1 -1
- package/dist/server/api/workItem.js +59 -0
- package/dist/server/api/workItem.js.map +1 -1
- package/dist/server/config.d.ts +3 -2
- package/dist/server/config.d.ts.map +1 -1
- package/dist/server/config.js +6 -1
- package/dist/server/config.js.map +1 -1
- package/dist/server/index.d.ts +8 -2
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +102 -4
- package/dist/server/index.js.map +1 -1
- package/dist/server/projectConfig.d.ts +37 -0
- package/dist/server/projectConfig.d.ts.map +1 -0
- package/dist/server/projectConfig.js +180 -0
- package/dist/server/projectConfig.js.map +1 -0
- package/dist/web/app.js +760 -44
- package/dist/web/index.html +107 -60
- package/dist/web/styles.css +256 -11
- package/dist/web/vueRenderers.js +71 -57
- package/package.json +2 -2
package/dist/web/app.js
CHANGED
|
@@ -9,6 +9,20 @@ const state = {
|
|
|
9
9
|
operations: [],
|
|
10
10
|
chats: [],
|
|
11
11
|
settings: {},
|
|
12
|
+
readonly: false,
|
|
13
|
+
fullAccess: false,
|
|
14
|
+
canAccessSystemSettings: false,
|
|
15
|
+
pageAccess: {
|
|
16
|
+
sandboxes: true,
|
|
17
|
+
diaries: true,
|
|
18
|
+
changes: true,
|
|
19
|
+
settings: false,
|
|
20
|
+
},
|
|
21
|
+
sandboxAccessAll: 'read',
|
|
22
|
+
sandboxReadIds: [],
|
|
23
|
+
sandboxWriteIds: [],
|
|
24
|
+
currentSandboxWritable: false,
|
|
25
|
+
projectSettingsDraft: null,
|
|
12
26
|
pendingAction: null,
|
|
13
27
|
nodeEntities: [],
|
|
14
28
|
nodeEntityStats: null,
|
|
@@ -16,7 +30,8 @@ const state = {
|
|
|
16
30
|
nodeEntityFilter: 'all',
|
|
17
31
|
editingNodeEntityId: null,
|
|
18
32
|
nodeEntityFormExpanded: false,
|
|
19
|
-
|
|
33
|
+
sandboxChatVisible: false,
|
|
34
|
+
sandboxChatVisibleBeforeFullscreen: false,
|
|
20
35
|
sandboxFullscreenMode: false,
|
|
21
36
|
workTreeViewMode: 'full',
|
|
22
37
|
workItemElementPreviewMode: 'none',
|
|
@@ -38,6 +53,7 @@ let sandboxActionHandler = null;
|
|
|
38
53
|
let sandboxEscLocked = false;
|
|
39
54
|
let resizeRenderTimer = null;
|
|
40
55
|
const WORK_ITEM_SHOW_ASSIGNEE_STORAGE_KEY = 'q-ai-note.work-item.show-assignee';
|
|
56
|
+
const WORK_TREE_VIEW_MODE_STORAGE_KEY = 'q-ai-note.work-tree.view-mode';
|
|
41
57
|
|
|
42
58
|
function loadWorkItemAssigneePreference() {
|
|
43
59
|
try {
|
|
@@ -60,14 +76,132 @@ function persistWorkItemAssigneePreference() {
|
|
|
60
76
|
}
|
|
61
77
|
}
|
|
62
78
|
|
|
63
|
-
function
|
|
79
|
+
function loadWorkTreeViewModePreference() {
|
|
80
|
+
try {
|
|
81
|
+
const raw = String(window.localStorage.getItem(WORK_TREE_VIEW_MODE_STORAGE_KEY) || '').trim();
|
|
82
|
+
if (raw === 'full' || raw === 'report' || raw === 'dense') {
|
|
83
|
+
state.workTreeViewMode = raw;
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
86
|
+
// Ignore storage failures in restricted environments.
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function persistWorkTreeViewModePreference() {
|
|
91
|
+
try {
|
|
92
|
+
window.localStorage.setItem(WORK_TREE_VIEW_MODE_STORAGE_KEY, state.workTreeViewMode || 'full');
|
|
93
|
+
} catch {
|
|
94
|
+
// Ignore storage failures in restricted environments.
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function applySandboxChatVisibility() {
|
|
64
99
|
const layout = document.getElementById('sandbox-layout');
|
|
65
|
-
const toggleBtn = document.getElementById('toggle-sandbox-
|
|
100
|
+
const toggleBtn = document.getElementById('toggle-sandbox-chat-btn');
|
|
66
101
|
if (!layout || !toggleBtn) return;
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
toggleBtn.
|
|
102
|
+
const chatAllowed = !state.readonly && state.currentSandboxWritable;
|
|
103
|
+
const shouldShow = chatAllowed && Boolean(state.sandboxChatVisible);
|
|
104
|
+
layout.classList.toggle('show-chat', shouldShow);
|
|
105
|
+
toggleBtn.classList.toggle('hidden', !chatAllowed);
|
|
106
|
+
toggleBtn.innerHTML = shouldShow
|
|
107
|
+
? '<span class="icon" aria-hidden="true">🤖</span><span>隐藏 AI 助手</span>'
|
|
108
|
+
: '<span class="icon" aria-hidden="true">🤖</span><span>显示 AI 助手</span>';
|
|
109
|
+
toggleBtn.setAttribute('aria-pressed', shouldShow ? 'true' : 'false');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function loadRuntimeMode() {
|
|
113
|
+
try {
|
|
114
|
+
const runtime = await apiRequest(`${API_BASE}/runtime`);
|
|
115
|
+
state.readonly = Boolean(runtime?.readonly);
|
|
116
|
+
state.fullAccess = Boolean(runtime?.full_access);
|
|
117
|
+
state.canAccessSystemSettings = Boolean(runtime?.can_access_system_settings);
|
|
118
|
+
state.pageAccess = {
|
|
119
|
+
sandboxes: Boolean(runtime?.page_access?.sandboxes),
|
|
120
|
+
diaries: Boolean(runtime?.page_access?.diaries),
|
|
121
|
+
changes: Boolean(runtime?.page_access?.changes),
|
|
122
|
+
settings: Boolean(runtime?.page_access?.settings),
|
|
123
|
+
};
|
|
124
|
+
state.sandboxAccessAll = String(runtime?.sandbox_access_all || 'none');
|
|
125
|
+
state.sandboxReadIds = Array.isArray(runtime?.sandbox_read_ids) ? runtime.sandbox_read_ids.map((id) => String(id)) : [];
|
|
126
|
+
state.sandboxWriteIds = Array.isArray(runtime?.sandbox_write_ids) ? runtime.sandbox_write_ids.map((id) => String(id)) : [];
|
|
127
|
+
state.currentSandboxWritable = !state.readonly && (state.fullAccess || state.sandboxAccessAll === 'write');
|
|
128
|
+
} catch {
|
|
129
|
+
state.readonly = false;
|
|
130
|
+
state.fullAccess = true;
|
|
131
|
+
state.canAccessSystemSettings = true;
|
|
132
|
+
state.pageAccess = { sandboxes: true, diaries: true, changes: true, settings: true };
|
|
133
|
+
state.sandboxAccessAll = 'write';
|
|
134
|
+
state.sandboxReadIds = [];
|
|
135
|
+
state.sandboxWriteIds = [];
|
|
136
|
+
state.currentSandboxWritable = true;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function canReadSandboxById(sandboxId) {
|
|
141
|
+
const id = String(sandboxId || '');
|
|
142
|
+
if (!id) return false;
|
|
143
|
+
if (state.fullAccess) return true;
|
|
144
|
+
if (state.sandboxAccessAll === 'read' || state.sandboxAccessAll === 'write') return true;
|
|
145
|
+
return state.sandboxReadIds.includes(id) || state.sandboxWriteIds.includes(id);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function canWriteSandboxById(sandboxId) {
|
|
149
|
+
const id = String(sandboxId || '');
|
|
150
|
+
if (!id) return false;
|
|
151
|
+
if (state.readonly) return false;
|
|
152
|
+
if (state.fullAccess) return true;
|
|
153
|
+
if (state.sandboxAccessAll === 'write') return true;
|
|
154
|
+
return state.sandboxWriteIds.includes(id);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function setHiddenById(id, hidden = true) {
|
|
158
|
+
const el = document.getElementById(id);
|
|
159
|
+
if (!el) return;
|
|
160
|
+
el.classList.toggle('hidden', hidden);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function applyReadonlyMode() {
|
|
164
|
+
document.body.classList.toggle('readonly-mode', state.readonly);
|
|
165
|
+
const sandboxesNav = document.querySelector('[data-nav="sandboxes"]');
|
|
166
|
+
if (sandboxesNav instanceof HTMLElement) {
|
|
167
|
+
sandboxesNav.classList.toggle('hidden', !state.pageAccess.sandboxes && !state.fullAccess);
|
|
168
|
+
}
|
|
169
|
+
const diariesNav = document.querySelector('[data-nav="diaries"]');
|
|
170
|
+
if (diariesNav instanceof HTMLElement) {
|
|
171
|
+
diariesNav.classList.toggle('hidden', !state.pageAccess.diaries && !state.fullAccess);
|
|
172
|
+
}
|
|
173
|
+
const changesNav = document.querySelector('[data-nav="changes"]');
|
|
174
|
+
if (changesNav instanceof HTMLElement) {
|
|
175
|
+
changesNav.classList.toggle('hidden', !state.pageAccess.changes && !state.fullAccess);
|
|
176
|
+
}
|
|
177
|
+
const projectSettingsNav = document.querySelector('[data-nav="settings"]');
|
|
178
|
+
if (projectSettingsNav instanceof HTMLElement) {
|
|
179
|
+
projectSettingsNav.classList.toggle('hidden', !state.pageAccess.settings);
|
|
180
|
+
}
|
|
181
|
+
const systemSettingsNav = document.querySelector('[data-nav="system-settings"]');
|
|
182
|
+
if (systemSettingsNav instanceof HTMLElement) {
|
|
183
|
+
systemSettingsNav.classList.toggle('hidden', !state.canAccessSystemSettings);
|
|
184
|
+
}
|
|
185
|
+
setHiddenById('page-settings', !state.pageAccess.settings);
|
|
186
|
+
setHiddenById('page-system-settings', !state.canAccessSystemSettings);
|
|
187
|
+
setHiddenById('add-sandbox-btn', state.readonly || !state.fullAccess);
|
|
188
|
+
setHiddenById('add-item-btn', state.readonly || !state.currentSandboxWritable);
|
|
189
|
+
setHiddenById('toggle-node-entity-form-btn', state.readonly || !state.currentSandboxWritable);
|
|
190
|
+
setHiddenById('drawer-diary-save-btn', state.readonly || !state.currentSandboxWritable);
|
|
191
|
+
setHiddenById('save-diary-btn', state.readonly);
|
|
192
|
+
const diaryForm = document.querySelector('#page-diaries .diary-form');
|
|
193
|
+
if (diaryForm instanceof HTMLElement) {
|
|
194
|
+
diaryForm.classList.toggle('hidden', state.readonly);
|
|
195
|
+
}
|
|
196
|
+
const drawerDiaryForm = document.querySelector('#node-entity-drawer .drawer-diary-quick-form');
|
|
197
|
+
if (drawerDiaryForm instanceof HTMLElement) {
|
|
198
|
+
drawerDiaryForm.classList.toggle('hidden', state.readonly || !state.currentSandboxWritable);
|
|
199
|
+
}
|
|
200
|
+
if (state.readonly) {
|
|
201
|
+
state.sandboxChatVisible = false;
|
|
202
|
+
closeQuickChatPopover();
|
|
203
|
+
}
|
|
204
|
+
applySandboxChatVisibility();
|
|
71
205
|
}
|
|
72
206
|
|
|
73
207
|
function getSandboxLayoutElement() {
|
|
@@ -100,7 +234,14 @@ function applySandboxFullscreenState() {
|
|
|
100
234
|
const layout = getSandboxLayoutElement();
|
|
101
235
|
const fullscreenRoot = getSandboxFullscreenElement();
|
|
102
236
|
const toggleBtn = document.getElementById('toggle-sandbox-fullscreen-btn');
|
|
237
|
+
const wasFullscreen = Boolean(state.sandboxFullscreenMode);
|
|
103
238
|
const isFullscreen = Boolean(fullscreenRoot && document.fullscreenElement === fullscreenRoot);
|
|
239
|
+
if (!wasFullscreen && isFullscreen) {
|
|
240
|
+
state.sandboxChatVisibleBeforeFullscreen = Boolean(state.sandboxChatVisible);
|
|
241
|
+
state.sandboxChatVisible = false;
|
|
242
|
+
} else if (wasFullscreen && !isFullscreen) {
|
|
243
|
+
state.sandboxChatVisible = Boolean(state.sandboxChatVisibleBeforeFullscreen);
|
|
244
|
+
}
|
|
104
245
|
state.sandboxFullscreenMode = isFullscreen;
|
|
105
246
|
if (layout) {
|
|
106
247
|
layout.classList.toggle('is-fullscreen', isFullscreen);
|
|
@@ -112,9 +253,22 @@ function applySandboxFullscreenState() {
|
|
|
112
253
|
toggleBtn.textContent = isFullscreen ? '退出全屏' : '全屏';
|
|
113
254
|
toggleBtn.setAttribute('aria-pressed', isFullscreen ? 'true' : 'false');
|
|
114
255
|
}
|
|
256
|
+
applySandboxChatVisibility();
|
|
257
|
+
applySandboxLayoutHeight();
|
|
115
258
|
void syncSandboxEscLock(isFullscreen);
|
|
116
259
|
}
|
|
117
260
|
|
|
261
|
+
function applySandboxLayoutHeight() {
|
|
262
|
+
const layout = getSandboxLayoutElement();
|
|
263
|
+
if (!(layout instanceof HTMLElement)) return;
|
|
264
|
+
const rect = layout.getBoundingClientRect();
|
|
265
|
+
const viewportHeight = Math.floor(window.visualViewport?.height || window.innerHeight || 0);
|
|
266
|
+
if (!Number.isFinite(viewportHeight) || viewportHeight <= 0) return;
|
|
267
|
+
const bottomGap = state.sandboxFullscreenMode ? 8 : 10;
|
|
268
|
+
const nextHeight = Math.max(320, viewportHeight - Math.floor(rect.top) - bottomGap);
|
|
269
|
+
layout.style.height = `${nextHeight}px`;
|
|
270
|
+
}
|
|
271
|
+
|
|
118
272
|
async function toggleSandboxFullscreen() {
|
|
119
273
|
const fullscreenRoot = getSandboxFullscreenElement();
|
|
120
274
|
if (!fullscreenRoot) return;
|
|
@@ -215,6 +369,7 @@ function applyWorkItemElementPreviewMode() {
|
|
|
215
369
|
function renderWorkTree() {
|
|
216
370
|
const tree = document.getElementById('work-tree');
|
|
217
371
|
if (!tree || !state.currentSandbox) return;
|
|
372
|
+
const treeReadonly = state.readonly || !state.currentSandboxWritable;
|
|
218
373
|
|
|
219
374
|
const allItems = state.currentSandbox.items || [];
|
|
220
375
|
const items = applyWorkItemFilters(allItems);
|
|
@@ -230,7 +385,7 @@ function renderWorkTree() {
|
|
|
230
385
|
}
|
|
231
386
|
|
|
232
387
|
if (allItems.length === 0) {
|
|
233
|
-
tree.innerHTML =
|
|
388
|
+
tree.innerHTML = `<div class="empty-state"><p>${treeReadonly ? '暂无任务' : '点击上方"添加"按钮创建第一个任务'}</p></div>`;
|
|
234
389
|
return;
|
|
235
390
|
}
|
|
236
391
|
|
|
@@ -246,7 +401,7 @@ function renderWorkTree() {
|
|
|
246
401
|
}
|
|
247
402
|
renderWorkTree();
|
|
248
403
|
},
|
|
249
|
-
onAddChild: (parentId) => {
|
|
404
|
+
onAddChild: treeReadonly ? undefined : (parentId) => {
|
|
250
405
|
document.getElementById('item-dialog-title').textContent = '添加子任务';
|
|
251
406
|
document.getElementById('item-dialog').dataset.editId = '';
|
|
252
407
|
document.getElementById('new-item-name').value = '';
|
|
@@ -257,14 +412,14 @@ function renderWorkTree() {
|
|
|
257
412
|
document.getElementById('new-item-parent').value = parentId;
|
|
258
413
|
document.getElementById('item-dialog').showModal();
|
|
259
414
|
},
|
|
260
|
-
onAddDiary: (nodeId) => {
|
|
415
|
+
onAddDiary: treeReadonly ? undefined : (nodeId) => {
|
|
261
416
|
showNodeEntityDrawer(nodeId, 'diary');
|
|
262
417
|
const textarea = document.getElementById('drawer-diary-content');
|
|
263
418
|
if (textarea instanceof HTMLTextAreaElement) {
|
|
264
419
|
textarea.focus();
|
|
265
420
|
}
|
|
266
421
|
},
|
|
267
|
-
onEdit: (id) => {
|
|
422
|
+
onEdit: treeReadonly ? undefined : (id) => {
|
|
268
423
|
editWorkItem(id);
|
|
269
424
|
},
|
|
270
425
|
onSelect: (id) => {
|
|
@@ -273,7 +428,7 @@ function renderWorkTree() {
|
|
|
273
428
|
onSelectEntity: (nodeId, entityType) => {
|
|
274
429
|
showNodeEntityDrawer(nodeId, entityType || 'all');
|
|
275
430
|
},
|
|
276
|
-
onMoveNode: async (dragNodeId, newParentId) => {
|
|
431
|
+
onMoveNode: treeReadonly ? undefined : async (dragNodeId, newParentId) => {
|
|
277
432
|
if (!state.currentSandbox) return;
|
|
278
433
|
if (!dragNodeId || dragNodeId === newParentId) return;
|
|
279
434
|
const byId = new Map((state.currentSandbox.items || []).map((item) => [item.id, item]));
|
|
@@ -301,7 +456,7 @@ function renderWorkTree() {
|
|
|
301
456
|
});
|
|
302
457
|
await loadSandbox(state.currentSandbox.id);
|
|
303
458
|
},
|
|
304
|
-
onReorderSiblings: async (dragNodeId, targetNodeId, position) => {
|
|
459
|
+
onReorderSiblings: treeReadonly ? undefined : async (dragNodeId, targetNodeId, position) => {
|
|
305
460
|
if (!state.currentSandbox) return;
|
|
306
461
|
if (!dragNodeId || !targetNodeId || dragNodeId === targetNodeId) return;
|
|
307
462
|
const byId = new Map((state.currentSandbox.items || []).map((item) => [item.id, item]));
|
|
@@ -334,7 +489,7 @@ function renderWorkTree() {
|
|
|
334
489
|
});
|
|
335
490
|
await loadSandbox(state.currentSandbox.id);
|
|
336
491
|
},
|
|
337
|
-
onReorderLanes: async (dragRootId, targetRootId) => {
|
|
492
|
+
onReorderLanes: treeReadonly ? undefined : async (dragRootId, targetRootId) => {
|
|
338
493
|
if (!state.currentSandbox) return;
|
|
339
494
|
if (!dragRootId || !targetRootId || dragRootId === targetRootId) return;
|
|
340
495
|
const roots = (state.currentSandbox.items || []).filter((item) => !item.parent_id);
|
|
@@ -359,19 +514,21 @@ function renderWorkTree() {
|
|
|
359
514
|
});
|
|
360
515
|
await loadSandbox(state.currentSandbox.id);
|
|
361
516
|
},
|
|
362
|
-
onDelete: async (id) => {
|
|
517
|
+
onDelete: treeReadonly ? undefined : async (id) => {
|
|
363
518
|
if (confirm('确定删除此任务?')) {
|
|
364
519
|
await apiRequest(`${API_BASE}/items/${id}`, { method: 'DELETE' });
|
|
365
520
|
await loadSandbox(state.currentSandbox.id);
|
|
366
521
|
}
|
|
367
522
|
},
|
|
368
|
-
onQuickChat: (id, el) => {
|
|
523
|
+
onQuickChat: treeReadonly ? undefined : (id, el) => {
|
|
369
524
|
openQuickChatPopover(id, el);
|
|
370
525
|
},
|
|
371
526
|
renderMode: state.workTreeViewMode === 'dense' ? 'dense' : 'card',
|
|
372
527
|
showAssignee: state.workItemShowAssignee,
|
|
373
528
|
elementPreviewMode: state.workItemElementPreviewMode,
|
|
374
529
|
entityRowsByNodeId,
|
|
530
|
+
readonly: treeReadonly,
|
|
531
|
+
selectedId: state.selectedNodeId || '',
|
|
375
532
|
});
|
|
376
533
|
|
|
377
534
|
populateParentSelect(allItems);
|
|
@@ -481,12 +638,17 @@ function compareSiblingOrder(a, b, keyName = 'order_key') {
|
|
|
481
638
|
return String(a?.created_at || '').localeCompare(String(b?.created_at || ''));
|
|
482
639
|
}
|
|
483
640
|
|
|
484
|
-
function populateParentSelect(items) {
|
|
641
|
+
function populateParentSelect(items, preferredParentId = null) {
|
|
485
642
|
const select = document.getElementById('new-item-parent');
|
|
486
643
|
if (!select) return;
|
|
487
|
-
|
|
644
|
+
const expectedValue = preferredParentId === null || preferredParentId === undefined
|
|
645
|
+
? String(select.value || '')
|
|
646
|
+
: String(preferredParentId || '');
|
|
647
|
+
|
|
488
648
|
select.innerHTML = '<option value="">无(顶级)</option>' +
|
|
489
649
|
items.map(i => `<option value="${i.id}">${safeText(i.name)}</option>`).join('');
|
|
650
|
+
const hasExpectedValue = Array.from(select.options).some((option) => option.value === expectedValue);
|
|
651
|
+
select.value = hasExpectedValue ? expectedValue : '';
|
|
490
652
|
}
|
|
491
653
|
|
|
492
654
|
function renderMarkdownSnippet(text) {
|
|
@@ -674,6 +836,7 @@ function closeNodeEntityDrawer() {
|
|
|
674
836
|
state.nodeEntityFilter = 'all';
|
|
675
837
|
state.editingNodeEntityId = null;
|
|
676
838
|
state.nodeEntityFormExpanded = false;
|
|
839
|
+
renderWorkTree();
|
|
677
840
|
}
|
|
678
841
|
|
|
679
842
|
function setNodeEntityFormExpanded(expanded) {
|
|
@@ -755,13 +918,15 @@ function renderNodeEntityList(nodeId) {
|
|
|
755
918
|
<span class="entity-type-pill">diary</span>
|
|
756
919
|
<strong>${safeText('日志记录')}</strong>
|
|
757
920
|
</div>
|
|
758
|
-
|
|
759
|
-
<
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
921
|
+
${state.readonly ? '' : `
|
|
922
|
+
<div class="entity-card-actions">
|
|
923
|
+
<button class="btn btn-secondary btn-sm" data-diary-edit-id="${safeText(row.id)}">编辑</button>
|
|
924
|
+
${row.processed ? '' : `
|
|
925
|
+
<button class="btn btn-secondary btn-sm" data-diary-confirm-id="${safeText(row.id)}">采纳</button>
|
|
926
|
+
<button class="btn btn-secondary btn-sm" data-diary-ignore-id="${safeText(row.id)}">忽略</button>
|
|
927
|
+
`}
|
|
928
|
+
</div>
|
|
929
|
+
`}
|
|
765
930
|
</div>
|
|
766
931
|
<div class="entity-meta">
|
|
767
932
|
${safeText(new Date(row.created_at).toLocaleString())}
|
|
@@ -778,10 +943,12 @@ function renderNodeEntityList(nodeId) {
|
|
|
778
943
|
<span class="entity-type-pill">${safeText(row.entity_type)}</span>
|
|
779
944
|
<strong>${safeText(row.title || '-')}</strong>
|
|
780
945
|
</div>
|
|
781
|
-
|
|
782
|
-
<
|
|
783
|
-
|
|
784
|
-
|
|
946
|
+
${state.readonly ? '' : `
|
|
947
|
+
<div class="entity-card-actions">
|
|
948
|
+
<button class="btn btn-secondary btn-sm" data-entity-edit-id="${safeText(row.id)}">编辑</button>
|
|
949
|
+
<button class="btn btn-secondary btn-sm" data-entity-delete-id="${safeText(row.id)}">删除</button>
|
|
950
|
+
</div>
|
|
951
|
+
`}
|
|
785
952
|
</div>
|
|
786
953
|
<div class="entity-meta">
|
|
787
954
|
${safeText(new Date(row.created_at).toLocaleString())}
|
|
@@ -920,6 +1087,7 @@ function showNodeEntityDrawer(nodeId, preferredFilter = 'all') {
|
|
|
920
1087
|
state.nodeEntityFilter = filter;
|
|
921
1088
|
title.textContent = node.name || nodeId;
|
|
922
1089
|
renderNodeEntitySummary(nodeId);
|
|
1090
|
+
renderWorkTree();
|
|
923
1091
|
renderQuickDiaryTargetLabel();
|
|
924
1092
|
renderNodeEntityFilterTabs();
|
|
925
1093
|
renderNodeEntityList(nodeId);
|
|
@@ -1031,6 +1199,7 @@ function composeQuickChatContent(nodeId, userQuestion) {
|
|
|
1031
1199
|
}
|
|
1032
1200
|
|
|
1033
1201
|
async function sendSandboxChatMessage(content, options = {}) {
|
|
1202
|
+
if (state.readonly) return;
|
|
1034
1203
|
if (!content || !state.currentSandbox) return;
|
|
1035
1204
|
const messages = document.getElementById('sandbox-chat-messages');
|
|
1036
1205
|
const btn = document.getElementById('sandbox-send-btn');
|
|
@@ -1189,6 +1358,13 @@ function openQuickChatPopover(nodeId, anchorEl) {
|
|
|
1189
1358
|
}
|
|
1190
1359
|
|
|
1191
1360
|
async function loadSandboxes() {
|
|
1361
|
+
if (!state.pageAccess.sandboxes && !state.fullAccess) {
|
|
1362
|
+
state.sandboxes = [];
|
|
1363
|
+
renderSandboxes();
|
|
1364
|
+
renderSandboxesSummary();
|
|
1365
|
+
updateSandboxSelect();
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1192
1368
|
state.sandboxes = await apiRequest(`${API_BASE}/sandboxes`);
|
|
1193
1369
|
renderSandboxes();
|
|
1194
1370
|
renderSandboxesSummary();
|
|
@@ -1221,17 +1397,19 @@ function renderSandboxes() {
|
|
|
1221
1397
|
onOpen: (id) => {
|
|
1222
1398
|
window.location.hash = `/sandbox/${id}`;
|
|
1223
1399
|
},
|
|
1224
|
-
onDelete: async (id) => {
|
|
1400
|
+
onDelete: (state.readonly || !state.fullAccess) ? undefined : async (id) => {
|
|
1225
1401
|
if (confirm('确定删除此沙盘?')) {
|
|
1226
1402
|
await apiRequest(`${API_BASE}/sandboxes/${id}`, { method: 'DELETE' });
|
|
1227
1403
|
await loadSandboxes();
|
|
1228
1404
|
}
|
|
1229
|
-
}
|
|
1405
|
+
},
|
|
1406
|
+
readonly: state.readonly || !state.fullAccess,
|
|
1230
1407
|
});
|
|
1231
1408
|
}
|
|
1232
1409
|
|
|
1233
1410
|
async function loadSandbox(id) {
|
|
1234
1411
|
closeQuickChatPopover();
|
|
1412
|
+
state.currentSandboxWritable = canWriteSandboxById(id);
|
|
1235
1413
|
const [sandbox, items, diaries, entities, entityStats] = await Promise.all([
|
|
1236
1414
|
apiRequest(`${API_BASE}/sandboxes/${id}`),
|
|
1237
1415
|
apiRequest(`${API_BASE}/sandboxes/${id}/items`),
|
|
@@ -1247,12 +1425,20 @@ async function loadSandbox(id) {
|
|
|
1247
1425
|
applyWorkTreeViewMode(state.workTreeViewMode || 'full');
|
|
1248
1426
|
applyWorkItemAssigneeToggle();
|
|
1249
1427
|
applyWorkItemElementPreviewMode();
|
|
1250
|
-
|
|
1428
|
+
applySandboxChatVisibility();
|
|
1251
1429
|
applySandboxFullscreenState();
|
|
1252
1430
|
renderQuickDiaryTargetLabel();
|
|
1253
1431
|
renderSandboxOverview();
|
|
1254
1432
|
renderWorkTree();
|
|
1255
|
-
|
|
1433
|
+
applySandboxLayoutHeight();
|
|
1434
|
+
applyReadonlyMode();
|
|
1435
|
+
if (state.readonly || !state.currentSandboxWritable) {
|
|
1436
|
+
state.chats = [];
|
|
1437
|
+
const messages = document.getElementById('sandbox-chat-messages');
|
|
1438
|
+
if (messages) messages.innerHTML = '';
|
|
1439
|
+
} else {
|
|
1440
|
+
loadSandboxChats(id);
|
|
1441
|
+
}
|
|
1256
1442
|
}
|
|
1257
1443
|
|
|
1258
1444
|
function renderSandboxOverview() {
|
|
@@ -1277,6 +1463,7 @@ function renderSandboxOverview() {
|
|
|
1277
1463
|
}
|
|
1278
1464
|
|
|
1279
1465
|
async function loadSandboxChats(sandboxId) {
|
|
1466
|
+
if (state.readonly) return;
|
|
1280
1467
|
const messages = document.getElementById('sandbox-chat-messages');
|
|
1281
1468
|
if (!messages) return;
|
|
1282
1469
|
|
|
@@ -1436,25 +1623,26 @@ function renderDiaries() {
|
|
|
1436
1623
|
openDiaryWorkItemInSandbox(sandboxId, workItemId);
|
|
1437
1624
|
},
|
|
1438
1625
|
renderContent: (content) => renderMarkdownSnippet(content),
|
|
1439
|
-
onConfirm: async (id) => {
|
|
1626
|
+
onConfirm: state.readonly ? undefined : async (id) => {
|
|
1440
1627
|
await apiRequest(`${API_BASE}/diaries/${id}/process`, {
|
|
1441
1628
|
method: 'PUT',
|
|
1442
1629
|
body: JSON.stringify({ action: 'confirm' }),
|
|
1443
1630
|
});
|
|
1444
1631
|
await loadDiaries();
|
|
1445
1632
|
},
|
|
1446
|
-
onIgnore: async (id) => {
|
|
1633
|
+
onIgnore: state.readonly ? undefined : async (id) => {
|
|
1447
1634
|
await apiRequest(`${API_BASE}/diaries/${id}/process`, {
|
|
1448
1635
|
method: 'PUT',
|
|
1449
1636
|
body: JSON.stringify({ action: 'ignore' }),
|
|
1450
1637
|
});
|
|
1451
1638
|
await loadDiaries();
|
|
1452
1639
|
},
|
|
1453
|
-
onEdit: async (id) => {
|
|
1640
|
+
onEdit: state.readonly ? undefined : async (id) => {
|
|
1454
1641
|
const diary = state.diaries.find((row) => row.id === id);
|
|
1455
1642
|
if (!diary) return;
|
|
1456
1643
|
openDiaryEditDialog(diary);
|
|
1457
1644
|
},
|
|
1645
|
+
readonly: state.readonly,
|
|
1458
1646
|
});
|
|
1459
1647
|
}
|
|
1460
1648
|
|
|
@@ -1741,6 +1929,252 @@ async function loadSettings() {
|
|
|
1741
1929
|
apiKeyInput.placeholder = state.settings.has_api_key ? '已配置,留空表示不修改' : 'sk-...';
|
|
1742
1930
|
document.getElementById('setting-model').value = state.settings.model || '';
|
|
1743
1931
|
renderSettingHeadersRows(state.settings.headers || {});
|
|
1932
|
+
const editableIpInput = document.getElementById('setting-editable-ips');
|
|
1933
|
+
if (editableIpInput instanceof HTMLTextAreaElement) {
|
|
1934
|
+
editableIpInput.value = Array.isArray(state.settings.editable_ip_allowlist)
|
|
1935
|
+
? state.settings.editable_ip_allowlist.join('\n')
|
|
1936
|
+
: '';
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
function normalizeProjectSettingsDraft(config) {
|
|
1941
|
+
const users = Array.isArray(config?.users) ? config.users : [];
|
|
1942
|
+
const profiles = Array.isArray(config?.profiles) ? config.profiles : [];
|
|
1943
|
+
const bindings = Array.isArray(config?.bindings) ? config.bindings : [];
|
|
1944
|
+
return {
|
|
1945
|
+
users: users.map((user, idx) => ({
|
|
1946
|
+
id: String(user?.id || `user-${idx + 1}`).trim(),
|
|
1947
|
+
name: String(user?.name || '').trim(),
|
|
1948
|
+
ips: Array.isArray(user?.ips) ? user.ips.map((ip) => String(ip || '').trim()).filter((ip) => Boolean(ip)) : [],
|
|
1949
|
+
})),
|
|
1950
|
+
profiles: profiles.map((profile, idx) => ({
|
|
1951
|
+
id: String(profile?.id || `profile-${idx + 1}`).trim(),
|
|
1952
|
+
name: String(profile?.name || '').trim(),
|
|
1953
|
+
apply_to_all_users: Boolean(profile?.apply_to_all_users),
|
|
1954
|
+
page_access: {
|
|
1955
|
+
sandboxes: Boolean(profile?.page_access?.sandboxes),
|
|
1956
|
+
diaries: Boolean(profile?.page_access?.diaries),
|
|
1957
|
+
changes: Boolean(profile?.page_access?.changes),
|
|
1958
|
+
settings: Boolean(profile?.page_access?.settings),
|
|
1959
|
+
},
|
|
1960
|
+
sandbox_access: {
|
|
1961
|
+
all: String(profile?.sandbox_access?.all || 'none'),
|
|
1962
|
+
items: Array.isArray(profile?.sandbox_access?.items)
|
|
1963
|
+
? profile.sandbox_access.items.map((item) => ({
|
|
1964
|
+
sandbox_id: String(item?.sandbox_id || '').trim(),
|
|
1965
|
+
access: String(item?.access || 'read'),
|
|
1966
|
+
}))
|
|
1967
|
+
: [],
|
|
1968
|
+
},
|
|
1969
|
+
})),
|
|
1970
|
+
bindings: bindings.map((binding) => ({
|
|
1971
|
+
user_id: String(binding?.user_id || '').trim(),
|
|
1972
|
+
profile_ids: Array.isArray(binding?.profile_ids)
|
|
1973
|
+
? binding.profile_ids.map((id) => String(id || '').trim()).filter((id) => Boolean(id))
|
|
1974
|
+
: [],
|
|
1975
|
+
})),
|
|
1976
|
+
};
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
function renderProjectUsersEditor() {
|
|
1980
|
+
const container = document.getElementById('project-users-list');
|
|
1981
|
+
if (!(container instanceof HTMLElement)) return;
|
|
1982
|
+
const users = state.projectSettingsDraft?.users || [];
|
|
1983
|
+
if (!users.length) {
|
|
1984
|
+
container.innerHTML = '<div class="project-empty">暂无用户,点击上方“添加用户”。</div>';
|
|
1985
|
+
return;
|
|
1986
|
+
}
|
|
1987
|
+
container.innerHTML = users.map((user, idx) => `
|
|
1988
|
+
<div class="project-row">
|
|
1989
|
+
<input type="text" data-role="project-user-id" data-idx="${idx}" placeholder="用户 ID(唯一)" value="${escapeHtml(user.id)}">
|
|
1990
|
+
<input type="text" data-role="project-user-name" data-idx="${idx}" placeholder="显示名称" value="${escapeHtml(user.name)}">
|
|
1991
|
+
<div class="project-ip-editor">
|
|
1992
|
+
<div class="project-ip-chips">
|
|
1993
|
+
${(user.ips || []).map((ip, ipIdx) => `
|
|
1994
|
+
<span class="project-ip-chip">
|
|
1995
|
+
<span>${escapeHtml(ip)}</span>
|
|
1996
|
+
<button class="project-ip-remove" type="button" title="删除 IP" data-role="remove-project-user-ip" data-idx="${idx}" data-ip-idx="${ipIdx}">×</button>
|
|
1997
|
+
</span>
|
|
1998
|
+
`).join('')}
|
|
1999
|
+
</div>
|
|
2000
|
+
<div class="project-ip-input-row">
|
|
2001
|
+
<input type="text" data-role="project-user-ip-input" data-idx="${idx}" placeholder="输入 IP 后回车或点击添加">
|
|
2002
|
+
<button class="btn btn-secondary btn-sm" data-role="add-project-user-ip" data-idx="${idx}" type="button">添加 IP</button>
|
|
2003
|
+
</div>
|
|
2004
|
+
</div>
|
|
2005
|
+
<button class="btn btn-secondary btn-sm" data-role="remove-project-user" data-idx="${idx}" type="button">删除</button>
|
|
2006
|
+
</div>
|
|
2007
|
+
`).join('');
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
function addUserIpAtIndex(userIdx, rawIp) {
|
|
2011
|
+
if (!state.projectSettingsDraft) return;
|
|
2012
|
+
const idx = Number(userIdx);
|
|
2013
|
+
if (!Number.isFinite(idx) || idx < 0) return;
|
|
2014
|
+
const user = state.projectSettingsDraft.users[idx];
|
|
2015
|
+
if (!user) return;
|
|
2016
|
+
const ip = String(rawIp || '').trim();
|
|
2017
|
+
if (!ip) return;
|
|
2018
|
+
if (ip.includes('/')) {
|
|
2019
|
+
alert('仅支持精确 IP,不支持 CIDR。');
|
|
2020
|
+
return;
|
|
2021
|
+
}
|
|
2022
|
+
if ((user.ips || []).includes(ip)) return;
|
|
2023
|
+
user.ips = [...(user.ips || []), ip];
|
|
2024
|
+
renderProjectSettingsEditor();
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
function renderProjectProfilesEditor() {
|
|
2028
|
+
const container = document.getElementById('project-profiles-list');
|
|
2029
|
+
if (!(container instanceof HTMLElement)) return;
|
|
2030
|
+
const profiles = state.projectSettingsDraft?.profiles || [];
|
|
2031
|
+
const sandboxOptions = (state.sandboxes || []).map((sandbox) => ({
|
|
2032
|
+
id: String(sandbox.id || ''),
|
|
2033
|
+
name: String(sandbox.name || ''),
|
|
2034
|
+
}));
|
|
2035
|
+
if (!profiles.length) {
|
|
2036
|
+
container.innerHTML = '<div class="project-empty">暂无 Profile,点击上方“添加 Profile”。</div>';
|
|
2037
|
+
return;
|
|
2038
|
+
}
|
|
2039
|
+
container.innerHTML = profiles.map((profile, idx) => `
|
|
2040
|
+
<div class="project-profile-card">
|
|
2041
|
+
<div class="project-profile-grid">
|
|
2042
|
+
<input type="text" data-role="project-profile-id" data-idx="${idx}" placeholder="Profile ID(唯一)" value="${escapeHtml(profile.id)}">
|
|
2043
|
+
<input type="text" data-role="project-profile-name" data-idx="${idx}" placeholder="Profile 名称" value="${escapeHtml(profile.name)}">
|
|
2044
|
+
<select data-role="project-profile-all-access" data-idx="${idx}">
|
|
2045
|
+
<option value="none" ${profile.sandbox_access.all === 'none' ? 'selected' : ''}>默认沙盘权限:none</option>
|
|
2046
|
+
<option value="read" ${profile.sandbox_access.all === 'read' ? 'selected' : ''}>默认沙盘权限:read</option>
|
|
2047
|
+
<option value="write" ${profile.sandbox_access.all === 'write' ? 'selected' : ''}>默认沙盘权限:write</option>
|
|
2048
|
+
</select>
|
|
2049
|
+
<label class="inline-checkbox">
|
|
2050
|
+
<input type="checkbox" data-role="project-profile-apply-all" data-idx="${idx}" ${profile.apply_to_all_users ? 'checked' : ''}>
|
|
2051
|
+
<span>apply_to_all_users</span>
|
|
2052
|
+
</label>
|
|
2053
|
+
</div>
|
|
2054
|
+
<div class="project-profile-pages">
|
|
2055
|
+
<label class="inline-checkbox"><input type="checkbox" data-role="project-profile-page" data-idx="${idx}" data-page="sandboxes" ${profile.page_access.sandboxes ? 'checked' : ''}><span>沙盘</span></label>
|
|
2056
|
+
<label class="inline-checkbox"><input type="checkbox" data-role="project-profile-page" data-idx="${idx}" data-page="diaries" ${profile.page_access.diaries ? 'checked' : ''}><span>日记</span></label>
|
|
2057
|
+
<label class="inline-checkbox"><input type="checkbox" data-role="project-profile-page" data-idx="${idx}" data-page="changes" ${profile.page_access.changes ? 'checked' : ''}><span>变化</span></label>
|
|
2058
|
+
<label class="inline-checkbox"><input type="checkbox" data-role="project-profile-page" data-idx="${idx}" data-page="settings" ${profile.page_access.settings ? 'checked' : ''}><span>项目设置</span></label>
|
|
2059
|
+
</div>
|
|
2060
|
+
<div class="project-sandbox-items">
|
|
2061
|
+
${(profile.sandbox_access.items || []).map((item, itemIdx) => `
|
|
2062
|
+
<div class="project-row compact">
|
|
2063
|
+
<select data-role="project-profile-sandbox-id" data-idx="${idx}" data-item-idx="${itemIdx}">
|
|
2064
|
+
<option value="">选择沙盘</option>
|
|
2065
|
+
${sandboxOptions.map((sandbox) => `
|
|
2066
|
+
<option value="${escapeHtml(sandbox.id)}" ${item.sandbox_id === sandbox.id ? 'selected' : ''}>
|
|
2067
|
+
${escapeHtml(sandbox.name || sandbox.id)} (${escapeHtml(sandbox.id)})
|
|
2068
|
+
</option>
|
|
2069
|
+
`).join('')}
|
|
2070
|
+
${item.sandbox_id && !sandboxOptions.some((sandbox) => sandbox.id === item.sandbox_id) ? `
|
|
2071
|
+
<option value="${escapeHtml(item.sandbox_id)}" selected>
|
|
2072
|
+
已不存在的沙盘 (${escapeHtml(item.sandbox_id)})
|
|
2073
|
+
</option>
|
|
2074
|
+
` : ''}
|
|
2075
|
+
</select>
|
|
2076
|
+
<select data-role="project-profile-sandbox-access" data-idx="${idx}" data-item-idx="${itemIdx}">
|
|
2077
|
+
<option value="none" ${item.access === 'none' ? 'selected' : ''}>none</option>
|
|
2078
|
+
<option value="read" ${item.access === 'read' ? 'selected' : ''}>read</option>
|
|
2079
|
+
<option value="write" ${item.access === 'write' ? 'selected' : ''}>write</option>
|
|
2080
|
+
</select>
|
|
2081
|
+
<button class="btn btn-secondary btn-sm" data-role="remove-project-profile-sandbox-item" data-idx="${idx}" data-item-idx="${itemIdx}" type="button">删除</button>
|
|
2082
|
+
</div>
|
|
2083
|
+
`).join('')}
|
|
2084
|
+
<button class="btn btn-secondary btn-sm" data-role="add-project-profile-sandbox-item" data-idx="${idx}" type="button">+ 添加沙盘白名单规则</button>
|
|
2085
|
+
</div>
|
|
2086
|
+
<div class="project-profile-footer">
|
|
2087
|
+
<button class="btn btn-secondary btn-sm" data-role="remove-project-profile" data-idx="${idx}" type="button">删除 Profile</button>
|
|
2088
|
+
</div>
|
|
2089
|
+
</div>
|
|
2090
|
+
`).join('');
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
function renderProjectBindingsEditor() {
|
|
2094
|
+
const container = document.getElementById('project-bindings-list');
|
|
2095
|
+
if (!(container instanceof HTMLElement)) return;
|
|
2096
|
+
const bindings = state.projectSettingsDraft?.bindings || [];
|
|
2097
|
+
const users = state.projectSettingsDraft?.users || [];
|
|
2098
|
+
const profiles = state.projectSettingsDraft?.profiles || [];
|
|
2099
|
+
if (!bindings.length) {
|
|
2100
|
+
container.innerHTML = '<div class="project-empty">暂无绑定,点击上方“添加绑定”。</div>';
|
|
2101
|
+
return;
|
|
2102
|
+
}
|
|
2103
|
+
container.innerHTML = bindings.map((binding, idx) => `
|
|
2104
|
+
<div class="project-binding-card">
|
|
2105
|
+
<div class="project-row compact">
|
|
2106
|
+
<select data-role="project-binding-user" data-idx="${idx}">
|
|
2107
|
+
<option value="">选择用户</option>
|
|
2108
|
+
${users.map((user) => `<option value="${escapeHtml(user.id)}" ${binding.user_id === user.id ? 'selected' : ''}>${escapeHtml(user.name || user.id)} (${escapeHtml(user.id)})</option>`).join('')}
|
|
2109
|
+
</select>
|
|
2110
|
+
<button class="btn btn-secondary btn-sm" data-role="remove-project-binding" data-idx="${idx}" type="button">删除</button>
|
|
2111
|
+
</div>
|
|
2112
|
+
<div class="project-binding-profiles">
|
|
2113
|
+
${profiles.map((profile) => `
|
|
2114
|
+
<label class="inline-checkbox">
|
|
2115
|
+
<input type="checkbox" data-role="project-binding-profile" data-idx="${idx}" data-profile-id="${escapeHtml(profile.id)}" ${binding.profile_ids.includes(profile.id) ? 'checked' : ''}>
|
|
2116
|
+
<span>${escapeHtml(profile.name || profile.id)}</span>
|
|
2117
|
+
</label>
|
|
2118
|
+
`).join('')}
|
|
2119
|
+
</div>
|
|
2120
|
+
</div>
|
|
2121
|
+
`).join('');
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
function renderProjectSettingsEditor() {
|
|
2125
|
+
renderProjectUsersEditor();
|
|
2126
|
+
renderProjectProfilesEditor();
|
|
2127
|
+
renderProjectBindingsEditor();
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
async function loadProjectSettings() {
|
|
2131
|
+
if (state.fullAccess || state.pageAccess.sandboxes) {
|
|
2132
|
+
try {
|
|
2133
|
+
await loadSandboxes();
|
|
2134
|
+
} catch {
|
|
2135
|
+
// Ignore sandbox list failures; project settings can still be edited.
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
const config = await apiRequest(`${API_BASE}/project-settings`);
|
|
2139
|
+
state.projectSettingsDraft = normalizeProjectSettingsDraft(config);
|
|
2140
|
+
renderProjectSettingsEditor();
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
function collectProjectSettingsPayload() {
|
|
2144
|
+
const draft = state.projectSettingsDraft || { users: [], profiles: [], bindings: [] };
|
|
2145
|
+
return {
|
|
2146
|
+
users: draft.users.map((user) => ({
|
|
2147
|
+
id: String(user.id || '').trim(),
|
|
2148
|
+
name: String(user.name || '').trim(),
|
|
2149
|
+
ips: Array.from(new Set((user.ips || []).map((ip) => String(ip || '').trim()).filter((ip) => Boolean(ip)))),
|
|
2150
|
+
})),
|
|
2151
|
+
profiles: draft.profiles.map((profile) => ({
|
|
2152
|
+
id: String(profile.id || '').trim(),
|
|
2153
|
+
name: String(profile.name || '').trim(),
|
|
2154
|
+
apply_to_all_users: Boolean(profile.apply_to_all_users),
|
|
2155
|
+
page_access: {
|
|
2156
|
+
sandboxes: Boolean(profile.page_access?.sandboxes),
|
|
2157
|
+
diaries: Boolean(profile.page_access?.diaries),
|
|
2158
|
+
changes: Boolean(profile.page_access?.changes),
|
|
2159
|
+
settings: Boolean(profile.page_access?.settings),
|
|
2160
|
+
},
|
|
2161
|
+
sandbox_access: {
|
|
2162
|
+
all: String(profile.sandbox_access?.all || 'none'),
|
|
2163
|
+
items: (profile.sandbox_access?.items || [])
|
|
2164
|
+
.map((item) => ({
|
|
2165
|
+
sandbox_id: String(item.sandbox_id || '').trim(),
|
|
2166
|
+
access: String(item.access || 'none'),
|
|
2167
|
+
}))
|
|
2168
|
+
.filter((item) => Boolean(item.sandbox_id)),
|
|
2169
|
+
},
|
|
2170
|
+
})),
|
|
2171
|
+
bindings: draft.bindings
|
|
2172
|
+
.map((binding) => ({
|
|
2173
|
+
user_id: String(binding.user_id || '').trim(),
|
|
2174
|
+
profile_ids: Array.from(new Set((binding.profile_ids || []).map((id) => String(id || '').trim()).filter((id) => Boolean(id)))),
|
|
2175
|
+
}))
|
|
2176
|
+
.filter((binding) => Boolean(binding.user_id)),
|
|
2177
|
+
};
|
|
1744
2178
|
}
|
|
1745
2179
|
|
|
1746
2180
|
function createSettingHeaderRow(key = '', value = '') {
|
|
@@ -1807,15 +2241,18 @@ function editWorkItem(id) {
|
|
|
1807
2241
|
document.getElementById('new-item-assignee').value = item.assignee;
|
|
1808
2242
|
document.getElementById('new-item-status').value = item.status;
|
|
1809
2243
|
document.getElementById('new-item-priority').value = item.priority;
|
|
2244
|
+
document.getElementById('new-item-parent').value = String(item.parent_id || '');
|
|
1810
2245
|
|
|
1811
2246
|
document.getElementById('item-dialog').dataset.editId = id;
|
|
1812
2247
|
document.getElementById('item-dialog').showModal();
|
|
1813
2248
|
}
|
|
1814
2249
|
|
|
1815
|
-
function initApp() {
|
|
1816
|
-
|
|
2250
|
+
async function initApp() {
|
|
2251
|
+
await loadRuntimeMode();
|
|
1817
2252
|
loadWorkItemAssigneePreference();
|
|
2253
|
+
loadWorkTreeViewModePreference();
|
|
1818
2254
|
renderQuickDiaryTargetLabel();
|
|
2255
|
+
applyReadonlyMode();
|
|
1819
2256
|
|
|
1820
2257
|
document.querySelectorAll('.nav-list a').forEach(link => {
|
|
1821
2258
|
link.addEventListener('click', (e) => {
|
|
@@ -1825,6 +2262,7 @@ function initApp() {
|
|
|
1825
2262
|
});
|
|
1826
2263
|
|
|
1827
2264
|
document.getElementById('add-sandbox-btn')?.addEventListener('click', () => {
|
|
2265
|
+
if (state.readonly) return;
|
|
1828
2266
|
document.getElementById('sandbox-dialog').showModal();
|
|
1829
2267
|
});
|
|
1830
2268
|
|
|
@@ -1833,6 +2271,7 @@ function initApp() {
|
|
|
1833
2271
|
});
|
|
1834
2272
|
|
|
1835
2273
|
const createSandbox = async (e) => {
|
|
2274
|
+
if (state.readonly) return;
|
|
1836
2275
|
e?.preventDefault?.();
|
|
1837
2276
|
const name = document.getElementById('new-sandbox-name').value;
|
|
1838
2277
|
const description = document.getElementById('new-sandbox-desc').value;
|
|
@@ -1856,6 +2295,7 @@ function initApp() {
|
|
|
1856
2295
|
document.getElementById('confirm-sandbox-btn')?.addEventListener('click', createSandbox);
|
|
1857
2296
|
|
|
1858
2297
|
document.getElementById('add-item-btn')?.addEventListener('click', () => {
|
|
2298
|
+
if (state.readonly) return;
|
|
1859
2299
|
document.getElementById('item-dialog-title').textContent = '添加任务';
|
|
1860
2300
|
document.getElementById('item-dialog').dataset.editId = '';
|
|
1861
2301
|
document.getElementById('new-item-name').value = '';
|
|
@@ -1867,9 +2307,11 @@ function initApp() {
|
|
|
1867
2307
|
document.getElementById('item-dialog').showModal();
|
|
1868
2308
|
});
|
|
1869
2309
|
|
|
1870
|
-
document.getElementById('toggle-sandbox-
|
|
1871
|
-
state.
|
|
1872
|
-
|
|
2310
|
+
document.getElementById('toggle-sandbox-chat-btn')?.addEventListener('click', () => {
|
|
2311
|
+
if (state.readonly) return;
|
|
2312
|
+
state.sandboxChatVisible = !state.sandboxChatVisible;
|
|
2313
|
+
applySandboxChatVisibility();
|
|
2314
|
+
applySandboxLayoutHeight();
|
|
1873
2315
|
});
|
|
1874
2316
|
|
|
1875
2317
|
document.getElementById('toggle-sandbox-fullscreen-btn')?.addEventListener('click', async () => {
|
|
@@ -1890,6 +2332,7 @@ function initApp() {
|
|
|
1890
2332
|
});
|
|
1891
2333
|
|
|
1892
2334
|
document.getElementById('toggle-node-entity-form-btn')?.addEventListener('click', () => {
|
|
2335
|
+
if (state.readonly) return;
|
|
1893
2336
|
setNodeEntityFormExpanded(!state.nodeEntityFormExpanded);
|
|
1894
2337
|
if (state.nodeEntityFormExpanded) {
|
|
1895
2338
|
document.getElementById('entity-title-input')?.focus();
|
|
@@ -1918,6 +2361,7 @@ function initApp() {
|
|
|
1918
2361
|
});
|
|
1919
2362
|
|
|
1920
2363
|
document.getElementById('create-node-entity-btn')?.addEventListener('click', async () => {
|
|
2364
|
+
if (state.readonly) return;
|
|
1921
2365
|
if (!state.currentSandbox || !state.selectedNodeId) return;
|
|
1922
2366
|
const btn = document.getElementById('create-node-entity-btn');
|
|
1923
2367
|
const title = document.getElementById('entity-title-input').value.trim();
|
|
@@ -2016,6 +2460,7 @@ function initApp() {
|
|
|
2016
2460
|
});
|
|
2017
2461
|
|
|
2018
2462
|
document.getElementById('confirm-item-btn')?.addEventListener('click', async () => {
|
|
2463
|
+
if (state.readonly) return;
|
|
2019
2464
|
const dialog = document.getElementById('item-dialog');
|
|
2020
2465
|
const editId = dialog.dataset.editId || null;
|
|
2021
2466
|
const isNewItem = !editId;
|
|
@@ -2062,6 +2507,7 @@ function initApp() {
|
|
|
2062
2507
|
});
|
|
2063
2508
|
|
|
2064
2509
|
document.getElementById('rollback-btn')?.addEventListener('click', async () => {
|
|
2510
|
+
if (state.readonly) return;
|
|
2065
2511
|
if (!state.currentSandbox?.items?.length) return;
|
|
2066
2512
|
const lastItem = state.currentSandbox.items[state.currentSandbox.items.length - 1];
|
|
2067
2513
|
await apiRequest(`${API_BASE}/items/${lastItem.id}/rollback`, { method: 'POST' });
|
|
@@ -2069,6 +2515,7 @@ function initApp() {
|
|
|
2069
2515
|
});
|
|
2070
2516
|
|
|
2071
2517
|
document.getElementById('sandbox-send-btn')?.addEventListener('click', async () => {
|
|
2518
|
+
if (state.readonly) return;
|
|
2072
2519
|
const input = document.getElementById('sandbox-chat-input');
|
|
2073
2520
|
const content = input.value.trim();
|
|
2074
2521
|
if (!content || !state.currentSandbox) return;
|
|
@@ -2186,12 +2633,14 @@ function initApp() {
|
|
|
2186
2633
|
sandboxActionHandler = handleAIAction;
|
|
2187
2634
|
|
|
2188
2635
|
document.getElementById('sandbox-chat-input')?.addEventListener('keypress', (e) => {
|
|
2636
|
+
if (state.readonly) return;
|
|
2189
2637
|
if (e.key === 'Enter') {
|
|
2190
2638
|
document.getElementById('sandbox-send-btn').click();
|
|
2191
2639
|
}
|
|
2192
2640
|
});
|
|
2193
2641
|
|
|
2194
2642
|
document.getElementById('save-diary-btn')?.addEventListener('click', async () => {
|
|
2643
|
+
if (state.readonly) return;
|
|
2195
2644
|
const content = document.getElementById('diary-content').value.trim();
|
|
2196
2645
|
if (!content) return;
|
|
2197
2646
|
await saveDiaryEntry({ content, sandboxId: null, workItemId: null });
|
|
@@ -2200,6 +2649,7 @@ function initApp() {
|
|
|
2200
2649
|
});
|
|
2201
2650
|
|
|
2202
2651
|
document.getElementById('drawer-diary-save-btn')?.addEventListener('click', async () => {
|
|
2652
|
+
if (state.readonly) return;
|
|
2203
2653
|
if (!state.currentSandbox || !state.selectedNodeId) return;
|
|
2204
2654
|
const textarea = document.getElementById('drawer-diary-content');
|
|
2205
2655
|
if (!(textarea instanceof HTMLTextAreaElement)) return;
|
|
@@ -2222,9 +2672,11 @@ function initApp() {
|
|
|
2222
2672
|
closeDiaryEditDialog();
|
|
2223
2673
|
});
|
|
2224
2674
|
document.getElementById('confirm-edit-diary-btn')?.addEventListener('click', async () => {
|
|
2675
|
+
if (state.readonly) return;
|
|
2225
2676
|
await submitDiaryEditDialog();
|
|
2226
2677
|
});
|
|
2227
2678
|
document.getElementById('diary-edit-content')?.addEventListener('keydown', async (event) => {
|
|
2679
|
+
if (state.readonly) return;
|
|
2228
2680
|
const isSubmit = event.key === 'Enter' && (event.metaKey || event.ctrlKey);
|
|
2229
2681
|
if (!isSubmit) return;
|
|
2230
2682
|
event.preventDefault();
|
|
@@ -2241,10 +2693,13 @@ function initApp() {
|
|
|
2241
2693
|
});
|
|
2242
2694
|
|
|
2243
2695
|
document.getElementById('save-settings-btn')?.addEventListener('click', async () => {
|
|
2696
|
+
if (state.readonly || !state.canAccessSystemSettings) return;
|
|
2244
2697
|
const api_url = document.getElementById('setting-api-url').value;
|
|
2245
2698
|
const api_key = document.getElementById('setting-api-key').value;
|
|
2246
2699
|
const model = document.getElementById('setting-model').value;
|
|
2247
2700
|
const headers = collectSettingHeadersFromUI();
|
|
2701
|
+
const editableIpsRaw = document.getElementById('setting-editable-ips')?.value || '';
|
|
2702
|
+
const editableIps = String(editableIpsRaw).split('\n').map((ip) => ip.trim()).filter((ip) => Boolean(ip));
|
|
2248
2703
|
|
|
2249
2704
|
await apiRequest(`${API_BASE}/settings/api_url`, {
|
|
2250
2705
|
method: 'PUT',
|
|
@@ -2264,17 +2719,239 @@ function initApp() {
|
|
|
2264
2719
|
method: 'PUT',
|
|
2265
2720
|
body: JSON.stringify({ value: headers }),
|
|
2266
2721
|
});
|
|
2722
|
+
await apiRequest(`${API_BASE}/settings/editable_ip_allowlist`, {
|
|
2723
|
+
method: 'PUT',
|
|
2724
|
+
body: JSON.stringify({ value: editableIps }),
|
|
2725
|
+
});
|
|
2267
2726
|
|
|
2268
2727
|
alert('设置已保存');
|
|
2728
|
+
await loadRuntimeMode();
|
|
2729
|
+
applyReadonlyMode();
|
|
2269
2730
|
await loadSettings();
|
|
2270
2731
|
});
|
|
2271
2732
|
|
|
2272
2733
|
document.getElementById('add-setting-header-btn')?.addEventListener('click', () => {
|
|
2734
|
+
if (state.readonly || !state.canAccessSystemSettings) return;
|
|
2273
2735
|
const list = document.getElementById('setting-headers-list');
|
|
2274
2736
|
if (!(list instanceof HTMLElement)) return;
|
|
2275
2737
|
list.appendChild(createSettingHeaderRow('', ''));
|
|
2276
2738
|
});
|
|
2277
2739
|
|
|
2740
|
+
document.getElementById('add-project-user-btn')?.addEventListener('click', () => {
|
|
2741
|
+
if (!state.projectSettingsDraft) return;
|
|
2742
|
+
state.projectSettingsDraft.users.push({ id: '', name: '', ips: [] });
|
|
2743
|
+
renderProjectSettingsEditor();
|
|
2744
|
+
});
|
|
2745
|
+
|
|
2746
|
+
document.getElementById('add-project-profile-btn')?.addEventListener('click', () => {
|
|
2747
|
+
if (!state.projectSettingsDraft) return;
|
|
2748
|
+
state.projectSettingsDraft.profiles.push({
|
|
2749
|
+
id: '',
|
|
2750
|
+
name: '',
|
|
2751
|
+
apply_to_all_users: false,
|
|
2752
|
+
page_access: { sandboxes: true, diaries: true, changes: true, settings: false },
|
|
2753
|
+
sandbox_access: { all: 'none', items: [] },
|
|
2754
|
+
});
|
|
2755
|
+
renderProjectSettingsEditor();
|
|
2756
|
+
});
|
|
2757
|
+
|
|
2758
|
+
document.getElementById('add-project-binding-btn')?.addEventListener('click', () => {
|
|
2759
|
+
if (!state.projectSettingsDraft) return;
|
|
2760
|
+
state.projectSettingsDraft.bindings.push({ user_id: '', profile_ids: [] });
|
|
2761
|
+
renderProjectSettingsEditor();
|
|
2762
|
+
});
|
|
2763
|
+
|
|
2764
|
+
document.getElementById('project-users-list')?.addEventListener('click', (event) => {
|
|
2765
|
+
const target = event.target;
|
|
2766
|
+
if (!(target instanceof HTMLElement)) return;
|
|
2767
|
+
if (!state.projectSettingsDraft) return;
|
|
2768
|
+
if (target.dataset.role === 'add-project-user-ip') {
|
|
2769
|
+
const idx = Number(target.dataset.idx);
|
|
2770
|
+
const input = document.querySelector(`[data-role="project-user-ip-input"][data-idx="${idx}"]`);
|
|
2771
|
+
if (input instanceof HTMLInputElement) {
|
|
2772
|
+
addUserIpAtIndex(idx, input.value);
|
|
2773
|
+
}
|
|
2774
|
+
return;
|
|
2775
|
+
}
|
|
2776
|
+
if (target.dataset.role === 'remove-project-user-ip') {
|
|
2777
|
+
const idx = Number(target.dataset.idx);
|
|
2778
|
+
const ipIdx = Number(target.dataset.ipIdx);
|
|
2779
|
+
if (!Number.isFinite(idx) || idx < 0 || !Number.isFinite(ipIdx) || ipIdx < 0) return;
|
|
2780
|
+
const user = state.projectSettingsDraft.users[idx];
|
|
2781
|
+
if (!user) return;
|
|
2782
|
+
user.ips.splice(ipIdx, 1);
|
|
2783
|
+
renderProjectSettingsEditor();
|
|
2784
|
+
return;
|
|
2785
|
+
}
|
|
2786
|
+
if (target.dataset.role !== 'remove-project-user') return;
|
|
2787
|
+
const idx = Number(target.dataset.idx);
|
|
2788
|
+
if (!Number.isFinite(idx) || idx < 0) return;
|
|
2789
|
+
const removed = state.projectSettingsDraft.users[idx];
|
|
2790
|
+
state.projectSettingsDraft.users.splice(idx, 1);
|
|
2791
|
+
if (removed?.id) {
|
|
2792
|
+
state.projectSettingsDraft.bindings = (state.projectSettingsDraft.bindings || []).filter((binding) => binding.user_id !== removed.id);
|
|
2793
|
+
}
|
|
2794
|
+
renderProjectSettingsEditor();
|
|
2795
|
+
});
|
|
2796
|
+
|
|
2797
|
+
document.getElementById('project-users-list')?.addEventListener('input', (event) => {
|
|
2798
|
+
const target = event.target;
|
|
2799
|
+
if (!(target instanceof HTMLInputElement) || !state.projectSettingsDraft) return;
|
|
2800
|
+
const idx = Number(target.dataset.idx);
|
|
2801
|
+
if (!Number.isFinite(idx) || idx < 0) return;
|
|
2802
|
+
const user = state.projectSettingsDraft.users[idx];
|
|
2803
|
+
if (!user) return;
|
|
2804
|
+
if (target.dataset.role === 'project-user-id') {
|
|
2805
|
+
user.id = target.value.trim();
|
|
2806
|
+
} else if (target.dataset.role === 'project-user-name') {
|
|
2807
|
+
user.name = target.value.trim();
|
|
2808
|
+
}
|
|
2809
|
+
});
|
|
2810
|
+
|
|
2811
|
+
document.getElementById('project-users-list')?.addEventListener('keydown', (event) => {
|
|
2812
|
+
const target = event.target;
|
|
2813
|
+
if (!(target instanceof HTMLInputElement) || !state.projectSettingsDraft) return;
|
|
2814
|
+
if (target.dataset.role !== 'project-user-ip-input') return;
|
|
2815
|
+
if (event.key !== 'Enter') return;
|
|
2816
|
+
event.preventDefault();
|
|
2817
|
+
const idx = Number(target.dataset.idx);
|
|
2818
|
+
addUserIpAtIndex(idx, target.value);
|
|
2819
|
+
});
|
|
2820
|
+
|
|
2821
|
+
document.getElementById('project-profiles-list')?.addEventListener('click', (event) => {
|
|
2822
|
+
const target = event.target;
|
|
2823
|
+
if (!(target instanceof HTMLElement) || !state.projectSettingsDraft) return;
|
|
2824
|
+
const profileIdx = Number(target.dataset.idx);
|
|
2825
|
+
if (!Number.isFinite(profileIdx) || profileIdx < 0) return;
|
|
2826
|
+
if (target.dataset.role === 'remove-project-profile') {
|
|
2827
|
+
const removed = state.projectSettingsDraft.profiles[profileIdx];
|
|
2828
|
+
state.projectSettingsDraft.profiles.splice(profileIdx, 1);
|
|
2829
|
+
if (removed?.id) {
|
|
2830
|
+
state.projectSettingsDraft.bindings.forEach((binding) => {
|
|
2831
|
+
binding.profile_ids = (binding.profile_ids || []).filter((id) => id !== removed.id);
|
|
2832
|
+
});
|
|
2833
|
+
}
|
|
2834
|
+
renderProjectSettingsEditor();
|
|
2835
|
+
return;
|
|
2836
|
+
}
|
|
2837
|
+
if (target.dataset.role === 'add-project-profile-sandbox-item') {
|
|
2838
|
+
const profile = state.projectSettingsDraft.profiles[profileIdx];
|
|
2839
|
+
if (!profile) return;
|
|
2840
|
+
profile.sandbox_access.items.push({ sandbox_id: '', access: 'read' });
|
|
2841
|
+
renderProjectSettingsEditor();
|
|
2842
|
+
return;
|
|
2843
|
+
}
|
|
2844
|
+
if (target.dataset.role === 'remove-project-profile-sandbox-item') {
|
|
2845
|
+
const profile = state.projectSettingsDraft.profiles[profileIdx];
|
|
2846
|
+
if (!profile) return;
|
|
2847
|
+
const itemIdx = Number(target.dataset.itemIdx);
|
|
2848
|
+
if (!Number.isFinite(itemIdx) || itemIdx < 0) return;
|
|
2849
|
+
profile.sandbox_access.items.splice(itemIdx, 1);
|
|
2850
|
+
renderProjectSettingsEditor();
|
|
2851
|
+
}
|
|
2852
|
+
});
|
|
2853
|
+
|
|
2854
|
+
document.getElementById('project-profiles-list')?.addEventListener('input', (event) => {
|
|
2855
|
+
const target = event.target;
|
|
2856
|
+
if (!(target instanceof HTMLInputElement) || !state.projectSettingsDraft) return;
|
|
2857
|
+
const profileIdx = Number(target.dataset.idx);
|
|
2858
|
+
if (!Number.isFinite(profileIdx) || profileIdx < 0) return;
|
|
2859
|
+
const profile = state.projectSettingsDraft.profiles[profileIdx];
|
|
2860
|
+
if (!profile) return;
|
|
2861
|
+
if (target.dataset.role === 'project-profile-id') {
|
|
2862
|
+
profile.id = target.value.trim();
|
|
2863
|
+
return;
|
|
2864
|
+
}
|
|
2865
|
+
if (target.dataset.role === 'project-profile-name') {
|
|
2866
|
+
profile.name = target.value.trim();
|
|
2867
|
+
return;
|
|
2868
|
+
}
|
|
2869
|
+
if (target.dataset.role === 'project-profile-apply-all') {
|
|
2870
|
+
profile.apply_to_all_users = target.checked;
|
|
2871
|
+
return;
|
|
2872
|
+
}
|
|
2873
|
+
if (target.dataset.role === 'project-profile-page') {
|
|
2874
|
+
const page = target.dataset.page;
|
|
2875
|
+
if (page === 'sandboxes' || page === 'diaries' || page === 'changes' || page === 'settings') {
|
|
2876
|
+
profile.page_access[page] = target.checked;
|
|
2877
|
+
}
|
|
2878
|
+
return;
|
|
2879
|
+
}
|
|
2880
|
+
});
|
|
2881
|
+
|
|
2882
|
+
document.getElementById('project-profiles-list')?.addEventListener('change', (event) => {
|
|
2883
|
+
const target = event.target;
|
|
2884
|
+
if (!(target instanceof HTMLSelectElement) || !state.projectSettingsDraft) return;
|
|
2885
|
+
const profileIdx = Number(target.dataset.idx);
|
|
2886
|
+
if (!Number.isFinite(profileIdx) || profileIdx < 0) return;
|
|
2887
|
+
const profile = state.projectSettingsDraft.profiles[profileIdx];
|
|
2888
|
+
if (!profile) return;
|
|
2889
|
+
if (target.dataset.role === 'project-profile-all-access') {
|
|
2890
|
+
profile.sandbox_access.all = target.value || 'none';
|
|
2891
|
+
return;
|
|
2892
|
+
}
|
|
2893
|
+
if (target.dataset.role === 'project-profile-sandbox-id') {
|
|
2894
|
+
const itemIdx = Number(target.dataset.itemIdx);
|
|
2895
|
+
if (!Number.isFinite(itemIdx) || itemIdx < 0) return;
|
|
2896
|
+
const item = profile.sandbox_access.items[itemIdx];
|
|
2897
|
+
if (item) item.sandbox_id = String(target.value || '').trim();
|
|
2898
|
+
return;
|
|
2899
|
+
}
|
|
2900
|
+
if (target.dataset.role === 'project-profile-sandbox-access') {
|
|
2901
|
+
const itemIdx = Number(target.dataset.itemIdx);
|
|
2902
|
+
if (!Number.isFinite(itemIdx) || itemIdx < 0) return;
|
|
2903
|
+
const item = profile.sandbox_access.items[itemIdx];
|
|
2904
|
+
if (item) item.access = target.value || 'none';
|
|
2905
|
+
}
|
|
2906
|
+
});
|
|
2907
|
+
|
|
2908
|
+
document.getElementById('project-bindings-list')?.addEventListener('click', (event) => {
|
|
2909
|
+
const target = event.target;
|
|
2910
|
+
if (!(target instanceof HTMLElement) || !state.projectSettingsDraft) return;
|
|
2911
|
+
if (target.dataset.role !== 'remove-project-binding') return;
|
|
2912
|
+
const idx = Number(target.dataset.idx);
|
|
2913
|
+
if (!Number.isFinite(idx) || idx < 0) return;
|
|
2914
|
+
state.projectSettingsDraft.bindings.splice(idx, 1);
|
|
2915
|
+
renderProjectSettingsEditor();
|
|
2916
|
+
});
|
|
2917
|
+
|
|
2918
|
+
document.getElementById('project-bindings-list')?.addEventListener('change', (event) => {
|
|
2919
|
+
const target = event.target;
|
|
2920
|
+
if (!state.projectSettingsDraft) return;
|
|
2921
|
+
const idx = Number(target instanceof HTMLElement ? target.dataset.idx : NaN);
|
|
2922
|
+
if (!Number.isFinite(idx) || idx < 0) return;
|
|
2923
|
+
const binding = state.projectSettingsDraft.bindings[idx];
|
|
2924
|
+
if (!binding) return;
|
|
2925
|
+
if (target instanceof HTMLSelectElement && target.dataset.role === 'project-binding-user') {
|
|
2926
|
+
binding.user_id = target.value || '';
|
|
2927
|
+
return;
|
|
2928
|
+
}
|
|
2929
|
+
if (target instanceof HTMLInputElement && target.dataset.role === 'project-binding-profile') {
|
|
2930
|
+
const profileId = String(target.dataset.profileId || '').trim();
|
|
2931
|
+
const set = new Set(binding.profile_ids || []);
|
|
2932
|
+
if (target.checked) set.add(profileId);
|
|
2933
|
+
else set.delete(profileId);
|
|
2934
|
+
binding.profile_ids = Array.from(set);
|
|
2935
|
+
}
|
|
2936
|
+
});
|
|
2937
|
+
|
|
2938
|
+
document.getElementById('save-project-settings-btn')?.addEventListener('click', async () => {
|
|
2939
|
+
if (state.readonly || (!state.fullAccess && !state.pageAccess.settings)) return;
|
|
2940
|
+
try {
|
|
2941
|
+
const payload = collectProjectSettingsPayload();
|
|
2942
|
+
await apiRequest(`${API_BASE}/project-settings`, {
|
|
2943
|
+
method: 'PUT',
|
|
2944
|
+
body: JSON.stringify(payload),
|
|
2945
|
+
});
|
|
2946
|
+
alert('项目设置已保存');
|
|
2947
|
+
await loadRuntimeMode();
|
|
2948
|
+
applyReadonlyMode();
|
|
2949
|
+
await loadProjectSettings();
|
|
2950
|
+
} catch (error) {
|
|
2951
|
+
alert(`项目设置保存失败: ${error?.message || error}`);
|
|
2952
|
+
}
|
|
2953
|
+
});
|
|
2954
|
+
|
|
2278
2955
|
document.getElementById('work-item-search')?.addEventListener('input', (e) => {
|
|
2279
2956
|
state.workItemSearch = e.target.value || '';
|
|
2280
2957
|
renderWorkTree();
|
|
@@ -2288,6 +2965,7 @@ function initApp() {
|
|
|
2288
2965
|
document.getElementById('work-tree-view-mode')?.addEventListener('change', (e) => {
|
|
2289
2966
|
const nextMode = e.target.value || 'full';
|
|
2290
2967
|
applyWorkTreeViewMode(nextMode);
|
|
2968
|
+
persistWorkTreeViewModePreference();
|
|
2291
2969
|
renderWorkTree();
|
|
2292
2970
|
});
|
|
2293
2971
|
|
|
@@ -2411,12 +3089,13 @@ function initApp() {
|
|
|
2411
3089
|
clearTimeout(resizeRenderTimer);
|
|
2412
3090
|
}
|
|
2413
3091
|
resizeRenderTimer = setTimeout(() => {
|
|
3092
|
+
applySandboxLayoutHeight();
|
|
2414
3093
|
renderWorkTree();
|
|
2415
3094
|
}, 120);
|
|
2416
3095
|
});
|
|
2417
3096
|
|
|
2418
3097
|
window.addEventListener('hashchange', handleRoute);
|
|
2419
|
-
handleRoute();
|
|
3098
|
+
await handleRoute();
|
|
2420
3099
|
|
|
2421
3100
|
async function handleRoute() {
|
|
2422
3101
|
const hash = window.location.hash.slice(1) || '/';
|
|
@@ -2428,13 +3107,31 @@ function initApp() {
|
|
|
2428
3107
|
}
|
|
2429
3108
|
|
|
2430
3109
|
if (pathHash === '/') {
|
|
2431
|
-
|
|
2432
|
-
|
|
3110
|
+
if (state.pageAccess.sandboxes || state.fullAccess) {
|
|
3111
|
+
showPage('sandboxes');
|
|
3112
|
+
await loadSandboxes();
|
|
3113
|
+
} else if (state.pageAccess.diaries) {
|
|
3114
|
+
window.location.hash = '/diaries';
|
|
3115
|
+
} else if (state.pageAccess.changes) {
|
|
3116
|
+
window.location.hash = '/changes';
|
|
3117
|
+
} else if (state.pageAccess.settings) {
|
|
3118
|
+
window.location.hash = '/settings';
|
|
3119
|
+
} else if (state.canAccessSystemSettings) {
|
|
3120
|
+
window.location.hash = '/system-settings';
|
|
3121
|
+
}
|
|
2433
3122
|
} else if (pathHash === '/sandboxes') {
|
|
3123
|
+
if (!state.pageAccess.sandboxes && !state.fullAccess) {
|
|
3124
|
+
window.location.hash = '/';
|
|
3125
|
+
return;
|
|
3126
|
+
}
|
|
2434
3127
|
showPage('sandboxes');
|
|
2435
3128
|
await loadSandboxes();
|
|
2436
3129
|
} else if (pathHash.startsWith('/sandbox/')) {
|
|
2437
3130
|
const id = decodeURIComponent(pathHash.split('/')[2] || '');
|
|
3131
|
+
if (!canReadSandboxById(id)) {
|
|
3132
|
+
window.location.hash = '/';
|
|
3133
|
+
return;
|
|
3134
|
+
}
|
|
2438
3135
|
showPage('sandbox-detail');
|
|
2439
3136
|
await loadSandbox(id);
|
|
2440
3137
|
const targetNodeId = String(query.get('node_id') || '').trim();
|
|
@@ -2444,15 +3141,34 @@ function initApp() {
|
|
|
2444
3141
|
showNodeEntityDrawer(targetNodeId, preferredFilter);
|
|
2445
3142
|
}
|
|
2446
3143
|
} else if (pathHash === '/diaries') {
|
|
3144
|
+
if (!state.pageAccess.diaries && !state.fullAccess) {
|
|
3145
|
+
window.location.hash = '/';
|
|
3146
|
+
return;
|
|
3147
|
+
}
|
|
2447
3148
|
showPage('diaries');
|
|
2448
3149
|
await loadDiaries();
|
|
2449
3150
|
await loadSandboxes();
|
|
2450
3151
|
} else if (pathHash === '/changes') {
|
|
3152
|
+
if (!state.pageAccess.changes && !state.fullAccess) {
|
|
3153
|
+
window.location.hash = '/';
|
|
3154
|
+
return;
|
|
3155
|
+
}
|
|
2451
3156
|
showPage('changes');
|
|
2452
3157
|
await loadSandboxes();
|
|
2453
3158
|
await loadChanges();
|
|
2454
3159
|
} else if (pathHash === '/settings') {
|
|
3160
|
+
if (!state.pageAccess.settings && !state.fullAccess) {
|
|
3161
|
+
window.location.hash = '/';
|
|
3162
|
+
return;
|
|
3163
|
+
}
|
|
2455
3164
|
showPage('settings');
|
|
3165
|
+
await loadProjectSettings();
|
|
3166
|
+
} else if (pathHash === '/system-settings') {
|
|
3167
|
+
if (!state.canAccessSystemSettings) {
|
|
3168
|
+
window.location.hash = '/';
|
|
3169
|
+
return;
|
|
3170
|
+
}
|
|
3171
|
+
showPage('system-settings');
|
|
2456
3172
|
await loadSettings();
|
|
2457
3173
|
}
|
|
2458
3174
|
}
|