@qnote/q-ai-note 1.0.6 → 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 +11 -14
- package/dist/cli.js +18 -31
- 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 +2 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +51 -11
- 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 +618 -38
- package/dist/web/index.html +103 -59
- package/dist/web/styles.css +189 -4
- package/dist/web/vueRenderers.js +7 -5
- package/package.json +2 -3
package/dist/web/app.js
CHANGED
|
@@ -10,6 +10,19 @@ const state = {
|
|
|
10
10
|
chats: [],
|
|
11
11
|
settings: {},
|
|
12
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,
|
|
13
26
|
pendingAction: null,
|
|
14
27
|
nodeEntities: [],
|
|
15
28
|
nodeEntityStats: null,
|
|
@@ -86,9 +99,10 @@ function applySandboxChatVisibility() {
|
|
|
86
99
|
const layout = document.getElementById('sandbox-layout');
|
|
87
100
|
const toggleBtn = document.getElementById('toggle-sandbox-chat-btn');
|
|
88
101
|
if (!layout || !toggleBtn) return;
|
|
89
|
-
const
|
|
102
|
+
const chatAllowed = !state.readonly && state.currentSandboxWritable;
|
|
103
|
+
const shouldShow = chatAllowed && Boolean(state.sandboxChatVisible);
|
|
90
104
|
layout.classList.toggle('show-chat', shouldShow);
|
|
91
|
-
toggleBtn.classList.toggle('hidden',
|
|
105
|
+
toggleBtn.classList.toggle('hidden', !chatAllowed);
|
|
92
106
|
toggleBtn.innerHTML = shouldShow
|
|
93
107
|
? '<span class="icon" aria-hidden="true">🤖</span><span>隐藏 AI 助手</span>'
|
|
94
108
|
: '<span class="icon" aria-hidden="true">🤖</span><span>显示 AI 助手</span>';
|
|
@@ -99,11 +113,47 @@ async function loadRuntimeMode() {
|
|
|
99
113
|
try {
|
|
100
114
|
const runtime = await apiRequest(`${API_BASE}/runtime`);
|
|
101
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');
|
|
102
128
|
} catch {
|
|
103
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;
|
|
104
137
|
}
|
|
105
138
|
}
|
|
106
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
|
+
|
|
107
157
|
function setHiddenById(id, hidden = true) {
|
|
108
158
|
const el = document.getElementById(id);
|
|
109
159
|
if (!el) return;
|
|
@@ -112,15 +162,32 @@ function setHiddenById(id, hidden = true) {
|
|
|
112
162
|
|
|
113
163
|
function applyReadonlyMode() {
|
|
114
164
|
document.body.classList.toggle('readonly-mode', state.readonly);
|
|
115
|
-
const
|
|
116
|
-
if (
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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);
|
|
124
191
|
setHiddenById('save-diary-btn', state.readonly);
|
|
125
192
|
const diaryForm = document.querySelector('#page-diaries .diary-form');
|
|
126
193
|
if (diaryForm instanceof HTMLElement) {
|
|
@@ -128,7 +195,7 @@ function applyReadonlyMode() {
|
|
|
128
195
|
}
|
|
129
196
|
const drawerDiaryForm = document.querySelector('#node-entity-drawer .drawer-diary-quick-form');
|
|
130
197
|
if (drawerDiaryForm instanceof HTMLElement) {
|
|
131
|
-
drawerDiaryForm.classList.toggle('hidden', state.readonly);
|
|
198
|
+
drawerDiaryForm.classList.toggle('hidden', state.readonly || !state.currentSandboxWritable);
|
|
132
199
|
}
|
|
133
200
|
if (state.readonly) {
|
|
134
201
|
state.sandboxChatVisible = false;
|
|
@@ -302,6 +369,7 @@ function applyWorkItemElementPreviewMode() {
|
|
|
302
369
|
function renderWorkTree() {
|
|
303
370
|
const tree = document.getElementById('work-tree');
|
|
304
371
|
if (!tree || !state.currentSandbox) return;
|
|
372
|
+
const treeReadonly = state.readonly || !state.currentSandboxWritable;
|
|
305
373
|
|
|
306
374
|
const allItems = state.currentSandbox.items || [];
|
|
307
375
|
const items = applyWorkItemFilters(allItems);
|
|
@@ -317,7 +385,7 @@ function renderWorkTree() {
|
|
|
317
385
|
}
|
|
318
386
|
|
|
319
387
|
if (allItems.length === 0) {
|
|
320
|
-
tree.innerHTML = `<div class="empty-state"><p>${
|
|
388
|
+
tree.innerHTML = `<div class="empty-state"><p>${treeReadonly ? '暂无任务' : '点击上方"添加"按钮创建第一个任务'}</p></div>`;
|
|
321
389
|
return;
|
|
322
390
|
}
|
|
323
391
|
|
|
@@ -333,7 +401,7 @@ function renderWorkTree() {
|
|
|
333
401
|
}
|
|
334
402
|
renderWorkTree();
|
|
335
403
|
},
|
|
336
|
-
onAddChild:
|
|
404
|
+
onAddChild: treeReadonly ? undefined : (parentId) => {
|
|
337
405
|
document.getElementById('item-dialog-title').textContent = '添加子任务';
|
|
338
406
|
document.getElementById('item-dialog').dataset.editId = '';
|
|
339
407
|
document.getElementById('new-item-name').value = '';
|
|
@@ -344,14 +412,14 @@ function renderWorkTree() {
|
|
|
344
412
|
document.getElementById('new-item-parent').value = parentId;
|
|
345
413
|
document.getElementById('item-dialog').showModal();
|
|
346
414
|
},
|
|
347
|
-
onAddDiary:
|
|
415
|
+
onAddDiary: treeReadonly ? undefined : (nodeId) => {
|
|
348
416
|
showNodeEntityDrawer(nodeId, 'diary');
|
|
349
417
|
const textarea = document.getElementById('drawer-diary-content');
|
|
350
418
|
if (textarea instanceof HTMLTextAreaElement) {
|
|
351
419
|
textarea.focus();
|
|
352
420
|
}
|
|
353
421
|
},
|
|
354
|
-
onEdit:
|
|
422
|
+
onEdit: treeReadonly ? undefined : (id) => {
|
|
355
423
|
editWorkItem(id);
|
|
356
424
|
},
|
|
357
425
|
onSelect: (id) => {
|
|
@@ -360,7 +428,7 @@ function renderWorkTree() {
|
|
|
360
428
|
onSelectEntity: (nodeId, entityType) => {
|
|
361
429
|
showNodeEntityDrawer(nodeId, entityType || 'all');
|
|
362
430
|
},
|
|
363
|
-
onMoveNode:
|
|
431
|
+
onMoveNode: treeReadonly ? undefined : async (dragNodeId, newParentId) => {
|
|
364
432
|
if (!state.currentSandbox) return;
|
|
365
433
|
if (!dragNodeId || dragNodeId === newParentId) return;
|
|
366
434
|
const byId = new Map((state.currentSandbox.items || []).map((item) => [item.id, item]));
|
|
@@ -388,7 +456,7 @@ function renderWorkTree() {
|
|
|
388
456
|
});
|
|
389
457
|
await loadSandbox(state.currentSandbox.id);
|
|
390
458
|
},
|
|
391
|
-
onReorderSiblings:
|
|
459
|
+
onReorderSiblings: treeReadonly ? undefined : async (dragNodeId, targetNodeId, position) => {
|
|
392
460
|
if (!state.currentSandbox) return;
|
|
393
461
|
if (!dragNodeId || !targetNodeId || dragNodeId === targetNodeId) return;
|
|
394
462
|
const byId = new Map((state.currentSandbox.items || []).map((item) => [item.id, item]));
|
|
@@ -421,7 +489,7 @@ function renderWorkTree() {
|
|
|
421
489
|
});
|
|
422
490
|
await loadSandbox(state.currentSandbox.id);
|
|
423
491
|
},
|
|
424
|
-
onReorderLanes:
|
|
492
|
+
onReorderLanes: treeReadonly ? undefined : async (dragRootId, targetRootId) => {
|
|
425
493
|
if (!state.currentSandbox) return;
|
|
426
494
|
if (!dragRootId || !targetRootId || dragRootId === targetRootId) return;
|
|
427
495
|
const roots = (state.currentSandbox.items || []).filter((item) => !item.parent_id);
|
|
@@ -446,20 +514,20 @@ function renderWorkTree() {
|
|
|
446
514
|
});
|
|
447
515
|
await loadSandbox(state.currentSandbox.id);
|
|
448
516
|
},
|
|
449
|
-
onDelete:
|
|
517
|
+
onDelete: treeReadonly ? undefined : async (id) => {
|
|
450
518
|
if (confirm('确定删除此任务?')) {
|
|
451
519
|
await apiRequest(`${API_BASE}/items/${id}`, { method: 'DELETE' });
|
|
452
520
|
await loadSandbox(state.currentSandbox.id);
|
|
453
521
|
}
|
|
454
522
|
},
|
|
455
|
-
onQuickChat:
|
|
523
|
+
onQuickChat: treeReadonly ? undefined : (id, el) => {
|
|
456
524
|
openQuickChatPopover(id, el);
|
|
457
525
|
},
|
|
458
526
|
renderMode: state.workTreeViewMode === 'dense' ? 'dense' : 'card',
|
|
459
527
|
showAssignee: state.workItemShowAssignee,
|
|
460
528
|
elementPreviewMode: state.workItemElementPreviewMode,
|
|
461
529
|
entityRowsByNodeId,
|
|
462
|
-
readonly:
|
|
530
|
+
readonly: treeReadonly,
|
|
463
531
|
selectedId: state.selectedNodeId || '',
|
|
464
532
|
});
|
|
465
533
|
|
|
@@ -570,12 +638,17 @@ function compareSiblingOrder(a, b, keyName = 'order_key') {
|
|
|
570
638
|
return String(a?.created_at || '').localeCompare(String(b?.created_at || ''));
|
|
571
639
|
}
|
|
572
640
|
|
|
573
|
-
function populateParentSelect(items) {
|
|
641
|
+
function populateParentSelect(items, preferredParentId = null) {
|
|
574
642
|
const select = document.getElementById('new-item-parent');
|
|
575
643
|
if (!select) return;
|
|
576
|
-
|
|
644
|
+
const expectedValue = preferredParentId === null || preferredParentId === undefined
|
|
645
|
+
? String(select.value || '')
|
|
646
|
+
: String(preferredParentId || '');
|
|
647
|
+
|
|
577
648
|
select.innerHTML = '<option value="">无(顶级)</option>' +
|
|
578
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 : '';
|
|
579
652
|
}
|
|
580
653
|
|
|
581
654
|
function renderMarkdownSnippet(text) {
|
|
@@ -1285,6 +1358,13 @@ function openQuickChatPopover(nodeId, anchorEl) {
|
|
|
1285
1358
|
}
|
|
1286
1359
|
|
|
1287
1360
|
async function loadSandboxes() {
|
|
1361
|
+
if (!state.pageAccess.sandboxes && !state.fullAccess) {
|
|
1362
|
+
state.sandboxes = [];
|
|
1363
|
+
renderSandboxes();
|
|
1364
|
+
renderSandboxesSummary();
|
|
1365
|
+
updateSandboxSelect();
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1288
1368
|
state.sandboxes = await apiRequest(`${API_BASE}/sandboxes`);
|
|
1289
1369
|
renderSandboxes();
|
|
1290
1370
|
renderSandboxesSummary();
|
|
@@ -1317,18 +1397,19 @@ function renderSandboxes() {
|
|
|
1317
1397
|
onOpen: (id) => {
|
|
1318
1398
|
window.location.hash = `/sandbox/${id}`;
|
|
1319
1399
|
},
|
|
1320
|
-
onDelete: state.readonly ? undefined : async (id) => {
|
|
1400
|
+
onDelete: (state.readonly || !state.fullAccess) ? undefined : async (id) => {
|
|
1321
1401
|
if (confirm('确定删除此沙盘?')) {
|
|
1322
1402
|
await apiRequest(`${API_BASE}/sandboxes/${id}`, { method: 'DELETE' });
|
|
1323
1403
|
await loadSandboxes();
|
|
1324
1404
|
}
|
|
1325
1405
|
},
|
|
1326
|
-
readonly: state.readonly,
|
|
1406
|
+
readonly: state.readonly || !state.fullAccess,
|
|
1327
1407
|
});
|
|
1328
1408
|
}
|
|
1329
1409
|
|
|
1330
1410
|
async function loadSandbox(id) {
|
|
1331
1411
|
closeQuickChatPopover();
|
|
1412
|
+
state.currentSandboxWritable = canWriteSandboxById(id);
|
|
1332
1413
|
const [sandbox, items, diaries, entities, entityStats] = await Promise.all([
|
|
1333
1414
|
apiRequest(`${API_BASE}/sandboxes/${id}`),
|
|
1334
1415
|
apiRequest(`${API_BASE}/sandboxes/${id}/items`),
|
|
@@ -1350,7 +1431,8 @@ async function loadSandbox(id) {
|
|
|
1350
1431
|
renderSandboxOverview();
|
|
1351
1432
|
renderWorkTree();
|
|
1352
1433
|
applySandboxLayoutHeight();
|
|
1353
|
-
|
|
1434
|
+
applyReadonlyMode();
|
|
1435
|
+
if (state.readonly || !state.currentSandboxWritable) {
|
|
1354
1436
|
state.chats = [];
|
|
1355
1437
|
const messages = document.getElementById('sandbox-chat-messages');
|
|
1356
1438
|
if (messages) messages.innerHTML = '';
|
|
@@ -1847,6 +1929,252 @@ async function loadSettings() {
|
|
|
1847
1929
|
apiKeyInput.placeholder = state.settings.has_api_key ? '已配置,留空表示不修改' : 'sk-...';
|
|
1848
1930
|
document.getElementById('setting-model').value = state.settings.model || '';
|
|
1849
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
|
+
};
|
|
1850
2178
|
}
|
|
1851
2179
|
|
|
1852
2180
|
function createSettingHeaderRow(key = '', value = '') {
|
|
@@ -1913,6 +2241,7 @@ function editWorkItem(id) {
|
|
|
1913
2241
|
document.getElementById('new-item-assignee').value = item.assignee;
|
|
1914
2242
|
document.getElementById('new-item-status').value = item.status;
|
|
1915
2243
|
document.getElementById('new-item-priority').value = item.priority;
|
|
2244
|
+
document.getElementById('new-item-parent').value = String(item.parent_id || '');
|
|
1916
2245
|
|
|
1917
2246
|
document.getElementById('item-dialog').dataset.editId = id;
|
|
1918
2247
|
document.getElementById('item-dialog').showModal();
|
|
@@ -2364,11 +2693,13 @@ async function initApp() {
|
|
|
2364
2693
|
});
|
|
2365
2694
|
|
|
2366
2695
|
document.getElementById('save-settings-btn')?.addEventListener('click', async () => {
|
|
2367
|
-
if (state.readonly) return;
|
|
2696
|
+
if (state.readonly || !state.canAccessSystemSettings) return;
|
|
2368
2697
|
const api_url = document.getElementById('setting-api-url').value;
|
|
2369
2698
|
const api_key = document.getElementById('setting-api-key').value;
|
|
2370
2699
|
const model = document.getElementById('setting-model').value;
|
|
2371
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));
|
|
2372
2703
|
|
|
2373
2704
|
await apiRequest(`${API_BASE}/settings/api_url`, {
|
|
2374
2705
|
method: 'PUT',
|
|
@@ -2388,18 +2719,239 @@ async function initApp() {
|
|
|
2388
2719
|
method: 'PUT',
|
|
2389
2720
|
body: JSON.stringify({ value: headers }),
|
|
2390
2721
|
});
|
|
2722
|
+
await apiRequest(`${API_BASE}/settings/editable_ip_allowlist`, {
|
|
2723
|
+
method: 'PUT',
|
|
2724
|
+
body: JSON.stringify({ value: editableIps }),
|
|
2725
|
+
});
|
|
2391
2726
|
|
|
2392
2727
|
alert('设置已保存');
|
|
2728
|
+
await loadRuntimeMode();
|
|
2729
|
+
applyReadonlyMode();
|
|
2393
2730
|
await loadSettings();
|
|
2394
2731
|
});
|
|
2395
2732
|
|
|
2396
2733
|
document.getElementById('add-setting-header-btn')?.addEventListener('click', () => {
|
|
2397
|
-
if (state.readonly) return;
|
|
2734
|
+
if (state.readonly || !state.canAccessSystemSettings) return;
|
|
2398
2735
|
const list = document.getElementById('setting-headers-list');
|
|
2399
2736
|
if (!(list instanceof HTMLElement)) return;
|
|
2400
2737
|
list.appendChild(createSettingHeaderRow('', ''));
|
|
2401
2738
|
});
|
|
2402
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
|
+
|
|
2403
2955
|
document.getElementById('work-item-search')?.addEventListener('input', (e) => {
|
|
2404
2956
|
state.workItemSearch = e.target.value || '';
|
|
2405
2957
|
renderWorkTree();
|
|
@@ -2555,13 +3107,31 @@ async function initApp() {
|
|
|
2555
3107
|
}
|
|
2556
3108
|
|
|
2557
3109
|
if (pathHash === '/') {
|
|
2558
|
-
|
|
2559
|
-
|
|
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
|
+
}
|
|
2560
3122
|
} else if (pathHash === '/sandboxes') {
|
|
3123
|
+
if (!state.pageAccess.sandboxes && !state.fullAccess) {
|
|
3124
|
+
window.location.hash = '/';
|
|
3125
|
+
return;
|
|
3126
|
+
}
|
|
2561
3127
|
showPage('sandboxes');
|
|
2562
3128
|
await loadSandboxes();
|
|
2563
3129
|
} else if (pathHash.startsWith('/sandbox/')) {
|
|
2564
3130
|
const id = decodeURIComponent(pathHash.split('/')[2] || '');
|
|
3131
|
+
if (!canReadSandboxById(id)) {
|
|
3132
|
+
window.location.hash = '/';
|
|
3133
|
+
return;
|
|
3134
|
+
}
|
|
2565
3135
|
showPage('sandbox-detail');
|
|
2566
3136
|
await loadSandbox(id);
|
|
2567
3137
|
const targetNodeId = String(query.get('node_id') || '').trim();
|
|
@@ -2571,24 +3141,34 @@ async function initApp() {
|
|
|
2571
3141
|
showNodeEntityDrawer(targetNodeId, preferredFilter);
|
|
2572
3142
|
}
|
|
2573
3143
|
} else if (pathHash === '/diaries') {
|
|
3144
|
+
if (!state.pageAccess.diaries && !state.fullAccess) {
|
|
3145
|
+
window.location.hash = '/';
|
|
3146
|
+
return;
|
|
3147
|
+
}
|
|
2574
3148
|
showPage('diaries');
|
|
2575
3149
|
await loadDiaries();
|
|
2576
3150
|
await loadSandboxes();
|
|
2577
3151
|
} else if (pathHash === '/changes') {
|
|
3152
|
+
if (!state.pageAccess.changes && !state.fullAccess) {
|
|
3153
|
+
window.location.hash = '/';
|
|
3154
|
+
return;
|
|
3155
|
+
}
|
|
2578
3156
|
showPage('changes');
|
|
2579
3157
|
await loadSandboxes();
|
|
2580
3158
|
await loadChanges();
|
|
2581
3159
|
} else if (pathHash === '/settings') {
|
|
2582
|
-
if (state.
|
|
2583
|
-
|
|
2584
|
-
window.location.hash = '/sandboxes';
|
|
2585
|
-
return;
|
|
2586
|
-
}
|
|
2587
|
-
showPage('sandboxes');
|
|
2588
|
-
await loadSandboxes();
|
|
3160
|
+
if (!state.pageAccess.settings && !state.fullAccess) {
|
|
3161
|
+
window.location.hash = '/';
|
|
2589
3162
|
return;
|
|
2590
3163
|
}
|
|
2591
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');
|
|
2592
3172
|
await loadSettings();
|
|
2593
3173
|
}
|
|
2594
3174
|
}
|