@qnote/q-ai-note 1.0.6 → 1.0.8
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 -39
- package/dist/web/index.html +103 -59
- package/dist/web/styles.css +194 -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,30 @@ 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('add-sandbox-btn', state.readonly || !state.fullAccess);
|
|
186
|
+
setHiddenById('add-item-btn', state.readonly || !state.currentSandboxWritable);
|
|
187
|
+
setHiddenById('toggle-node-entity-form-btn', state.readonly || !state.currentSandboxWritable);
|
|
188
|
+
setHiddenById('drawer-diary-save-btn', state.readonly || !state.currentSandboxWritable);
|
|
124
189
|
setHiddenById('save-diary-btn', state.readonly);
|
|
125
190
|
const diaryForm = document.querySelector('#page-diaries .diary-form');
|
|
126
191
|
if (diaryForm instanceof HTMLElement) {
|
|
@@ -128,7 +193,7 @@ function applyReadonlyMode() {
|
|
|
128
193
|
}
|
|
129
194
|
const drawerDiaryForm = document.querySelector('#node-entity-drawer .drawer-diary-quick-form');
|
|
130
195
|
if (drawerDiaryForm instanceof HTMLElement) {
|
|
131
|
-
drawerDiaryForm.classList.toggle('hidden', state.readonly);
|
|
196
|
+
drawerDiaryForm.classList.toggle('hidden', state.readonly || !state.currentSandboxWritable);
|
|
132
197
|
}
|
|
133
198
|
if (state.readonly) {
|
|
134
199
|
state.sandboxChatVisible = false;
|
|
@@ -302,6 +367,7 @@ function applyWorkItemElementPreviewMode() {
|
|
|
302
367
|
function renderWorkTree() {
|
|
303
368
|
const tree = document.getElementById('work-tree');
|
|
304
369
|
if (!tree || !state.currentSandbox) return;
|
|
370
|
+
const treeReadonly = state.readonly || !state.currentSandboxWritable;
|
|
305
371
|
|
|
306
372
|
const allItems = state.currentSandbox.items || [];
|
|
307
373
|
const items = applyWorkItemFilters(allItems);
|
|
@@ -317,7 +383,7 @@ function renderWorkTree() {
|
|
|
317
383
|
}
|
|
318
384
|
|
|
319
385
|
if (allItems.length === 0) {
|
|
320
|
-
tree.innerHTML = `<div class="empty-state"><p>${
|
|
386
|
+
tree.innerHTML = `<div class="empty-state"><p>${treeReadonly ? '暂无任务' : '点击上方"添加"按钮创建第一个任务'}</p></div>`;
|
|
321
387
|
return;
|
|
322
388
|
}
|
|
323
389
|
|
|
@@ -333,7 +399,7 @@ function renderWorkTree() {
|
|
|
333
399
|
}
|
|
334
400
|
renderWorkTree();
|
|
335
401
|
},
|
|
336
|
-
onAddChild:
|
|
402
|
+
onAddChild: treeReadonly ? undefined : (parentId) => {
|
|
337
403
|
document.getElementById('item-dialog-title').textContent = '添加子任务';
|
|
338
404
|
document.getElementById('item-dialog').dataset.editId = '';
|
|
339
405
|
document.getElementById('new-item-name').value = '';
|
|
@@ -344,14 +410,14 @@ function renderWorkTree() {
|
|
|
344
410
|
document.getElementById('new-item-parent').value = parentId;
|
|
345
411
|
document.getElementById('item-dialog').showModal();
|
|
346
412
|
},
|
|
347
|
-
onAddDiary:
|
|
413
|
+
onAddDiary: treeReadonly ? undefined : (nodeId) => {
|
|
348
414
|
showNodeEntityDrawer(nodeId, 'diary');
|
|
349
415
|
const textarea = document.getElementById('drawer-diary-content');
|
|
350
416
|
if (textarea instanceof HTMLTextAreaElement) {
|
|
351
417
|
textarea.focus();
|
|
352
418
|
}
|
|
353
419
|
},
|
|
354
|
-
onEdit:
|
|
420
|
+
onEdit: treeReadonly ? undefined : (id) => {
|
|
355
421
|
editWorkItem(id);
|
|
356
422
|
},
|
|
357
423
|
onSelect: (id) => {
|
|
@@ -360,7 +426,7 @@ function renderWorkTree() {
|
|
|
360
426
|
onSelectEntity: (nodeId, entityType) => {
|
|
361
427
|
showNodeEntityDrawer(nodeId, entityType || 'all');
|
|
362
428
|
},
|
|
363
|
-
onMoveNode:
|
|
429
|
+
onMoveNode: treeReadonly ? undefined : async (dragNodeId, newParentId) => {
|
|
364
430
|
if (!state.currentSandbox) return;
|
|
365
431
|
if (!dragNodeId || dragNodeId === newParentId) return;
|
|
366
432
|
const byId = new Map((state.currentSandbox.items || []).map((item) => [item.id, item]));
|
|
@@ -388,7 +454,7 @@ function renderWorkTree() {
|
|
|
388
454
|
});
|
|
389
455
|
await loadSandbox(state.currentSandbox.id);
|
|
390
456
|
},
|
|
391
|
-
onReorderSiblings:
|
|
457
|
+
onReorderSiblings: treeReadonly ? undefined : async (dragNodeId, targetNodeId, position) => {
|
|
392
458
|
if (!state.currentSandbox) return;
|
|
393
459
|
if (!dragNodeId || !targetNodeId || dragNodeId === targetNodeId) return;
|
|
394
460
|
const byId = new Map((state.currentSandbox.items || []).map((item) => [item.id, item]));
|
|
@@ -421,7 +487,7 @@ function renderWorkTree() {
|
|
|
421
487
|
});
|
|
422
488
|
await loadSandbox(state.currentSandbox.id);
|
|
423
489
|
},
|
|
424
|
-
onReorderLanes:
|
|
490
|
+
onReorderLanes: treeReadonly ? undefined : async (dragRootId, targetRootId) => {
|
|
425
491
|
if (!state.currentSandbox) return;
|
|
426
492
|
if (!dragRootId || !targetRootId || dragRootId === targetRootId) return;
|
|
427
493
|
const roots = (state.currentSandbox.items || []).filter((item) => !item.parent_id);
|
|
@@ -446,20 +512,20 @@ function renderWorkTree() {
|
|
|
446
512
|
});
|
|
447
513
|
await loadSandbox(state.currentSandbox.id);
|
|
448
514
|
},
|
|
449
|
-
onDelete:
|
|
515
|
+
onDelete: treeReadonly ? undefined : async (id) => {
|
|
450
516
|
if (confirm('确定删除此任务?')) {
|
|
451
517
|
await apiRequest(`${API_BASE}/items/${id}`, { method: 'DELETE' });
|
|
452
518
|
await loadSandbox(state.currentSandbox.id);
|
|
453
519
|
}
|
|
454
520
|
},
|
|
455
|
-
onQuickChat:
|
|
521
|
+
onQuickChat: treeReadonly ? undefined : (id, el) => {
|
|
456
522
|
openQuickChatPopover(id, el);
|
|
457
523
|
},
|
|
458
524
|
renderMode: state.workTreeViewMode === 'dense' ? 'dense' : 'card',
|
|
459
525
|
showAssignee: state.workItemShowAssignee,
|
|
460
526
|
elementPreviewMode: state.workItemElementPreviewMode,
|
|
461
527
|
entityRowsByNodeId,
|
|
462
|
-
readonly:
|
|
528
|
+
readonly: treeReadonly,
|
|
463
529
|
selectedId: state.selectedNodeId || '',
|
|
464
530
|
});
|
|
465
531
|
|
|
@@ -570,12 +636,17 @@ function compareSiblingOrder(a, b, keyName = 'order_key') {
|
|
|
570
636
|
return String(a?.created_at || '').localeCompare(String(b?.created_at || ''));
|
|
571
637
|
}
|
|
572
638
|
|
|
573
|
-
function populateParentSelect(items) {
|
|
639
|
+
function populateParentSelect(items, preferredParentId = null) {
|
|
574
640
|
const select = document.getElementById('new-item-parent');
|
|
575
641
|
if (!select) return;
|
|
576
|
-
|
|
642
|
+
const expectedValue = preferredParentId === null || preferredParentId === undefined
|
|
643
|
+
? String(select.value || '')
|
|
644
|
+
: String(preferredParentId || '');
|
|
645
|
+
|
|
577
646
|
select.innerHTML = '<option value="">无(顶级)</option>' +
|
|
578
647
|
items.map(i => `<option value="${i.id}">${safeText(i.name)}</option>`).join('');
|
|
648
|
+
const hasExpectedValue = Array.from(select.options).some((option) => option.value === expectedValue);
|
|
649
|
+
select.value = hasExpectedValue ? expectedValue : '';
|
|
579
650
|
}
|
|
580
651
|
|
|
581
652
|
function renderMarkdownSnippet(text) {
|
|
@@ -1285,6 +1356,13 @@ function openQuickChatPopover(nodeId, anchorEl) {
|
|
|
1285
1356
|
}
|
|
1286
1357
|
|
|
1287
1358
|
async function loadSandboxes() {
|
|
1359
|
+
if (!state.pageAccess.sandboxes && !state.fullAccess) {
|
|
1360
|
+
state.sandboxes = [];
|
|
1361
|
+
renderSandboxes();
|
|
1362
|
+
renderSandboxesSummary();
|
|
1363
|
+
updateSandboxSelect();
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1288
1366
|
state.sandboxes = await apiRequest(`${API_BASE}/sandboxes`);
|
|
1289
1367
|
renderSandboxes();
|
|
1290
1368
|
renderSandboxesSummary();
|
|
@@ -1317,18 +1395,19 @@ function renderSandboxes() {
|
|
|
1317
1395
|
onOpen: (id) => {
|
|
1318
1396
|
window.location.hash = `/sandbox/${id}`;
|
|
1319
1397
|
},
|
|
1320
|
-
onDelete: state.readonly ? undefined : async (id) => {
|
|
1398
|
+
onDelete: (state.readonly || !state.fullAccess) ? undefined : async (id) => {
|
|
1321
1399
|
if (confirm('确定删除此沙盘?')) {
|
|
1322
1400
|
await apiRequest(`${API_BASE}/sandboxes/${id}`, { method: 'DELETE' });
|
|
1323
1401
|
await loadSandboxes();
|
|
1324
1402
|
}
|
|
1325
1403
|
},
|
|
1326
|
-
readonly: state.readonly,
|
|
1404
|
+
readonly: state.readonly || !state.fullAccess,
|
|
1327
1405
|
});
|
|
1328
1406
|
}
|
|
1329
1407
|
|
|
1330
1408
|
async function loadSandbox(id) {
|
|
1331
1409
|
closeQuickChatPopover();
|
|
1410
|
+
state.currentSandboxWritable = canWriteSandboxById(id);
|
|
1332
1411
|
const [sandbox, items, diaries, entities, entityStats] = await Promise.all([
|
|
1333
1412
|
apiRequest(`${API_BASE}/sandboxes/${id}`),
|
|
1334
1413
|
apiRequest(`${API_BASE}/sandboxes/${id}/items`),
|
|
@@ -1350,7 +1429,8 @@ async function loadSandbox(id) {
|
|
|
1350
1429
|
renderSandboxOverview();
|
|
1351
1430
|
renderWorkTree();
|
|
1352
1431
|
applySandboxLayoutHeight();
|
|
1353
|
-
|
|
1432
|
+
applyReadonlyMode();
|
|
1433
|
+
if (state.readonly || !state.currentSandboxWritable) {
|
|
1354
1434
|
state.chats = [];
|
|
1355
1435
|
const messages = document.getElementById('sandbox-chat-messages');
|
|
1356
1436
|
if (messages) messages.innerHTML = '';
|
|
@@ -1847,6 +1927,252 @@ async function loadSettings() {
|
|
|
1847
1927
|
apiKeyInput.placeholder = state.settings.has_api_key ? '已配置,留空表示不修改' : 'sk-...';
|
|
1848
1928
|
document.getElementById('setting-model').value = state.settings.model || '';
|
|
1849
1929
|
renderSettingHeadersRows(state.settings.headers || {});
|
|
1930
|
+
const editableIpInput = document.getElementById('setting-editable-ips');
|
|
1931
|
+
if (editableIpInput instanceof HTMLTextAreaElement) {
|
|
1932
|
+
editableIpInput.value = Array.isArray(state.settings.editable_ip_allowlist)
|
|
1933
|
+
? state.settings.editable_ip_allowlist.join('\n')
|
|
1934
|
+
: '';
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
function normalizeProjectSettingsDraft(config) {
|
|
1939
|
+
const users = Array.isArray(config?.users) ? config.users : [];
|
|
1940
|
+
const profiles = Array.isArray(config?.profiles) ? config.profiles : [];
|
|
1941
|
+
const bindings = Array.isArray(config?.bindings) ? config.bindings : [];
|
|
1942
|
+
return {
|
|
1943
|
+
users: users.map((user, idx) => ({
|
|
1944
|
+
id: String(user?.id || `user-${idx + 1}`).trim(),
|
|
1945
|
+
name: String(user?.name || '').trim(),
|
|
1946
|
+
ips: Array.isArray(user?.ips) ? user.ips.map((ip) => String(ip || '').trim()).filter((ip) => Boolean(ip)) : [],
|
|
1947
|
+
})),
|
|
1948
|
+
profiles: profiles.map((profile, idx) => ({
|
|
1949
|
+
id: String(profile?.id || `profile-${idx + 1}`).trim(),
|
|
1950
|
+
name: String(profile?.name || '').trim(),
|
|
1951
|
+
apply_to_all_users: Boolean(profile?.apply_to_all_users),
|
|
1952
|
+
page_access: {
|
|
1953
|
+
sandboxes: Boolean(profile?.page_access?.sandboxes),
|
|
1954
|
+
diaries: Boolean(profile?.page_access?.diaries),
|
|
1955
|
+
changes: Boolean(profile?.page_access?.changes),
|
|
1956
|
+
settings: Boolean(profile?.page_access?.settings),
|
|
1957
|
+
},
|
|
1958
|
+
sandbox_access: {
|
|
1959
|
+
all: String(profile?.sandbox_access?.all || 'none'),
|
|
1960
|
+
items: Array.isArray(profile?.sandbox_access?.items)
|
|
1961
|
+
? profile.sandbox_access.items.map((item) => ({
|
|
1962
|
+
sandbox_id: String(item?.sandbox_id || '').trim(),
|
|
1963
|
+
access: String(item?.access || 'read'),
|
|
1964
|
+
}))
|
|
1965
|
+
: [],
|
|
1966
|
+
},
|
|
1967
|
+
})),
|
|
1968
|
+
bindings: bindings.map((binding) => ({
|
|
1969
|
+
user_id: String(binding?.user_id || '').trim(),
|
|
1970
|
+
profile_ids: Array.isArray(binding?.profile_ids)
|
|
1971
|
+
? binding.profile_ids.map((id) => String(id || '').trim()).filter((id) => Boolean(id))
|
|
1972
|
+
: [],
|
|
1973
|
+
})),
|
|
1974
|
+
};
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
function renderProjectUsersEditor() {
|
|
1978
|
+
const container = document.getElementById('project-users-list');
|
|
1979
|
+
if (!(container instanceof HTMLElement)) return;
|
|
1980
|
+
const users = state.projectSettingsDraft?.users || [];
|
|
1981
|
+
if (!users.length) {
|
|
1982
|
+
container.innerHTML = '<div class="project-empty">暂无用户,点击上方“添加用户”。</div>';
|
|
1983
|
+
return;
|
|
1984
|
+
}
|
|
1985
|
+
container.innerHTML = users.map((user, idx) => `
|
|
1986
|
+
<div class="project-row">
|
|
1987
|
+
<input type="text" data-role="project-user-id" data-idx="${idx}" placeholder="用户 ID(唯一)" value="${escapeHtml(user.id)}">
|
|
1988
|
+
<input type="text" data-role="project-user-name" data-idx="${idx}" placeholder="显示名称" value="${escapeHtml(user.name)}">
|
|
1989
|
+
<div class="project-ip-editor">
|
|
1990
|
+
<div class="project-ip-chips">
|
|
1991
|
+
${(user.ips || []).map((ip, ipIdx) => `
|
|
1992
|
+
<span class="project-ip-chip">
|
|
1993
|
+
<span>${escapeHtml(ip)}</span>
|
|
1994
|
+
<button class="project-ip-remove" type="button" title="删除 IP" data-role="remove-project-user-ip" data-idx="${idx}" data-ip-idx="${ipIdx}">×</button>
|
|
1995
|
+
</span>
|
|
1996
|
+
`).join('')}
|
|
1997
|
+
</div>
|
|
1998
|
+
<div class="project-ip-input-row">
|
|
1999
|
+
<input type="text" data-role="project-user-ip-input" data-idx="${idx}" placeholder="输入 IP 后回车或点击添加">
|
|
2000
|
+
<button class="btn btn-secondary btn-sm" data-role="add-project-user-ip" data-idx="${idx}" type="button">添加 IP</button>
|
|
2001
|
+
</div>
|
|
2002
|
+
</div>
|
|
2003
|
+
<button class="btn btn-secondary btn-sm" data-role="remove-project-user" data-idx="${idx}" type="button">删除</button>
|
|
2004
|
+
</div>
|
|
2005
|
+
`).join('');
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
function addUserIpAtIndex(userIdx, rawIp) {
|
|
2009
|
+
if (!state.projectSettingsDraft) return;
|
|
2010
|
+
const idx = Number(userIdx);
|
|
2011
|
+
if (!Number.isFinite(idx) || idx < 0) return;
|
|
2012
|
+
const user = state.projectSettingsDraft.users[idx];
|
|
2013
|
+
if (!user) return;
|
|
2014
|
+
const ip = String(rawIp || '').trim();
|
|
2015
|
+
if (!ip) return;
|
|
2016
|
+
if (ip.includes('/')) {
|
|
2017
|
+
alert('仅支持精确 IP,不支持 CIDR。');
|
|
2018
|
+
return;
|
|
2019
|
+
}
|
|
2020
|
+
if ((user.ips || []).includes(ip)) return;
|
|
2021
|
+
user.ips = [...(user.ips || []), ip];
|
|
2022
|
+
renderProjectSettingsEditor();
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
function renderProjectProfilesEditor() {
|
|
2026
|
+
const container = document.getElementById('project-profiles-list');
|
|
2027
|
+
if (!(container instanceof HTMLElement)) return;
|
|
2028
|
+
const profiles = state.projectSettingsDraft?.profiles || [];
|
|
2029
|
+
const sandboxOptions = (state.sandboxes || []).map((sandbox) => ({
|
|
2030
|
+
id: String(sandbox.id || ''),
|
|
2031
|
+
name: String(sandbox.name || ''),
|
|
2032
|
+
}));
|
|
2033
|
+
if (!profiles.length) {
|
|
2034
|
+
container.innerHTML = '<div class="project-empty">暂无 Profile,点击上方“添加 Profile”。</div>';
|
|
2035
|
+
return;
|
|
2036
|
+
}
|
|
2037
|
+
container.innerHTML = profiles.map((profile, idx) => `
|
|
2038
|
+
<div class="project-profile-card">
|
|
2039
|
+
<div class="project-profile-grid">
|
|
2040
|
+
<input type="text" data-role="project-profile-id" data-idx="${idx}" placeholder="Profile ID(唯一)" value="${escapeHtml(profile.id)}">
|
|
2041
|
+
<input type="text" data-role="project-profile-name" data-idx="${idx}" placeholder="Profile 名称" value="${escapeHtml(profile.name)}">
|
|
2042
|
+
<select data-role="project-profile-all-access" data-idx="${idx}">
|
|
2043
|
+
<option value="none" ${profile.sandbox_access.all === 'none' ? 'selected' : ''}>默认沙盘权限:none</option>
|
|
2044
|
+
<option value="read" ${profile.sandbox_access.all === 'read' ? 'selected' : ''}>默认沙盘权限:read</option>
|
|
2045
|
+
<option value="write" ${profile.sandbox_access.all === 'write' ? 'selected' : ''}>默认沙盘权限:write</option>
|
|
2046
|
+
</select>
|
|
2047
|
+
<label class="inline-checkbox">
|
|
2048
|
+
<input type="checkbox" data-role="project-profile-apply-all" data-idx="${idx}" ${profile.apply_to_all_users ? 'checked' : ''}>
|
|
2049
|
+
<span>apply_to_all_users</span>
|
|
2050
|
+
</label>
|
|
2051
|
+
</div>
|
|
2052
|
+
<div class="project-profile-pages">
|
|
2053
|
+
<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>
|
|
2054
|
+
<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>
|
|
2055
|
+
<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>
|
|
2056
|
+
<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>
|
|
2057
|
+
</div>
|
|
2058
|
+
<div class="project-sandbox-items">
|
|
2059
|
+
${(profile.sandbox_access.items || []).map((item, itemIdx) => `
|
|
2060
|
+
<div class="project-row compact">
|
|
2061
|
+
<select data-role="project-profile-sandbox-id" data-idx="${idx}" data-item-idx="${itemIdx}">
|
|
2062
|
+
<option value="">选择沙盘</option>
|
|
2063
|
+
${sandboxOptions.map((sandbox) => `
|
|
2064
|
+
<option value="${escapeHtml(sandbox.id)}" ${item.sandbox_id === sandbox.id ? 'selected' : ''}>
|
|
2065
|
+
${escapeHtml(sandbox.name || sandbox.id)} (${escapeHtml(sandbox.id)})
|
|
2066
|
+
</option>
|
|
2067
|
+
`).join('')}
|
|
2068
|
+
${item.sandbox_id && !sandboxOptions.some((sandbox) => sandbox.id === item.sandbox_id) ? `
|
|
2069
|
+
<option value="${escapeHtml(item.sandbox_id)}" selected>
|
|
2070
|
+
已不存在的沙盘 (${escapeHtml(item.sandbox_id)})
|
|
2071
|
+
</option>
|
|
2072
|
+
` : ''}
|
|
2073
|
+
</select>
|
|
2074
|
+
<select data-role="project-profile-sandbox-access" data-idx="${idx}" data-item-idx="${itemIdx}">
|
|
2075
|
+
<option value="none" ${item.access === 'none' ? 'selected' : ''}>none</option>
|
|
2076
|
+
<option value="read" ${item.access === 'read' ? 'selected' : ''}>read</option>
|
|
2077
|
+
<option value="write" ${item.access === 'write' ? 'selected' : ''}>write</option>
|
|
2078
|
+
</select>
|
|
2079
|
+
<button class="btn btn-secondary btn-sm" data-role="remove-project-profile-sandbox-item" data-idx="${idx}" data-item-idx="${itemIdx}" type="button">删除</button>
|
|
2080
|
+
</div>
|
|
2081
|
+
`).join('')}
|
|
2082
|
+
<button class="btn btn-secondary btn-sm" data-role="add-project-profile-sandbox-item" data-idx="${idx}" type="button">+ 添加沙盘白名单规则</button>
|
|
2083
|
+
</div>
|
|
2084
|
+
<div class="project-profile-footer">
|
|
2085
|
+
<button class="btn btn-secondary btn-sm" data-role="remove-project-profile" data-idx="${idx}" type="button">删除 Profile</button>
|
|
2086
|
+
</div>
|
|
2087
|
+
</div>
|
|
2088
|
+
`).join('');
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
function renderProjectBindingsEditor() {
|
|
2092
|
+
const container = document.getElementById('project-bindings-list');
|
|
2093
|
+
if (!(container instanceof HTMLElement)) return;
|
|
2094
|
+
const bindings = state.projectSettingsDraft?.bindings || [];
|
|
2095
|
+
const users = state.projectSettingsDraft?.users || [];
|
|
2096
|
+
const profiles = state.projectSettingsDraft?.profiles || [];
|
|
2097
|
+
if (!bindings.length) {
|
|
2098
|
+
container.innerHTML = '<div class="project-empty">暂无绑定,点击上方“添加绑定”。</div>';
|
|
2099
|
+
return;
|
|
2100
|
+
}
|
|
2101
|
+
container.innerHTML = bindings.map((binding, idx) => `
|
|
2102
|
+
<div class="project-binding-card">
|
|
2103
|
+
<div class="project-row compact">
|
|
2104
|
+
<select data-role="project-binding-user" data-idx="${idx}">
|
|
2105
|
+
<option value="">选择用户</option>
|
|
2106
|
+
${users.map((user) => `<option value="${escapeHtml(user.id)}" ${binding.user_id === user.id ? 'selected' : ''}>${escapeHtml(user.name || user.id)} (${escapeHtml(user.id)})</option>`).join('')}
|
|
2107
|
+
</select>
|
|
2108
|
+
<button class="btn btn-secondary btn-sm" data-role="remove-project-binding" data-idx="${idx}" type="button">删除</button>
|
|
2109
|
+
</div>
|
|
2110
|
+
<div class="project-binding-profiles">
|
|
2111
|
+
${profiles.map((profile) => `
|
|
2112
|
+
<label class="inline-checkbox">
|
|
2113
|
+
<input type="checkbox" data-role="project-binding-profile" data-idx="${idx}" data-profile-id="${escapeHtml(profile.id)}" ${binding.profile_ids.includes(profile.id) ? 'checked' : ''}>
|
|
2114
|
+
<span>${escapeHtml(profile.name || profile.id)}</span>
|
|
2115
|
+
</label>
|
|
2116
|
+
`).join('')}
|
|
2117
|
+
</div>
|
|
2118
|
+
</div>
|
|
2119
|
+
`).join('');
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
function renderProjectSettingsEditor() {
|
|
2123
|
+
renderProjectUsersEditor();
|
|
2124
|
+
renderProjectProfilesEditor();
|
|
2125
|
+
renderProjectBindingsEditor();
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
async function loadProjectSettings() {
|
|
2129
|
+
if (state.fullAccess || state.pageAccess.sandboxes) {
|
|
2130
|
+
try {
|
|
2131
|
+
await loadSandboxes();
|
|
2132
|
+
} catch {
|
|
2133
|
+
// Ignore sandbox list failures; project settings can still be edited.
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
const config = await apiRequest(`${API_BASE}/project-settings`);
|
|
2137
|
+
state.projectSettingsDraft = normalizeProjectSettingsDraft(config);
|
|
2138
|
+
renderProjectSettingsEditor();
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
function collectProjectSettingsPayload() {
|
|
2142
|
+
const draft = state.projectSettingsDraft || { users: [], profiles: [], bindings: [] };
|
|
2143
|
+
return {
|
|
2144
|
+
users: draft.users.map((user) => ({
|
|
2145
|
+
id: String(user.id || '').trim(),
|
|
2146
|
+
name: String(user.name || '').trim(),
|
|
2147
|
+
ips: Array.from(new Set((user.ips || []).map((ip) => String(ip || '').trim()).filter((ip) => Boolean(ip)))),
|
|
2148
|
+
})),
|
|
2149
|
+
profiles: draft.profiles.map((profile) => ({
|
|
2150
|
+
id: String(profile.id || '').trim(),
|
|
2151
|
+
name: String(profile.name || '').trim(),
|
|
2152
|
+
apply_to_all_users: Boolean(profile.apply_to_all_users),
|
|
2153
|
+
page_access: {
|
|
2154
|
+
sandboxes: Boolean(profile.page_access?.sandboxes),
|
|
2155
|
+
diaries: Boolean(profile.page_access?.diaries),
|
|
2156
|
+
changes: Boolean(profile.page_access?.changes),
|
|
2157
|
+
settings: Boolean(profile.page_access?.settings),
|
|
2158
|
+
},
|
|
2159
|
+
sandbox_access: {
|
|
2160
|
+
all: String(profile.sandbox_access?.all || 'none'),
|
|
2161
|
+
items: (profile.sandbox_access?.items || [])
|
|
2162
|
+
.map((item) => ({
|
|
2163
|
+
sandbox_id: String(item.sandbox_id || '').trim(),
|
|
2164
|
+
access: String(item.access || 'none'),
|
|
2165
|
+
}))
|
|
2166
|
+
.filter((item) => Boolean(item.sandbox_id)),
|
|
2167
|
+
},
|
|
2168
|
+
})),
|
|
2169
|
+
bindings: draft.bindings
|
|
2170
|
+
.map((binding) => ({
|
|
2171
|
+
user_id: String(binding.user_id || '').trim(),
|
|
2172
|
+
profile_ids: Array.from(new Set((binding.profile_ids || []).map((id) => String(id || '').trim()).filter((id) => Boolean(id)))),
|
|
2173
|
+
}))
|
|
2174
|
+
.filter((binding) => Boolean(binding.user_id)),
|
|
2175
|
+
};
|
|
1850
2176
|
}
|
|
1851
2177
|
|
|
1852
2178
|
function createSettingHeaderRow(key = '', value = '') {
|
|
@@ -1899,7 +2225,8 @@ function showPage(pageId) {
|
|
|
1899
2225
|
}
|
|
1900
2226
|
|
|
1901
2227
|
document.querySelectorAll('.nav-list a').forEach(a => a.classList.remove('active'));
|
|
1902
|
-
const
|
|
2228
|
+
const navPageId = pageId === 'sandbox-detail' ? 'sandboxes' : pageId;
|
|
2229
|
+
const nav = document.querySelector(`[data-nav="${navPageId}"]`);
|
|
1903
2230
|
if (nav) nav.classList.add('active');
|
|
1904
2231
|
}
|
|
1905
2232
|
|
|
@@ -1913,6 +2240,7 @@ function editWorkItem(id) {
|
|
|
1913
2240
|
document.getElementById('new-item-assignee').value = item.assignee;
|
|
1914
2241
|
document.getElementById('new-item-status').value = item.status;
|
|
1915
2242
|
document.getElementById('new-item-priority').value = item.priority;
|
|
2243
|
+
document.getElementById('new-item-parent').value = String(item.parent_id || '');
|
|
1916
2244
|
|
|
1917
2245
|
document.getElementById('item-dialog').dataset.editId = id;
|
|
1918
2246
|
document.getElementById('item-dialog').showModal();
|
|
@@ -2364,11 +2692,13 @@ async function initApp() {
|
|
|
2364
2692
|
});
|
|
2365
2693
|
|
|
2366
2694
|
document.getElementById('save-settings-btn')?.addEventListener('click', async () => {
|
|
2367
|
-
if (state.readonly) return;
|
|
2695
|
+
if (state.readonly || !state.canAccessSystemSettings) return;
|
|
2368
2696
|
const api_url = document.getElementById('setting-api-url').value;
|
|
2369
2697
|
const api_key = document.getElementById('setting-api-key').value;
|
|
2370
2698
|
const model = document.getElementById('setting-model').value;
|
|
2371
2699
|
const headers = collectSettingHeadersFromUI();
|
|
2700
|
+
const editableIpsRaw = document.getElementById('setting-editable-ips')?.value || '';
|
|
2701
|
+
const editableIps = String(editableIpsRaw).split('\n').map((ip) => ip.trim()).filter((ip) => Boolean(ip));
|
|
2372
2702
|
|
|
2373
2703
|
await apiRequest(`${API_BASE}/settings/api_url`, {
|
|
2374
2704
|
method: 'PUT',
|
|
@@ -2388,18 +2718,239 @@ async function initApp() {
|
|
|
2388
2718
|
method: 'PUT',
|
|
2389
2719
|
body: JSON.stringify({ value: headers }),
|
|
2390
2720
|
});
|
|
2721
|
+
await apiRequest(`${API_BASE}/settings/editable_ip_allowlist`, {
|
|
2722
|
+
method: 'PUT',
|
|
2723
|
+
body: JSON.stringify({ value: editableIps }),
|
|
2724
|
+
});
|
|
2391
2725
|
|
|
2392
2726
|
alert('设置已保存');
|
|
2727
|
+
await loadRuntimeMode();
|
|
2728
|
+
applyReadonlyMode();
|
|
2393
2729
|
await loadSettings();
|
|
2394
2730
|
});
|
|
2395
2731
|
|
|
2396
2732
|
document.getElementById('add-setting-header-btn')?.addEventListener('click', () => {
|
|
2397
|
-
if (state.readonly) return;
|
|
2733
|
+
if (state.readonly || !state.canAccessSystemSettings) return;
|
|
2398
2734
|
const list = document.getElementById('setting-headers-list');
|
|
2399
2735
|
if (!(list instanceof HTMLElement)) return;
|
|
2400
2736
|
list.appendChild(createSettingHeaderRow('', ''));
|
|
2401
2737
|
});
|
|
2402
2738
|
|
|
2739
|
+
document.getElementById('add-project-user-btn')?.addEventListener('click', () => {
|
|
2740
|
+
if (!state.projectSettingsDraft) return;
|
|
2741
|
+
state.projectSettingsDraft.users.push({ id: '', name: '', ips: [] });
|
|
2742
|
+
renderProjectSettingsEditor();
|
|
2743
|
+
});
|
|
2744
|
+
|
|
2745
|
+
document.getElementById('add-project-profile-btn')?.addEventListener('click', () => {
|
|
2746
|
+
if (!state.projectSettingsDraft) return;
|
|
2747
|
+
state.projectSettingsDraft.profiles.push({
|
|
2748
|
+
id: '',
|
|
2749
|
+
name: '',
|
|
2750
|
+
apply_to_all_users: false,
|
|
2751
|
+
page_access: { sandboxes: true, diaries: true, changes: true, settings: false },
|
|
2752
|
+
sandbox_access: { all: 'none', items: [] },
|
|
2753
|
+
});
|
|
2754
|
+
renderProjectSettingsEditor();
|
|
2755
|
+
});
|
|
2756
|
+
|
|
2757
|
+
document.getElementById('add-project-binding-btn')?.addEventListener('click', () => {
|
|
2758
|
+
if (!state.projectSettingsDraft) return;
|
|
2759
|
+
state.projectSettingsDraft.bindings.push({ user_id: '', profile_ids: [] });
|
|
2760
|
+
renderProjectSettingsEditor();
|
|
2761
|
+
});
|
|
2762
|
+
|
|
2763
|
+
document.getElementById('project-users-list')?.addEventListener('click', (event) => {
|
|
2764
|
+
const target = event.target;
|
|
2765
|
+
if (!(target instanceof HTMLElement)) return;
|
|
2766
|
+
if (!state.projectSettingsDraft) return;
|
|
2767
|
+
if (target.dataset.role === 'add-project-user-ip') {
|
|
2768
|
+
const idx = Number(target.dataset.idx);
|
|
2769
|
+
const input = document.querySelector(`[data-role="project-user-ip-input"][data-idx="${idx}"]`);
|
|
2770
|
+
if (input instanceof HTMLInputElement) {
|
|
2771
|
+
addUserIpAtIndex(idx, input.value);
|
|
2772
|
+
}
|
|
2773
|
+
return;
|
|
2774
|
+
}
|
|
2775
|
+
if (target.dataset.role === 'remove-project-user-ip') {
|
|
2776
|
+
const idx = Number(target.dataset.idx);
|
|
2777
|
+
const ipIdx = Number(target.dataset.ipIdx);
|
|
2778
|
+
if (!Number.isFinite(idx) || idx < 0 || !Number.isFinite(ipIdx) || ipIdx < 0) return;
|
|
2779
|
+
const user = state.projectSettingsDraft.users[idx];
|
|
2780
|
+
if (!user) return;
|
|
2781
|
+
user.ips.splice(ipIdx, 1);
|
|
2782
|
+
renderProjectSettingsEditor();
|
|
2783
|
+
return;
|
|
2784
|
+
}
|
|
2785
|
+
if (target.dataset.role !== 'remove-project-user') return;
|
|
2786
|
+
const idx = Number(target.dataset.idx);
|
|
2787
|
+
if (!Number.isFinite(idx) || idx < 0) return;
|
|
2788
|
+
const removed = state.projectSettingsDraft.users[idx];
|
|
2789
|
+
state.projectSettingsDraft.users.splice(idx, 1);
|
|
2790
|
+
if (removed?.id) {
|
|
2791
|
+
state.projectSettingsDraft.bindings = (state.projectSettingsDraft.bindings || []).filter((binding) => binding.user_id !== removed.id);
|
|
2792
|
+
}
|
|
2793
|
+
renderProjectSettingsEditor();
|
|
2794
|
+
});
|
|
2795
|
+
|
|
2796
|
+
document.getElementById('project-users-list')?.addEventListener('input', (event) => {
|
|
2797
|
+
const target = event.target;
|
|
2798
|
+
if (!(target instanceof HTMLInputElement) || !state.projectSettingsDraft) return;
|
|
2799
|
+
const idx = Number(target.dataset.idx);
|
|
2800
|
+
if (!Number.isFinite(idx) || idx < 0) return;
|
|
2801
|
+
const user = state.projectSettingsDraft.users[idx];
|
|
2802
|
+
if (!user) return;
|
|
2803
|
+
if (target.dataset.role === 'project-user-id') {
|
|
2804
|
+
user.id = target.value.trim();
|
|
2805
|
+
} else if (target.dataset.role === 'project-user-name') {
|
|
2806
|
+
user.name = target.value.trim();
|
|
2807
|
+
}
|
|
2808
|
+
});
|
|
2809
|
+
|
|
2810
|
+
document.getElementById('project-users-list')?.addEventListener('keydown', (event) => {
|
|
2811
|
+
const target = event.target;
|
|
2812
|
+
if (!(target instanceof HTMLInputElement) || !state.projectSettingsDraft) return;
|
|
2813
|
+
if (target.dataset.role !== 'project-user-ip-input') return;
|
|
2814
|
+
if (event.key !== 'Enter') return;
|
|
2815
|
+
event.preventDefault();
|
|
2816
|
+
const idx = Number(target.dataset.idx);
|
|
2817
|
+
addUserIpAtIndex(idx, target.value);
|
|
2818
|
+
});
|
|
2819
|
+
|
|
2820
|
+
document.getElementById('project-profiles-list')?.addEventListener('click', (event) => {
|
|
2821
|
+
const target = event.target;
|
|
2822
|
+
if (!(target instanceof HTMLElement) || !state.projectSettingsDraft) return;
|
|
2823
|
+
const profileIdx = Number(target.dataset.idx);
|
|
2824
|
+
if (!Number.isFinite(profileIdx) || profileIdx < 0) return;
|
|
2825
|
+
if (target.dataset.role === 'remove-project-profile') {
|
|
2826
|
+
const removed = state.projectSettingsDraft.profiles[profileIdx];
|
|
2827
|
+
state.projectSettingsDraft.profiles.splice(profileIdx, 1);
|
|
2828
|
+
if (removed?.id) {
|
|
2829
|
+
state.projectSettingsDraft.bindings.forEach((binding) => {
|
|
2830
|
+
binding.profile_ids = (binding.profile_ids || []).filter((id) => id !== removed.id);
|
|
2831
|
+
});
|
|
2832
|
+
}
|
|
2833
|
+
renderProjectSettingsEditor();
|
|
2834
|
+
return;
|
|
2835
|
+
}
|
|
2836
|
+
if (target.dataset.role === 'add-project-profile-sandbox-item') {
|
|
2837
|
+
const profile = state.projectSettingsDraft.profiles[profileIdx];
|
|
2838
|
+
if (!profile) return;
|
|
2839
|
+
profile.sandbox_access.items.push({ sandbox_id: '', access: 'read' });
|
|
2840
|
+
renderProjectSettingsEditor();
|
|
2841
|
+
return;
|
|
2842
|
+
}
|
|
2843
|
+
if (target.dataset.role === 'remove-project-profile-sandbox-item') {
|
|
2844
|
+
const profile = state.projectSettingsDraft.profiles[profileIdx];
|
|
2845
|
+
if (!profile) return;
|
|
2846
|
+
const itemIdx = Number(target.dataset.itemIdx);
|
|
2847
|
+
if (!Number.isFinite(itemIdx) || itemIdx < 0) return;
|
|
2848
|
+
profile.sandbox_access.items.splice(itemIdx, 1);
|
|
2849
|
+
renderProjectSettingsEditor();
|
|
2850
|
+
}
|
|
2851
|
+
});
|
|
2852
|
+
|
|
2853
|
+
document.getElementById('project-profiles-list')?.addEventListener('input', (event) => {
|
|
2854
|
+
const target = event.target;
|
|
2855
|
+
if (!(target instanceof HTMLInputElement) || !state.projectSettingsDraft) return;
|
|
2856
|
+
const profileIdx = Number(target.dataset.idx);
|
|
2857
|
+
if (!Number.isFinite(profileIdx) || profileIdx < 0) return;
|
|
2858
|
+
const profile = state.projectSettingsDraft.profiles[profileIdx];
|
|
2859
|
+
if (!profile) return;
|
|
2860
|
+
if (target.dataset.role === 'project-profile-id') {
|
|
2861
|
+
profile.id = target.value.trim();
|
|
2862
|
+
return;
|
|
2863
|
+
}
|
|
2864
|
+
if (target.dataset.role === 'project-profile-name') {
|
|
2865
|
+
profile.name = target.value.trim();
|
|
2866
|
+
return;
|
|
2867
|
+
}
|
|
2868
|
+
if (target.dataset.role === 'project-profile-apply-all') {
|
|
2869
|
+
profile.apply_to_all_users = target.checked;
|
|
2870
|
+
return;
|
|
2871
|
+
}
|
|
2872
|
+
if (target.dataset.role === 'project-profile-page') {
|
|
2873
|
+
const page = target.dataset.page;
|
|
2874
|
+
if (page === 'sandboxes' || page === 'diaries' || page === 'changes' || page === 'settings') {
|
|
2875
|
+
profile.page_access[page] = target.checked;
|
|
2876
|
+
}
|
|
2877
|
+
return;
|
|
2878
|
+
}
|
|
2879
|
+
});
|
|
2880
|
+
|
|
2881
|
+
document.getElementById('project-profiles-list')?.addEventListener('change', (event) => {
|
|
2882
|
+
const target = event.target;
|
|
2883
|
+
if (!(target instanceof HTMLSelectElement) || !state.projectSettingsDraft) return;
|
|
2884
|
+
const profileIdx = Number(target.dataset.idx);
|
|
2885
|
+
if (!Number.isFinite(profileIdx) || profileIdx < 0) return;
|
|
2886
|
+
const profile = state.projectSettingsDraft.profiles[profileIdx];
|
|
2887
|
+
if (!profile) return;
|
|
2888
|
+
if (target.dataset.role === 'project-profile-all-access') {
|
|
2889
|
+
profile.sandbox_access.all = target.value || 'none';
|
|
2890
|
+
return;
|
|
2891
|
+
}
|
|
2892
|
+
if (target.dataset.role === 'project-profile-sandbox-id') {
|
|
2893
|
+
const itemIdx = Number(target.dataset.itemIdx);
|
|
2894
|
+
if (!Number.isFinite(itemIdx) || itemIdx < 0) return;
|
|
2895
|
+
const item = profile.sandbox_access.items[itemIdx];
|
|
2896
|
+
if (item) item.sandbox_id = String(target.value || '').trim();
|
|
2897
|
+
return;
|
|
2898
|
+
}
|
|
2899
|
+
if (target.dataset.role === 'project-profile-sandbox-access') {
|
|
2900
|
+
const itemIdx = Number(target.dataset.itemIdx);
|
|
2901
|
+
if (!Number.isFinite(itemIdx) || itemIdx < 0) return;
|
|
2902
|
+
const item = profile.sandbox_access.items[itemIdx];
|
|
2903
|
+
if (item) item.access = target.value || 'none';
|
|
2904
|
+
}
|
|
2905
|
+
});
|
|
2906
|
+
|
|
2907
|
+
document.getElementById('project-bindings-list')?.addEventListener('click', (event) => {
|
|
2908
|
+
const target = event.target;
|
|
2909
|
+
if (!(target instanceof HTMLElement) || !state.projectSettingsDraft) return;
|
|
2910
|
+
if (target.dataset.role !== 'remove-project-binding') return;
|
|
2911
|
+
const idx = Number(target.dataset.idx);
|
|
2912
|
+
if (!Number.isFinite(idx) || idx < 0) return;
|
|
2913
|
+
state.projectSettingsDraft.bindings.splice(idx, 1);
|
|
2914
|
+
renderProjectSettingsEditor();
|
|
2915
|
+
});
|
|
2916
|
+
|
|
2917
|
+
document.getElementById('project-bindings-list')?.addEventListener('change', (event) => {
|
|
2918
|
+
const target = event.target;
|
|
2919
|
+
if (!state.projectSettingsDraft) return;
|
|
2920
|
+
const idx = Number(target instanceof HTMLElement ? target.dataset.idx : NaN);
|
|
2921
|
+
if (!Number.isFinite(idx) || idx < 0) return;
|
|
2922
|
+
const binding = state.projectSettingsDraft.bindings[idx];
|
|
2923
|
+
if (!binding) return;
|
|
2924
|
+
if (target instanceof HTMLSelectElement && target.dataset.role === 'project-binding-user') {
|
|
2925
|
+
binding.user_id = target.value || '';
|
|
2926
|
+
return;
|
|
2927
|
+
}
|
|
2928
|
+
if (target instanceof HTMLInputElement && target.dataset.role === 'project-binding-profile') {
|
|
2929
|
+
const profileId = String(target.dataset.profileId || '').trim();
|
|
2930
|
+
const set = new Set(binding.profile_ids || []);
|
|
2931
|
+
if (target.checked) set.add(profileId);
|
|
2932
|
+
else set.delete(profileId);
|
|
2933
|
+
binding.profile_ids = Array.from(set);
|
|
2934
|
+
}
|
|
2935
|
+
});
|
|
2936
|
+
|
|
2937
|
+
document.getElementById('save-project-settings-btn')?.addEventListener('click', async () => {
|
|
2938
|
+
if (state.readonly || (!state.fullAccess && !state.pageAccess.settings)) return;
|
|
2939
|
+
try {
|
|
2940
|
+
const payload = collectProjectSettingsPayload();
|
|
2941
|
+
await apiRequest(`${API_BASE}/project-settings`, {
|
|
2942
|
+
method: 'PUT',
|
|
2943
|
+
body: JSON.stringify(payload),
|
|
2944
|
+
});
|
|
2945
|
+
alert('项目设置已保存');
|
|
2946
|
+
await loadRuntimeMode();
|
|
2947
|
+
applyReadonlyMode();
|
|
2948
|
+
await loadProjectSettings();
|
|
2949
|
+
} catch (error) {
|
|
2950
|
+
alert(`项目设置保存失败: ${error?.message || error}`);
|
|
2951
|
+
}
|
|
2952
|
+
});
|
|
2953
|
+
|
|
2403
2954
|
document.getElementById('work-item-search')?.addEventListener('input', (e) => {
|
|
2404
2955
|
state.workItemSearch = e.target.value || '';
|
|
2405
2956
|
renderWorkTree();
|
|
@@ -2555,13 +3106,31 @@ async function initApp() {
|
|
|
2555
3106
|
}
|
|
2556
3107
|
|
|
2557
3108
|
if (pathHash === '/') {
|
|
2558
|
-
|
|
2559
|
-
|
|
3109
|
+
if (state.pageAccess.sandboxes || state.fullAccess) {
|
|
3110
|
+
showPage('sandboxes');
|
|
3111
|
+
await loadSandboxes();
|
|
3112
|
+
} else if (state.pageAccess.diaries) {
|
|
3113
|
+
window.location.hash = '/diaries';
|
|
3114
|
+
} else if (state.pageAccess.changes) {
|
|
3115
|
+
window.location.hash = '/changes';
|
|
3116
|
+
} else if (state.pageAccess.settings) {
|
|
3117
|
+
window.location.hash = '/settings';
|
|
3118
|
+
} else if (state.canAccessSystemSettings) {
|
|
3119
|
+
window.location.hash = '/system-settings';
|
|
3120
|
+
}
|
|
2560
3121
|
} else if (pathHash === '/sandboxes') {
|
|
3122
|
+
if (!state.pageAccess.sandboxes && !state.fullAccess) {
|
|
3123
|
+
window.location.hash = '/';
|
|
3124
|
+
return;
|
|
3125
|
+
}
|
|
2561
3126
|
showPage('sandboxes');
|
|
2562
3127
|
await loadSandboxes();
|
|
2563
3128
|
} else if (pathHash.startsWith('/sandbox/')) {
|
|
2564
3129
|
const id = decodeURIComponent(pathHash.split('/')[2] || '');
|
|
3130
|
+
if (!canReadSandboxById(id)) {
|
|
3131
|
+
window.location.hash = '/';
|
|
3132
|
+
return;
|
|
3133
|
+
}
|
|
2565
3134
|
showPage('sandbox-detail');
|
|
2566
3135
|
await loadSandbox(id);
|
|
2567
3136
|
const targetNodeId = String(query.get('node_id') || '').trim();
|
|
@@ -2571,24 +3140,34 @@ async function initApp() {
|
|
|
2571
3140
|
showNodeEntityDrawer(targetNodeId, preferredFilter);
|
|
2572
3141
|
}
|
|
2573
3142
|
} else if (pathHash === '/diaries') {
|
|
3143
|
+
if (!state.pageAccess.diaries && !state.fullAccess) {
|
|
3144
|
+
window.location.hash = '/';
|
|
3145
|
+
return;
|
|
3146
|
+
}
|
|
2574
3147
|
showPage('diaries');
|
|
2575
3148
|
await loadDiaries();
|
|
2576
3149
|
await loadSandboxes();
|
|
2577
3150
|
} else if (pathHash === '/changes') {
|
|
3151
|
+
if (!state.pageAccess.changes && !state.fullAccess) {
|
|
3152
|
+
window.location.hash = '/';
|
|
3153
|
+
return;
|
|
3154
|
+
}
|
|
2578
3155
|
showPage('changes');
|
|
2579
3156
|
await loadSandboxes();
|
|
2580
3157
|
await loadChanges();
|
|
2581
3158
|
} else if (pathHash === '/settings') {
|
|
2582
|
-
if (state.
|
|
2583
|
-
|
|
2584
|
-
window.location.hash = '/sandboxes';
|
|
2585
|
-
return;
|
|
2586
|
-
}
|
|
2587
|
-
showPage('sandboxes');
|
|
2588
|
-
await loadSandboxes();
|
|
3159
|
+
if (!state.pageAccess.settings && !state.fullAccess) {
|
|
3160
|
+
window.location.hash = '/';
|
|
2589
3161
|
return;
|
|
2590
3162
|
}
|
|
2591
3163
|
showPage('settings');
|
|
3164
|
+
await loadProjectSettings();
|
|
3165
|
+
} else if (pathHash === '/system-settings') {
|
|
3166
|
+
if (!state.canAccessSystemSettings) {
|
|
3167
|
+
window.location.hash = '/';
|
|
3168
|
+
return;
|
|
3169
|
+
}
|
|
3170
|
+
showPage('system-settings');
|
|
2592
3171
|
await loadSettings();
|
|
2593
3172
|
}
|
|
2594
3173
|
}
|