dsh-taskboard 0.3.3 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -6
- package/lib/client.js +1201 -42
- package/lib/host/execution.js +6 -1
- package/lib/host/execution.js.map +1 -1
- package/lib/host/git.js +95 -2
- package/lib/host/git.js.map +1 -1
- package/lib/host/protocol-text.js +5 -3
- package/lib/host/protocol-text.js.map +1 -1
- package/lib/host/routes.js +184 -2
- package/lib/host/routes.js.map +1 -1
- package/lib/host/store.js +12 -0
- package/lib/host/store.js.map +1 -1
- package/lib/host/templates.js +166 -0
- package/lib/host/templates.js.map +1 -0
- package/lib/host/tools.js +202 -2
- package/lib/host/tools.js.map +1 -1
- package/lib/index.js +7 -2
- package/lib/index.js.map +1 -1
- package/lib/shared/api.js.map +1 -1
- package/lib/shared/protocol.js +277 -1
- package/lib/shared/protocol.js.map +1 -1
- package/package.json +1 -1
- package/src/client/api.ts +28 -0
- package/src/client/board/ImportModal.tsx +182 -0
- package/src/client/board/TaskBoard.tsx +45 -3
- package/src/client/board/TaskCard.tsx +9 -0
- package/src/client/board/TaskDetail.tsx +192 -8
- package/src/client/board/TaskFormModal.tsx +100 -18
- package/src/client/board/TemplateManager.tsx +121 -0
- package/src/client/board-mount.tsx +8 -0
- package/src/client/controller.ts +152 -8
- package/src/client/sidebar-entry.ts +6 -3
- package/src/client/styles.ts +153 -0
- package/src/host/execution.ts +10 -2
- package/src/host/git.ts +77 -0
- package/src/host/protocol-text.ts +5 -3
- package/src/host/routes.ts +215 -0
- package/src/host/store.ts +13 -0
- package/src/host/templates.ts +143 -0
- package/src/host/tools.ts +198 -2
- package/src/index.ts +6 -0
- package/src/shared/api.ts +54 -0
- package/src/shared/protocol.ts +344 -0
- package/src/shared/version.ts +1 -1
package/lib/client.js
CHANGED
|
@@ -46,6 +46,20 @@ window.__ModuleLoader__.load({
|
|
|
46
46
|
workspaceId,
|
|
47
47
|
taskId
|
|
48
48
|
}),
|
|
49
|
+
diff: (taskId, query) => {
|
|
50
|
+
const params = new URLSearchParams({ execution: query.execution });
|
|
51
|
+
if (query.commit !== void 0) params.set("commit", query.commit);
|
|
52
|
+
if (query.path !== void 0) params.set("path", query.path);
|
|
53
|
+
return unwrap(fetch(`/dsh-taskboard/tasks/${encodeURIComponent(taskId)}/diff?${params.toString()}`));
|
|
54
|
+
},
|
|
55
|
+
importPreview: (file) => post("/dsh-taskboard/import/preview", file),
|
|
56
|
+
importCommit: (mode, ledger) => post("/dsh-taskboard/import", {
|
|
57
|
+
mode,
|
|
58
|
+
ledger
|
|
59
|
+
}),
|
|
60
|
+
templates: () => unwrap(fetch("/dsh-taskboard/templates")),
|
|
61
|
+
templateUpsert: (body) => post("/dsh-taskboard/templates", body),
|
|
62
|
+
templateDelete: (id) => post("/dsh-taskboard/templates/delete", { id }),
|
|
49
63
|
stream(onChange, onGap) {
|
|
50
64
|
const es = new EventSource("/dsh-taskboard/events");
|
|
51
65
|
let revision;
|
|
@@ -211,6 +225,14 @@ window.__ModuleLoader__.load({
|
|
|
211
225
|
tasks: []
|
|
212
226
|
};
|
|
213
227
|
}
|
|
228
|
+
/** Checklist progress: how many items are checked (absent checklist → 0/0). */
|
|
229
|
+
function checklistProgress(task) {
|
|
230
|
+
const items = task.checklist ?? [];
|
|
231
|
+
return {
|
|
232
|
+
done: items.filter((i) => i.checked).length,
|
|
233
|
+
total: items.length
|
|
234
|
+
};
|
|
235
|
+
}
|
|
214
236
|
|
|
215
237
|
//#endregion
|
|
216
238
|
//#region src/client/controller.ts
|
|
@@ -269,7 +291,10 @@ window.__ModuleLoader__.load({
|
|
|
269
291
|
sortBy: view.sortBy,
|
|
270
292
|
composerOpen: false,
|
|
271
293
|
secondaryOpen: false,
|
|
272
|
-
diagOpen: false
|
|
294
|
+
diagOpen: false,
|
|
295
|
+
templates: [],
|
|
296
|
+
tplManagerOpen: false,
|
|
297
|
+
importOpen: false
|
|
273
298
|
};
|
|
274
299
|
}
|
|
275
300
|
/**
|
|
@@ -402,25 +427,36 @@ window.__ModuleLoader__.load({
|
|
|
402
427
|
select(id) {
|
|
403
428
|
this.setState({ selectedId: id });
|
|
404
429
|
}
|
|
405
|
-
/** Show/hide the task form (create mode when opening). */
|
|
430
|
+
/** Show/hide the task form (create mode when opening); always blank (no template prefill). */
|
|
406
431
|
setComposer(open) {
|
|
407
432
|
this.setState({
|
|
408
433
|
composerOpen: open,
|
|
409
|
-
editingId: void 0
|
|
434
|
+
editingId: void 0,
|
|
435
|
+
templatePrefill: void 0
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
/** Open the create form prefilled from a chosen template (0.4.0). */
|
|
439
|
+
newFromTemplate(spec) {
|
|
440
|
+
this.setState({
|
|
441
|
+
composerOpen: true,
|
|
442
|
+
editingId: void 0,
|
|
443
|
+
templatePrefill: spec
|
|
410
444
|
});
|
|
411
445
|
}
|
|
412
|
-
/** Open the form modal editing an existing task. */
|
|
446
|
+
/** Open the form modal editing an existing task (clears any template prefill). */
|
|
413
447
|
openEditor(id) {
|
|
414
448
|
this.setState({
|
|
415
449
|
composerOpen: true,
|
|
416
|
-
editingId: id
|
|
450
|
+
editingId: id,
|
|
451
|
+
templatePrefill: void 0
|
|
417
452
|
});
|
|
418
453
|
}
|
|
419
454
|
/** Close the form modal whatever its mode. */
|
|
420
455
|
closeForm() {
|
|
421
456
|
this.setState({
|
|
422
457
|
composerOpen: false,
|
|
423
|
-
editingId: void 0
|
|
458
|
+
editingId: void 0,
|
|
459
|
+
templatePrefill: void 0
|
|
424
460
|
});
|
|
425
461
|
}
|
|
426
462
|
/** Toggle the secondary tab. */
|
|
@@ -537,6 +573,42 @@ window.__ModuleLoader__.load({
|
|
|
537
573
|
this.setState({ error: error instanceof Error ? error.message : String(error) });
|
|
538
574
|
}
|
|
539
575
|
}
|
|
576
|
+
/**
|
|
577
|
+
* Toggle one checklist item as the USER (0.4.0): flips the item, records
|
|
578
|
+
* `checkedBy: 'user'`, keeps other items as they are (one update call).
|
|
579
|
+
*/
|
|
580
|
+
async toggleChecklistItem(task, itemId) {
|
|
581
|
+
const items = (task.checklist ?? []).map((item) => item.id === itemId ? item.checked ? {
|
|
582
|
+
id: item.id,
|
|
583
|
+
text: item.text,
|
|
584
|
+
checked: false
|
|
585
|
+
} : {
|
|
586
|
+
id: item.id,
|
|
587
|
+
text: item.text,
|
|
588
|
+
checked: true,
|
|
589
|
+
checkedBy: "user",
|
|
590
|
+
checkedAt: Date.now(),
|
|
591
|
+
...item.note !== void 0 ? { note: item.note } : {}
|
|
592
|
+
} : item);
|
|
593
|
+
try {
|
|
594
|
+
await this.client.update(task.id, {
|
|
595
|
+
ifVersion: task.version,
|
|
596
|
+
checklist: items
|
|
597
|
+
});
|
|
598
|
+
await this.refresh();
|
|
599
|
+
} catch (error) {
|
|
600
|
+
this.setState({ error: error instanceof Error ? error.message : String(error) });
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
/** Diff view (0.4.0): one execution's commit or changed path; errors surface via throw. */
|
|
604
|
+
async fetchDiff(taskId, query) {
|
|
605
|
+
try {
|
|
606
|
+
return await this.client.diff(taskId, query);
|
|
607
|
+
} catch (error) {
|
|
608
|
+
this.setState({ error: error instanceof Error ? error.message : String(error) });
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
540
612
|
/** Append a user comment. */
|
|
541
613
|
async comment(id, body) {
|
|
542
614
|
try {
|
|
@@ -634,7 +706,7 @@ window.__ModuleLoader__.load({
|
|
|
634
706
|
this.setState({ error: error instanceof Error ? error.message : String(error) });
|
|
635
707
|
}
|
|
636
708
|
}
|
|
637
|
-
/** Duplicate a task into a fresh todo card (same project/urgency/prompt/execution/model/isolation). */
|
|
709
|
+
/** Duplicate a task into a fresh todo card (same project/urgency/prompt/execution/model/isolation/checklist). */
|
|
638
710
|
async duplicate(task) {
|
|
639
711
|
try {
|
|
640
712
|
await this.client.create({
|
|
@@ -649,13 +721,109 @@ window.__ModuleLoader__.load({
|
|
|
649
721
|
} : { mode: "claim" },
|
|
650
722
|
model: task.model,
|
|
651
723
|
isolation: task.isolation,
|
|
652
|
-
...task.presetId !== void 0 ? { presetId: task.presetId } : {}
|
|
724
|
+
...task.presetId !== void 0 ? { presetId: task.presetId } : {},
|
|
725
|
+
...task.checklist !== void 0 && task.checklist.length > 0 ? { checklist: task.checklist.map((i) => i.text) } : {}
|
|
653
726
|
});
|
|
654
727
|
await this.refresh();
|
|
655
728
|
} catch (error) {
|
|
656
729
|
this.setState({ error: error instanceof Error ? error.message : String(error) });
|
|
657
730
|
}
|
|
658
731
|
}
|
|
732
|
+
/** Load the template list (best effort; errors surface). */
|
|
733
|
+
async loadTemplates() {
|
|
734
|
+
try {
|
|
735
|
+
const value = await this.client.templates();
|
|
736
|
+
this.setState({
|
|
737
|
+
templates: value.templates,
|
|
738
|
+
error: void 0
|
|
739
|
+
});
|
|
740
|
+
return value.templates;
|
|
741
|
+
} catch (error) {
|
|
742
|
+
this.setState({ error: error instanceof Error ? error.message : String(error) });
|
|
743
|
+
return [];
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
/** Open the + 新建任务 dropdown's template list fresh (called on menu open). */
|
|
747
|
+
prepareTemplateMenu() {
|
|
748
|
+
if (this.state.templates.length === 0) this.loadTemplates();
|
|
749
|
+
}
|
|
750
|
+
/** Open the template manager modal. */
|
|
751
|
+
openTemplateManager() {
|
|
752
|
+
this.setState({ tplManagerOpen: true });
|
|
753
|
+
this.loadTemplates();
|
|
754
|
+
}
|
|
755
|
+
/** Close the template manager modal. */
|
|
756
|
+
closeTemplateManager() {
|
|
757
|
+
this.setState({ tplManagerOpen: false });
|
|
758
|
+
}
|
|
759
|
+
/** Create or replace a template; refreshes the list. */
|
|
760
|
+
async upsertTemplate(body) {
|
|
761
|
+
try {
|
|
762
|
+
await this.client.templateUpsert(body);
|
|
763
|
+
await this.loadTemplates();
|
|
764
|
+
return true;
|
|
765
|
+
} catch (error) {
|
|
766
|
+
this.setState({ error: error instanceof Error ? error.message : String(error) });
|
|
767
|
+
return false;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
/** Delete a template by id; refreshes the list. */
|
|
771
|
+
async deleteTemplate(id) {
|
|
772
|
+
try {
|
|
773
|
+
await this.client.templateDelete(id);
|
|
774
|
+
await this.loadTemplates();
|
|
775
|
+
} catch (error) {
|
|
776
|
+
this.setState({ error: error instanceof Error ? error.message : String(error) });
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
/** 存为模板 from a task card: carries every configurable field incl. checklist texts. */
|
|
780
|
+
async saveAsTemplate(task) {
|
|
781
|
+
return this.upsertTemplate({
|
|
782
|
+
name: task.title.slice(0, 60),
|
|
783
|
+
task: {
|
|
784
|
+
title: task.title,
|
|
785
|
+
description: task.description.length > 0 ? task.description : void 0,
|
|
786
|
+
prompt: task.prompt.length > 0 ? task.prompt : void 0,
|
|
787
|
+
urgency: task.urgency,
|
|
788
|
+
execution: task.execution.mode === "scheduled" && task.execution.cron !== void 0 ? {
|
|
789
|
+
mode: "scheduled",
|
|
790
|
+
cron: task.execution.cron
|
|
791
|
+
} : { mode: "claim" },
|
|
792
|
+
model: task.model,
|
|
793
|
+
isolation: task.isolation,
|
|
794
|
+
...task.presetId !== void 0 ? { presetId: task.presetId } : {},
|
|
795
|
+
...task.checklist !== void 0 && task.checklist.length > 0 ? { checklist: task.checklist.map((i) => i.text) } : {}
|
|
796
|
+
}
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
/** Open the import modal. */
|
|
800
|
+
openImport() {
|
|
801
|
+
this.setState({ importOpen: true });
|
|
802
|
+
}
|
|
803
|
+
/** Close the import modal. */
|
|
804
|
+
closeImport() {
|
|
805
|
+
this.setState({ importOpen: false });
|
|
806
|
+
}
|
|
807
|
+
/** Dry-run an import file: classify its tasks against the live ledger. */
|
|
808
|
+
async importPreview(file) {
|
|
809
|
+
try {
|
|
810
|
+
return (await this.client.importPreview(file)).plan;
|
|
811
|
+
} catch (error) {
|
|
812
|
+
this.setState({ error: error instanceof Error ? error.message : String(error) });
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
/** Commit an import; refreshes the ledger afterwards. */
|
|
817
|
+
async importCommit(mode, ledger) {
|
|
818
|
+
try {
|
|
819
|
+
const value = await this.client.importCommit(mode, ledger);
|
|
820
|
+
await this.refresh();
|
|
821
|
+
return value;
|
|
822
|
+
} catch (error) {
|
|
823
|
+
this.setState({ error: error instanceof Error ? error.message : String(error) });
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
659
827
|
/** Download the whole ledger as a JSON backup file. */
|
|
660
828
|
exportJson() {
|
|
661
829
|
const stamp = /* @__PURE__ */ new Date();
|
|
@@ -1241,6 +1409,159 @@ window.__ModuleLoader__.load({
|
|
|
1241
1409
|
border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.25));
|
|
1242
1410
|
}
|
|
1243
1411
|
.dsh-atb-diag-orphan-path { font-size: 11.5px; font-family: ui-monospace, Consolas, monospace; word-break: break-all; }
|
|
1412
|
+
|
|
1413
|
+
/* ---------- 0.4.0 checklist ---------- */
|
|
1414
|
+
.dsh-atb-cke { display: flex; flex-direction: column; gap: 6px; }
|
|
1415
|
+
.dsh-atb-cke-row { display: flex; align-items: center; gap: 8px; }
|
|
1416
|
+
.dsh-atb-cke-box { flex-shrink: 0; width: 15px; height: 15px; cursor: pointer; }
|
|
1417
|
+
.dsh-atb-cke-text {
|
|
1418
|
+
flex: 1; min-width: 0; font-size: 12.5px; padding: 6px 9px;
|
|
1419
|
+
border-radius: 8px; border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.3));
|
|
1420
|
+
background: var(--dsw-alias-bg-layer-1, transparent); color: inherit;
|
|
1421
|
+
}
|
|
1422
|
+
.dsh-atb-cke-del {
|
|
1423
|
+
flex-shrink: 0; border: none; background: none; cursor: pointer; padding: 4px;
|
|
1424
|
+
color: var(--dsw-alias-label-tertiary, gray); font-size: 12px; border-radius: 6px;
|
|
1425
|
+
}
|
|
1426
|
+
.dsh-atb-cke-del:hover { color: var(--dsw-alias-state-error-primary, #e5484d); background: rgba(229,72,77,.08); }
|
|
1427
|
+
.dsh-atb-cke-add {
|
|
1428
|
+
align-self: flex-start; border: 1px dashed var(--dsw-alias-border-l2, rgba(128,128,128,.4));
|
|
1429
|
+
background: none; color: var(--dsw-alias-label-secondary, inherit); cursor: pointer;
|
|
1430
|
+
font-size: 11.5px; padding: 5px 12px; border-radius: 8px;
|
|
1431
|
+
}
|
|
1432
|
+
.dsh-atb-cke-add:hover { color: var(--dsw-alias-state-business-primary, #3e63dd); border-color: var(--dsw-alias-state-business-primary, #3e63dd); }
|
|
1433
|
+
.dsh-atb-cke-hint { font-size: 10.5px; color: var(--dsw-alias-label-tertiary, gray); }
|
|
1434
|
+
|
|
1435
|
+
.dsh-atb-cl-progress { margin-left: 8px; font-size: 11px; font-weight: 400; color: var(--dsw-alias-label-tertiary, gray); }
|
|
1436
|
+
.dsh-atb-cl-progress[data-tone="bad"] { color: var(--dsw-alias-state-error-primary, #e5484d); font-weight: 600; }
|
|
1437
|
+
.dsh-atb-cl-items { display: flex; flex-direction: column; gap: 5px; }
|
|
1438
|
+
.dsh-atb-cl-item {
|
|
1439
|
+
display: flex; align-items: baseline; gap: 9px; padding: 6px 9px; border-radius: 8px;
|
|
1440
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.22));
|
|
1441
|
+
background: var(--dsw-alias-bg-layer-1, rgba(128,128,128,.03)); cursor: pointer;
|
|
1442
|
+
}
|
|
1443
|
+
.dsh-atb-cl-item:hover { border-color: var(--dsw-alias-border-l2, rgba(128,128,128,.4)); }
|
|
1444
|
+
.dsh-atb-cl-item input { flex-shrink: 0; transform: translateY(1px); cursor: pointer; }
|
|
1445
|
+
.dsh-atb-cl-item[data-checked="true"] .dsh-atb-cl-text { text-decoration: line-through; color: var(--dsw-alias-label-tertiary, gray); }
|
|
1446
|
+
.dsh-atb-cl-item[data-alert="true"] {
|
|
1447
|
+
border-color: rgba(229,72,77,.45); background: rgba(229,72,77,.06);
|
|
1448
|
+
}
|
|
1449
|
+
.dsh-atb-cl-text { flex: 1; min-width: 0; font-size: 12.5px; word-break: break-word; }
|
|
1450
|
+
.dsh-atb-cl-meta { flex-shrink: 0; font-size: 10.5px; color: var(--dsw-alias-label-tertiary, gray); display: flex; flex-direction: column; gap: 2px; align-items: flex-end; }
|
|
1451
|
+
.dsh-atb-cl-note { max-width: 280px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--dsw-alias-label-secondary, inherit); }
|
|
1452
|
+
|
|
1453
|
+
/* ---------- 0.4.0 report ---------- */
|
|
1454
|
+
.dsh-atb-rpt-summary { font-size: 12.5px; line-height: 1.6; white-space: pre-wrap; word-break: break-word; margin-bottom: 8px; }
|
|
1455
|
+
.dsh-atb-rpt-sec { margin-bottom: 8px; }
|
|
1456
|
+
.dsh-atb-rpt-label { font-size: 11px; color: var(--dsw-alias-label-tertiary, gray); margin-bottom: 4px; }
|
|
1457
|
+
.dsh-atb-rpt-list { margin: 0; padding-left: 18px; font-size: 12px; line-height: 1.55; word-break: break-all; }
|
|
1458
|
+
.dsh-atb-rpt-risk {
|
|
1459
|
+
font-size: 12px; line-height: 1.55; white-space: pre-wrap; word-break: break-word;
|
|
1460
|
+
color: var(--dsw-alias-state-warn-primary, #f5a524);
|
|
1461
|
+
background: rgba(245,165,36,.08); border: 1px solid rgba(245,165,36,.3);
|
|
1462
|
+
border-radius: 8px; padding: 6px 10px;
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
/* ---------- 0.4.0 diff viewer ---------- */
|
|
1466
|
+
.dsh-atb-iso-commit { display: flex; flex-direction: column; gap: 3px; }
|
|
1467
|
+
.dsh-atb-iso-commit-btn {
|
|
1468
|
+
display: flex; gap: 8px; font-size: 11.5px; align-items: baseline; text-align: left;
|
|
1469
|
+
border: none; background: none; padding: 2px 4px; margin: 0 -4px; border-radius: 6px; cursor: pointer;
|
|
1470
|
+
color: inherit; width: fit-content; max-width: 100%;
|
|
1471
|
+
}
|
|
1472
|
+
.dsh-atb-iso-commit-btn:hover { background: var(--dsw-alias-bg-layer-2, rgba(128,128,128,.08)); }
|
|
1473
|
+
.dsh-atb-iso-commit-btn code {
|
|
1474
|
+
font-family: ui-monospace, Consolas, monospace; font-size: 10.5px;
|
|
1475
|
+
color: var(--dsw-alias-state-business-primary, #3e63dd); flex-shrink: 0;
|
|
1476
|
+
}
|
|
1477
|
+
.dsh-atb-iso-commit-btn span { word-break: break-all; color: var(--dsw-alias-label-secondary, inherit); }
|
|
1478
|
+
.dsh-atb-iso-commit[data-open="true"] > .dsh-atb-iso-commit-btn code { font-weight: 700; }
|
|
1479
|
+
.dsh-atb-iso-dirty { display: flex; flex-direction: column; gap: 6px;
|
|
1480
|
+
font-size: 11.5px; color: var(--dsw-alias-state-error-primary, #e5484d);
|
|
1481
|
+
background: rgba(229,72,77,.09); border: 1px solid rgba(229,72,77,.35);
|
|
1482
|
+
border-radius: 8px; padding: 6px 10px; margin-bottom: 8px;
|
|
1483
|
+
}
|
|
1484
|
+
.dsh-atb-iso-dirty-toggle { border: none; background: none; cursor: pointer; padding: 0; text-align: left; color: inherit; font-size: inherit; }
|
|
1485
|
+
.dsh-atb-iso-dirty-files { display: flex; flex-direction: column; gap: 2px; }
|
|
1486
|
+
.dsh-atb-iso-dirty-file {
|
|
1487
|
+
border: none; background: none; cursor: pointer; text-align: left; padding: 1px 2px;
|
|
1488
|
+
font-size: 11px; color: var(--dsw-alias-label-secondary, inherit); border-radius: 4px; word-break: break-all;
|
|
1489
|
+
}
|
|
1490
|
+
.dsh-atb-iso-dirty-file:hover { background: rgba(128,128,128,.1); color: var(--dsw-alias-state-business-primary, #3e63dd); }
|
|
1491
|
+
.dsh-atb-iso-dirty-file code { font-family: ui-monospace, Consolas, monospace; font-size: 10px; margin-right: 6px; }
|
|
1492
|
+
.dsh-atb-diffview { margin-top: 6px; border-radius: 8px; overflow: hidden;
|
|
1493
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.25)); }
|
|
1494
|
+
.dsh-atb-diffview-head { display: flex; align-items: center; gap: 10px; padding: 5px 10px;
|
|
1495
|
+
background: var(--dsw-alias-bg-layer-2, rgba(128,128,128,.08)); }
|
|
1496
|
+
.dsh-atb-diffview-title { font-family: ui-monospace, Consolas, monospace; font-size: 10.5px;
|
|
1497
|
+
color: var(--dsw-alias-state-business-primary, #3e63dd); word-break: break-all; }
|
|
1498
|
+
.dsh-atb-diffview-hint { font-size: 10.5px; color: var(--dsw-alias-label-tertiary, gray); }
|
|
1499
|
+
.dsh-atb-diffview-error { padding: 8px 10px; font-size: 11.5px; color: var(--dsw-alias-state-error-primary, #e5484d); }
|
|
1500
|
+
.dsh-atb-diffview-pre {
|
|
1501
|
+
margin: 0; padding: 8px 10px; max-height: 340px; overflow: auto;
|
|
1502
|
+
font-family: ui-monospace, Consolas, monospace; font-size: 10.5px; line-height: 1.5;
|
|
1503
|
+
white-space: pre; color: var(--dsw-alias-label-secondary, inherit);
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
/* ---------- 0.4.0 new-task menu + template manager + import ---------- */
|
|
1507
|
+
.dsh-atb-newmenu { position: relative; display: inline-flex; }
|
|
1508
|
+
.dsh-atb-newmenu-backdrop { position: fixed; inset: 0; z-index: 40; }
|
|
1509
|
+
.dsh-atb-newmenu-list {
|
|
1510
|
+
position: absolute; top: calc(100% + 4px); left: 0; z-index: 41; min-width: 180px;
|
|
1511
|
+
display: flex; flex-direction: column; padding: 5px; border-radius: 10px;
|
|
1512
|
+
background: var(--dsw-alias-bg-overlay, #fff); color: var(--dsw-alias-label-primary, inherit);
|
|
1513
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.25));
|
|
1514
|
+
box-shadow: var(--dsw-shadow-lv3, 0 12px 32px rgba(0,0,0,.18));
|
|
1515
|
+
}
|
|
1516
|
+
.dsh-atb-newmenu-opt {
|
|
1517
|
+
border: none; background: none; text-align: left; cursor: pointer; padding: 7px 10px;
|
|
1518
|
+
font-size: 12.5px; color: inherit; border-radius: 7px; white-space: nowrap;
|
|
1519
|
+
}
|
|
1520
|
+
.dsh-atb-newmenu-opt:hover { background: var(--dsw-alias-bg-layer-2, rgba(128,128,128,.1)); }
|
|
1521
|
+
.dsh-atb-newmenu-sep { height: 1px; margin: 4px 6px; background: var(--dsw-alias-border-l2, rgba(128,128,128,.25)); }
|
|
1522
|
+
|
|
1523
|
+
.dsh-atb-tplm { max-width: 560px; width: min(560px, 92vw); }
|
|
1524
|
+
.dsh-atb-tplm-list { display: flex; flex-direction: column; gap: 8px; }
|
|
1525
|
+
.dsh-atb-tplm-row {
|
|
1526
|
+
display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-radius: 8px;
|
|
1527
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.25));
|
|
1528
|
+
}
|
|
1529
|
+
.dsh-atb-tplm-name {
|
|
1530
|
+
flex: 0 0 160px; font-size: 12.5px; padding: 5px 8px; border-radius: 7px;
|
|
1531
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.3));
|
|
1532
|
+
background: var(--dsw-alias-bg-layer-1, transparent); color: inherit;
|
|
1533
|
+
}
|
|
1534
|
+
.dsh-atb-tplm-meta { flex: 1; min-width: 0; font-size: 10.5px; color: var(--dsw-alias-label-tertiary, gray); }
|
|
1535
|
+
.dsh-atb-tplm-btns { display: flex; gap: 6px; flex-shrink: 0; }
|
|
1536
|
+
|
|
1537
|
+
.dsh-atb-imp { max-width: 600px; width: min(600px, 92vw); }
|
|
1538
|
+
.dsh-atb-imp-picker { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
|
|
1539
|
+
.dsh-atb-imp-picker input[type="file"] { font-size: 12px; }
|
|
1540
|
+
.dsh-atb-imp-filename { font-size: 11.5px; color: var(--dsw-alias-state-business-primary, #3e63dd); word-break: break-all; }
|
|
1541
|
+
.dsh-atb-imp-note { font-size: 11px; color: var(--dsw-alias-label-tertiary, gray); margin-bottom: 10px; }
|
|
1542
|
+
.dsh-atb-imp-error { font-size: 12px; color: var(--dsw-alias-state-error-primary, #e5484d); margin-bottom: 8px; }
|
|
1543
|
+
.dsh-atb-imp-stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-bottom: 12px; }
|
|
1544
|
+
.dsh-atb-imp-stat {
|
|
1545
|
+
display: flex; flex-direction: column; align-items: center; gap: 2px; padding: 9px 6px; border-radius: 9px;
|
|
1546
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.25));
|
|
1547
|
+
}
|
|
1548
|
+
.dsh-atb-imp-stat b { font-size: 17px; font-weight: 700; }
|
|
1549
|
+
.dsh-atb-imp-stat span { font-size: 10.5px; color: var(--dsw-alias-label-tertiary, gray); }
|
|
1550
|
+
.dsh-atb-imp-stat[data-tone="ok"] b { color: var(--dsw-alias-state-success-primary, #30a46c); }
|
|
1551
|
+
.dsh-atb-imp-stat[data-tone="warn"] b { color: var(--dsw-alias-state-warn-primary, #f5a524); }
|
|
1552
|
+
.dsh-atb-imp-stat[data-tone="bad"] b { color: var(--dsw-alias-state-error-primary, #e5484d); }
|
|
1553
|
+
.dsh-atb-imp-sec h4 { margin: 0 0 6px; font-size: 12px; }
|
|
1554
|
+
.dsh-atb-imp-sec { margin-bottom: 10px; }
|
|
1555
|
+
.dsh-atb-imp-list { display: flex; flex-direction: column; gap: 4px; max-height: 160px; overflow-y: auto; }
|
|
1556
|
+
.dsh-atb-imp-row {
|
|
1557
|
+
display: flex; align-items: center; gap: 10px; justify-content: space-between;
|
|
1558
|
+
padding: 5px 9px; border-radius: 7px; border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.2));
|
|
1559
|
+
}
|
|
1560
|
+
.dsh-atb-imp-row[data-tone="bad"] { border-color: rgba(229,72,77,.35); }
|
|
1561
|
+
.dsh-atb-imp-row-title { font-size: 12px; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1562
|
+
.dsh-atb-imp-row-status { font-size: 10.5px; color: var(--dsw-alias-label-tertiary, gray); flex-shrink: 0; }
|
|
1563
|
+
.dsh-atb-imp-result { font-size: 12px; color: var(--dsw-alias-state-success-primary, #30a46c); margin-top: 10px; }
|
|
1564
|
+
.dsh-atb-badge[data-kind="checklist"] { color: var(--dsw-alias-label-secondary, inherit); }
|
|
1244
1565
|
`;
|
|
1245
1566
|
let injected = false;
|
|
1246
1567
|
/** Inject the stylesheet once (idempotent). */
|
|
@@ -1255,6 +1576,8 @@ window.__ModuleLoader__.load({
|
|
|
1255
1576
|
|
|
1256
1577
|
//#endregion
|
|
1257
1578
|
//#region src/client/sidebar-entry.ts
|
|
1579
|
+
/** Stable data attribute identifying this entry row. */
|
|
1580
|
+
const ENTRY_SELECTOR = "[data-dsh-atb-entry]";
|
|
1258
1581
|
/** Inline icon (16px nav-icon look). */
|
|
1259
1582
|
const ICON = "<svg viewBox=\"0 0 16 16\" width=\"14\" height=\"14\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><rect x=\"2\" y=\"2.5\" width=\"12\" height=\"11\" rx=\"1.5\"/><path d=\"M2 6.5h12M6.5 6.5v7\"/></svg>";
|
|
1260
1583
|
/**
|
|
@@ -1269,15 +1592,18 @@ window.__ModuleLoader__.load({
|
|
|
1269
1592
|
/**
|
|
1270
1593
|
* The New Session button: nested in the logo row on current shells, a direct
|
|
1271
1594
|
* child BUTTON on the real shell (the family plugins' fallback), with
|
|
1272
|
-
* aria-label/text fallbacks for other shells.
|
|
1595
|
+
* aria-label/text fallbacks for other shells. The direct-child scan skips
|
|
1596
|
+
* our own entry (0.4.1): on shells where the insertion anchor lands inside a
|
|
1597
|
+
* class-carrying container, a self-referential anchor would pin the entry
|
|
1598
|
+
* against that container's own geometry instead of the family block.
|
|
1273
1599
|
*/
|
|
1274
1600
|
function newSessionButton(root) {
|
|
1275
1601
|
const nested = root.querySelector("button[class*=\"newSession\"]");
|
|
1276
1602
|
if (nested !== null) return nested;
|
|
1277
|
-
for (const child of root.children) if (child instanceof HTMLButtonElement) return child;
|
|
1603
|
+
for (const child of root.children) if (child instanceof HTMLButtonElement && !child.matches("[data-dsh-atb-entry]")) return child;
|
|
1278
1604
|
const byAria = root.querySelector("button[aria-label=\"新建会话\"], button[aria-label=\"New Session\"], button[aria-label*=\"新会话\"], button[aria-label*=\"new session\" i]");
|
|
1279
1605
|
if (byAria !== null) return byAria;
|
|
1280
|
-
return Array.from(root.querySelectorAll("button")).find((button) => /新会话|新建会话|new session/i.test(button.textContent ?? ""));
|
|
1606
|
+
return Array.from(root.querySelectorAll("button")).find((button) => !button.matches("[data-dsh-atb-entry]") && /新会话|新建会话|new session/i.test(button.textContent ?? ""));
|
|
1281
1607
|
}
|
|
1282
1608
|
/** Build the entry row (a detached button; insert once the shell is up). */
|
|
1283
1609
|
function createEntry(controller) {
|
|
@@ -1485,7 +1811,7 @@ window.__ModuleLoader__.load({
|
|
|
1485
1811
|
* @module dsh-taskboard/shared/version
|
|
1486
1812
|
*/
|
|
1487
1813
|
/** The package version (must equal package.json "version"). */
|
|
1488
|
-
const PLUGIN_VERSION = "0.
|
|
1814
|
+
const PLUGIN_VERSION = "0.4.1";
|
|
1489
1815
|
|
|
1490
1816
|
//#endregion
|
|
1491
1817
|
//#region src/client/board/TaskCard.tsx
|
|
@@ -1599,6 +1925,17 @@ window.__ModuleLoader__.load({
|
|
|
1599
1925
|
className: "dsh-atb-badge",
|
|
1600
1926
|
children: task.model.model
|
|
1601
1927
|
}),
|
|
1928
|
+
task.checklist !== void 0 && task.checklist.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1929
|
+
className: "dsh-atb-badge",
|
|
1930
|
+
"data-kind": task.status === "in_review" && task.checklist.some((i) => !i.checked) ? "blocked" : "checklist",
|
|
1931
|
+
title: task.status === "in_review" && task.checklist.some((i) => !i.checked) ? "待验收:清单未全部勾选" : "验收清单进度",
|
|
1932
|
+
children: [
|
|
1933
|
+
"☑ ",
|
|
1934
|
+
task.checklist.filter((i) => i.checked).length,
|
|
1935
|
+
"/",
|
|
1936
|
+
task.checklist.length
|
|
1937
|
+
]
|
|
1938
|
+
}),
|
|
1602
1939
|
task.status === "done" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1603
1940
|
className: "dsh-atb-badge",
|
|
1604
1941
|
"data-kind": "done",
|
|
@@ -1813,6 +2150,179 @@ window.__ModuleLoader__.load({
|
|
|
1813
2150
|
function shortHash(hash) {
|
|
1814
2151
|
return hash === void 0 ? "" : hash.slice(0, 8);
|
|
1815
2152
|
}
|
|
2153
|
+
/** Extract the path from one `git status --porcelain` line (rename-aware). */
|
|
2154
|
+
function porcelainPath(line) {
|
|
2155
|
+
let p = line.slice(3);
|
|
2156
|
+
const arrow = p.indexOf(" -> ");
|
|
2157
|
+
if (arrow >= 0) p = p.slice(arrow + 4);
|
|
2158
|
+
if (p.startsWith("\"") && p.endsWith("\"") && p.length > 1) p = p.slice(1, -1);
|
|
2159
|
+
return p;
|
|
2160
|
+
}
|
|
2161
|
+
/**
|
|
2162
|
+
* Lazy diff viewer (0.4.0): loads on mount, renders inside a capped <pre>.
|
|
2163
|
+
* @param spec - what to show: one commit hash, or one changed path.
|
|
2164
|
+
*/
|
|
2165
|
+
function DiffView({ controller, task, execution, commit, path }) {
|
|
2166
|
+
const [state, setState] = (0, react.useState)({ loading: true });
|
|
2167
|
+
(0, react.useEffect)(() => {
|
|
2168
|
+
let alive = true;
|
|
2169
|
+
setState({ loading: true });
|
|
2170
|
+
controller.fetchDiff(task.id, {
|
|
2171
|
+
execution: execution.id,
|
|
2172
|
+
...commit !== void 0 ? { commit } : { path: path ?? "" }
|
|
2173
|
+
}).then((result) => {
|
|
2174
|
+
if (!alive) return;
|
|
2175
|
+
if (result === void 0) setState({
|
|
2176
|
+
loading: false,
|
|
2177
|
+
failed: true
|
|
2178
|
+
});
|
|
2179
|
+
else setState({
|
|
2180
|
+
loading: false,
|
|
2181
|
+
diff: result.diff,
|
|
2182
|
+
truncated: result.truncated
|
|
2183
|
+
});
|
|
2184
|
+
});
|
|
2185
|
+
return () => {
|
|
2186
|
+
alive = false;
|
|
2187
|
+
};
|
|
2188
|
+
}, [
|
|
2189
|
+
controller,
|
|
2190
|
+
task.id,
|
|
2191
|
+
execution.id,
|
|
2192
|
+
commit,
|
|
2193
|
+
path
|
|
2194
|
+
]);
|
|
2195
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2196
|
+
className: "dsh-atb-diffview",
|
|
2197
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2198
|
+
className: "dsh-atb-diffview-head",
|
|
2199
|
+
children: [
|
|
2200
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2201
|
+
className: "dsh-atb-diffview-title",
|
|
2202
|
+
children: commit !== void 0 ? `提交 ${shortHash(commit)}` : `文件 ${path}`
|
|
2203
|
+
}),
|
|
2204
|
+
state.loading && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2205
|
+
className: "dsh-atb-diffview-hint",
|
|
2206
|
+
children: "读取中…"
|
|
2207
|
+
}),
|
|
2208
|
+
state.truncated === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2209
|
+
className: "dsh-atb-diffview-hint",
|
|
2210
|
+
children: "⚠ 内容过长已截断"
|
|
2211
|
+
})
|
|
2212
|
+
]
|
|
2213
|
+
}), state.failed === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2214
|
+
className: "dsh-atb-diffview-error",
|
|
2215
|
+
children: "获取失败(原因见看板顶部错误条;对象可能已随 worktree 删除丢失)"
|
|
2216
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
|
|
2217
|
+
className: "dsh-atb-diffview-pre",
|
|
2218
|
+
children: state.diff ?? ""
|
|
2219
|
+
})]
|
|
2220
|
+
});
|
|
2221
|
+
}
|
|
2222
|
+
/**
|
|
2223
|
+
* The DoD checklist block (0.4.0): user-togglable items, checker + evidence
|
|
2224
|
+
* per row; unchecked items highlight while the task sits in in_review.
|
|
2225
|
+
*/
|
|
2226
|
+
function ChecklistBlock({ task, controller }) {
|
|
2227
|
+
const items = task.checklist ?? [];
|
|
2228
|
+
if (items.length === 0) return null;
|
|
2229
|
+
const { done, total } = checklistProgress(task);
|
|
2230
|
+
const unchecked = total - done;
|
|
2231
|
+
const reviewing = task.status === "in_review";
|
|
2232
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2233
|
+
className: "dsh-atb-fieldcard",
|
|
2234
|
+
"data-kind": "checklist",
|
|
2235
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2236
|
+
className: "dsh-atb-fieldcard-label",
|
|
2237
|
+
children: ["验收清单(DoD)", /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2238
|
+
className: "dsh-atb-cl-progress",
|
|
2239
|
+
"data-tone": reviewing && unchecked > 0 ? "bad" : void 0,
|
|
2240
|
+
children: [
|
|
2241
|
+
"☑ ",
|
|
2242
|
+
done,
|
|
2243
|
+
"/",
|
|
2244
|
+
total,
|
|
2245
|
+
reviewing && unchecked > 0 ? ` · ${unchecked} 项未完成` : done === total ? " · 全部完成" : ""
|
|
2246
|
+
]
|
|
2247
|
+
})]
|
|
2248
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2249
|
+
className: "dsh-atb-cl-items",
|
|
2250
|
+
children: items.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2251
|
+
className: "dsh-atb-cl-item",
|
|
2252
|
+
"data-checked": item.checked ? "true" : void 0,
|
|
2253
|
+
"data-alert": reviewing && !item.checked ? "true" : void 0,
|
|
2254
|
+
children: [
|
|
2255
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2256
|
+
type: "checkbox",
|
|
2257
|
+
checked: item.checked,
|
|
2258
|
+
onChange: () => void controller.toggleChecklistItem(task, item.id)
|
|
2259
|
+
}),
|
|
2260
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2261
|
+
className: "dsh-atb-cl-text",
|
|
2262
|
+
children: item.text
|
|
2263
|
+
}),
|
|
2264
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2265
|
+
className: "dsh-atb-cl-meta",
|
|
2266
|
+
children: [item.checked ? `${item.checkedBy === "user" ? "👤 用户" : `🤖 ${shortId(item.checkedBy)}`} · ${fmtTime(item.checkedAt)}` : "未完成", item.note !== void 0 && item.note.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2267
|
+
className: "dsh-atb-cl-note",
|
|
2268
|
+
title: item.note,
|
|
2269
|
+
children: ["证据:", item.note]
|
|
2270
|
+
})]
|
|
2271
|
+
})
|
|
2272
|
+
]
|
|
2273
|
+
}, item.id))
|
|
2274
|
+
})]
|
|
2275
|
+
});
|
|
2276
|
+
}
|
|
2277
|
+
/**
|
|
2278
|
+
* The structured execution report block (0.4.0): the newest execution that
|
|
2279
|
+
* carries one, rendered section by section for the reviewer.
|
|
2280
|
+
*/
|
|
2281
|
+
function ReportBlock({ task }) {
|
|
2282
|
+
const execution = [...task.executions].reverse().find((e) => e.report !== void 0);
|
|
2283
|
+
const report = execution?.report;
|
|
2284
|
+
if (execution === void 0 || report === void 0) return null;
|
|
2285
|
+
const section = (label, rows) => rows !== void 0 && rows.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2286
|
+
className: "dsh-atb-rpt-sec",
|
|
2287
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2288
|
+
className: "dsh-atb-rpt-label",
|
|
2289
|
+
children: label
|
|
2290
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
2291
|
+
className: "dsh-atb-rpt-list",
|
|
2292
|
+
children: rows.map((row, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: row }, i))
|
|
2293
|
+
})]
|
|
2294
|
+
}) : null;
|
|
2295
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2296
|
+
className: "dsh-atb-fieldcard",
|
|
2297
|
+
"data-kind": "report",
|
|
2298
|
+
children: [
|
|
2299
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2300
|
+
className: "dsh-atb-fieldcard-label",
|
|
2301
|
+
children: ["执行报告", /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2302
|
+
className: "dsh-atb-cl-progress",
|
|
2303
|
+
children: ["由执行会话提交 · ", fmtTime(execution.endedAt ?? execution.startedAt)]
|
|
2304
|
+
})]
|
|
2305
|
+
}),
|
|
2306
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2307
|
+
className: "dsh-atb-rpt-summary",
|
|
2308
|
+
children: report.summary
|
|
2309
|
+
}),
|
|
2310
|
+
section("改动文件", report.changedFiles),
|
|
2311
|
+
section("自验情况", report.checks),
|
|
2312
|
+
section("产物", report.artifacts),
|
|
2313
|
+
report.risk.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2314
|
+
className: "dsh-atb-rpt-sec",
|
|
2315
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2316
|
+
className: "dsh-atb-rpt-label",
|
|
2317
|
+
children: "剩余风险"
|
|
2318
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2319
|
+
className: "dsh-atb-rpt-risk",
|
|
2320
|
+
children: report.risk
|
|
2321
|
+
})]
|
|
2322
|
+
})
|
|
2323
|
+
]
|
|
2324
|
+
});
|
|
2325
|
+
}
|
|
1816
2326
|
/**
|
|
1817
2327
|
* The 0.3.0 isolation block: branch / baseline→head commits / change stats /
|
|
1818
2328
|
* uncommitted-changes warning, plus the user-only git actions (merge /
|
|
@@ -1823,6 +2333,8 @@ window.__ModuleLoader__.load({
|
|
|
1823
2333
|
const [confirmMerge, setConfirmMerge] = (0, react.useState)(false);
|
|
1824
2334
|
const [confirmRemove, setConfirmRemove] = (0, react.useState)(null);
|
|
1825
2335
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
2336
|
+
const [openDiff, setOpenDiff] = (0, react.useState)(null);
|
|
2337
|
+
const [dirtyOpen, setDirtyOpen] = (0, react.useState)(false);
|
|
1826
2338
|
const execution = latestIsolated(task);
|
|
1827
2339
|
const running = task.executions.some((e) => e.outcome === "running");
|
|
1828
2340
|
if (execution === void 0) return null;
|
|
@@ -1907,7 +2419,19 @@ window.__ModuleLoader__.load({
|
|
|
1907
2419
|
className: "dsh-atb-iso-commits",
|
|
1908
2420
|
children: [commits.slice(0, 10).map((c) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1909
2421
|
className: "dsh-atb-iso-commit",
|
|
1910
|
-
|
|
2422
|
+
"data-open": openDiff?.commit === c.hash ? "true" : void 0,
|
|
2423
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2424
|
+
type: "button",
|
|
2425
|
+
className: "dsh-atb-iso-commit-btn",
|
|
2426
|
+
title: "点击展开该提交的 diff",
|
|
2427
|
+
onClick: () => setOpenDiff(openDiff?.commit === c.hash ? null : { commit: c.hash }),
|
|
2428
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: shortHash(c.hash) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: c.subject })]
|
|
2429
|
+
}), openDiff?.commit === c.hash && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DiffView, {
|
|
2430
|
+
controller,
|
|
2431
|
+
task,
|
|
2432
|
+
execution,
|
|
2433
|
+
commit: c.hash
|
|
2434
|
+
})]
|
|
1911
2435
|
}, c.hash)), commitTotal > 10 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1912
2436
|
className: "dsh-atb-iso-more",
|
|
1913
2437
|
children: [
|
|
@@ -1922,11 +2446,48 @@ window.__ModuleLoader__.load({
|
|
|
1922
2446
|
}),
|
|
1923
2447
|
dirtyTotal > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1924
2448
|
className: "dsh-atb-iso-dirty",
|
|
1925
|
-
title: dirty.join("\n"),
|
|
1926
2449
|
children: [
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
2450
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2451
|
+
type: "button",
|
|
2452
|
+
className: "dsh-atb-iso-dirty-toggle",
|
|
2453
|
+
onClick: () => setDirtyOpen(!dirtyOpen),
|
|
2454
|
+
children: [
|
|
2455
|
+
"⚠ 有 ",
|
|
2456
|
+
dirtyTotal,
|
|
2457
|
+
" 处未提交修改(合并前请让 agent 提交,或手动处理)",
|
|
2458
|
+
dirtyOpen ? " ▲" : " ▼ 查看文件"
|
|
2459
|
+
]
|
|
2460
|
+
}),
|
|
2461
|
+
dirtyOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2462
|
+
className: "dsh-atb-iso-dirty-files",
|
|
2463
|
+
children: [dirty.slice(0, 30).map((line, index) => {
|
|
2464
|
+
const filePath = porcelainPath(line);
|
|
2465
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2466
|
+
type: "button",
|
|
2467
|
+
className: "dsh-atb-iso-dirty-file",
|
|
2468
|
+
title: "点击查看该文件的未提交 diff",
|
|
2469
|
+
onClick: () => setOpenDiff(openDiff?.path === filePath ? null : { path: filePath }),
|
|
2470
|
+
children: [
|
|
2471
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: line.slice(0, 2) }),
|
|
2472
|
+
" ",
|
|
2473
|
+
filePath
|
|
2474
|
+
]
|
|
2475
|
+
}, `${line}-${index}`);
|
|
2476
|
+
}), dirtyTotal > 30 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2477
|
+
className: "dsh-atb-iso-more",
|
|
2478
|
+
children: [
|
|
2479
|
+
"… 共 ",
|
|
2480
|
+
dirtyTotal,
|
|
2481
|
+
" 处(完整列表见任务台账)"
|
|
2482
|
+
]
|
|
2483
|
+
})]
|
|
2484
|
+
}),
|
|
2485
|
+
openDiff?.path !== void 0 && dirtyOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DiffView, {
|
|
2486
|
+
controller,
|
|
2487
|
+
task,
|
|
2488
|
+
execution,
|
|
2489
|
+
path: openDiff.path
|
|
2490
|
+
})
|
|
1930
2491
|
]
|
|
1931
2492
|
}),
|
|
1932
2493
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -2031,6 +2592,7 @@ window.__ModuleLoader__.load({
|
|
|
2031
2592
|
const runningExecution = task.executions.find((e) => e.outcome === "running");
|
|
2032
2593
|
const holder = task.status === "in_progress" ? task.claimedBy : void 0;
|
|
2033
2594
|
const stale = now !== void 0 && isStaleClaim(task, now);
|
|
2595
|
+
const unchecked = (task.checklist ?? []).filter((i) => !i.checked).length;
|
|
2034
2596
|
/** Jump to an execution's session; prompt precisely when it cannot open. */
|
|
2035
2597
|
const jumpToSession = (sessionId) => {
|
|
2036
2598
|
controller.openSession(sessionId).then((result) => {
|
|
@@ -2088,6 +2650,16 @@ window.__ModuleLoader__.load({
|
|
|
2088
2650
|
tone: "urgent",
|
|
2089
2651
|
children: "受阻"
|
|
2090
2652
|
}),
|
|
2653
|
+
task.checklist !== void 0 && task.checklist.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Chip, {
|
|
2654
|
+
icon: "☑",
|
|
2655
|
+
tone: task.status === "in_review" && task.checklist.some((i) => !i.checked) ? "urgent" : void 0,
|
|
2656
|
+
children: [
|
|
2657
|
+
"清单 ",
|
|
2658
|
+
checklistProgress(task).done,
|
|
2659
|
+
"/",
|
|
2660
|
+
task.checklist.length
|
|
2661
|
+
]
|
|
2662
|
+
}),
|
|
2091
2663
|
task.branch !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Chip, {
|
|
2092
2664
|
icon: "🌿",
|
|
2093
2665
|
tone: void 0,
|
|
@@ -2140,6 +2712,17 @@ window.__ModuleLoader__.load({
|
|
|
2140
2712
|
onClick: () => void controller.duplicate(task),
|
|
2141
2713
|
children: "⧉ 复制"
|
|
2142
2714
|
}),
|
|
2715
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2716
|
+
type: "button",
|
|
2717
|
+
className: "dsh-atb-detail-edit",
|
|
2718
|
+
title: "把此任务的配置(含清单)保存为模板,新建任务时可用",
|
|
2719
|
+
onClick: () => {
|
|
2720
|
+
controller.saveAsTemplate(task).then((ok) => {
|
|
2721
|
+
if (ok) showAlert("已存为模板(新建任务 ▼ 下拉可用,可在模板管理中改名)");
|
|
2722
|
+
});
|
|
2723
|
+
},
|
|
2724
|
+
children: "⌗ 存为模板"
|
|
2725
|
+
}),
|
|
2143
2726
|
canRun && task.branch !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2144
2727
|
type: "button",
|
|
2145
2728
|
className: "dsh-atb-detail-run",
|
|
@@ -2221,6 +2804,11 @@ window.__ModuleLoader__.load({
|
|
|
2221
2804
|
task,
|
|
2222
2805
|
controller
|
|
2223
2806
|
}),
|
|
2807
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ReportBlock, { task }),
|
|
2808
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChecklistBlock, {
|
|
2809
|
+
task,
|
|
2810
|
+
controller
|
|
2811
|
+
}),
|
|
2224
2812
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2225
2813
|
className: "dsh-atb-detail-actions",
|
|
2226
2814
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -2231,7 +2819,8 @@ window.__ModuleLoader__.load({
|
|
|
2231
2819
|
children: [
|
|
2232
2820
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2233
2821
|
className: "dsh-atb-confirm-label",
|
|
2234
|
-
|
|
2822
|
+
"data-tone": unchecked > 0 ? "bad" : void 0,
|
|
2823
|
+
children: unchecked > 0 ? `仍有 ${unchecked} 项清单未勾选,确认完成?` : "确认完成?"
|
|
2235
2824
|
}),
|
|
2236
2825
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2237
2826
|
type: "button",
|
|
@@ -2505,28 +3094,94 @@ window.__ModuleLoader__.load({
|
|
|
2505
3094
|
});
|
|
2506
3095
|
}
|
|
2507
3096
|
/**
|
|
2508
|
-
* The
|
|
2509
|
-
*
|
|
2510
|
-
*
|
|
3097
|
+
* The checklist (DoD) editor: toggle + text + remove per row, add button,
|
|
3098
|
+
* cap-enforced. Edit mode preserves checked state and notes (the GUI
|
|
3099
|
+
* replaces the whole list on save).
|
|
3100
|
+
*/
|
|
3101
|
+
function ChecklistEditor({ rows, onChange, editing }) {
|
|
3102
|
+
const setRow = (index, patch) => {
|
|
3103
|
+
onChange(rows.map((row, i) => i === index ? {
|
|
3104
|
+
...row,
|
|
3105
|
+
...patch
|
|
3106
|
+
} : row));
|
|
3107
|
+
};
|
|
3108
|
+
const checked = rows.filter((r) => r.checked).length;
|
|
3109
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3110
|
+
className: "dsh-atb-cke",
|
|
3111
|
+
children: [
|
|
3112
|
+
rows.map((row, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3113
|
+
className: "dsh-atb-cke-row",
|
|
3114
|
+
children: [
|
|
3115
|
+
editing && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
3116
|
+
type: "checkbox",
|
|
3117
|
+
className: "dsh-atb-cke-box",
|
|
3118
|
+
checked: row.checked,
|
|
3119
|
+
title: `勾选状态随保存保留(当前勾选人:${row.checkedBy ?? "未勾选"})`,
|
|
3120
|
+
onChange: (e) => setRow(index, { checked: e.target.checked })
|
|
3121
|
+
}),
|
|
3122
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
3123
|
+
className: "dsh-atb-cke-text",
|
|
3124
|
+
value: row.text,
|
|
3125
|
+
maxLength: 200,
|
|
3126
|
+
placeholder: `验收项 ${index + 1}(完成标准)`,
|
|
3127
|
+
spellCheck: false,
|
|
3128
|
+
onChange: (e) => setRow(index, { text: e.target.value })
|
|
3129
|
+
}),
|
|
3130
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3131
|
+
type: "button",
|
|
3132
|
+
className: "dsh-atb-cke-del",
|
|
3133
|
+
title: "删除该验收项",
|
|
3134
|
+
onClick: () => onChange(rows.filter((_, i) => i !== index)),
|
|
3135
|
+
children: "✕"
|
|
3136
|
+
})
|
|
3137
|
+
]
|
|
3138
|
+
}, row.id ?? `new-${index}`)),
|
|
3139
|
+
rows.length < 30 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3140
|
+
type: "button",
|
|
3141
|
+
className: "dsh-atb-cke-add",
|
|
3142
|
+
onClick: () => onChange([...rows, {
|
|
3143
|
+
text: "",
|
|
3144
|
+
checked: false
|
|
3145
|
+
}]),
|
|
3146
|
+
children: "+ 添加验收项"
|
|
3147
|
+
}),
|
|
3148
|
+
rows.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3149
|
+
className: "dsh-atb-cke-hint",
|
|
3150
|
+
children: editing ? `已勾选 ${checked}/${rows.length}(保存将整体覆盖清单,勾选状态保留)` : `共 ${rows.length} 项,执行会话按清单干活并逐项勾选,未完成项验收时高亮`
|
|
3151
|
+
})
|
|
3152
|
+
]
|
|
3153
|
+
});
|
|
3154
|
+
}
|
|
3155
|
+
/**
|
|
3156
|
+
* The form modal. Without `task` it composes a new task (optionally
|
|
3157
|
+
* prefilled from a chosen template); with `task` it edits that record
|
|
3158
|
+
* (project, urgency, execution, model included — the GUI is the owner
|
|
3159
|
+
* surface).
|
|
2511
3160
|
* @param controller - the controller.
|
|
2512
3161
|
* @param task - the task being edited (create mode when absent).
|
|
2513
3162
|
*/
|
|
2514
3163
|
function TaskFormModal({ controller, task }) {
|
|
2515
3164
|
const state = controller.getSnapshot();
|
|
3165
|
+
const prefill = state.templatePrefill;
|
|
2516
3166
|
const editing = task !== void 0;
|
|
2517
|
-
const [title, setTitle] = (0, react.useState)(task?.title ?? "");
|
|
2518
|
-
const [description, setDescription] = (0, react.useState)(task?.description ?? "");
|
|
2519
|
-
const [prompt, setPrompt] = (0, react.useState)(task?.prompt ?? "");
|
|
3167
|
+
const [title, setTitle] = (0, react.useState)(task?.title ?? prefill?.title ?? "");
|
|
3168
|
+
const [description, setDescription] = (0, react.useState)(task?.description ?? prefill?.description ?? "");
|
|
3169
|
+
const [prompt, setPrompt] = (0, react.useState)(task?.prompt ?? prefill?.prompt ?? "");
|
|
2520
3170
|
const [workspaceId, setWorkspaceId] = (0, react.useState)(task?.workspaceId ?? state.filters.workspaceId ?? state.workspaces[0]?.id ?? "");
|
|
2521
|
-
const [urgency, setUrgency] = (0, react.useState)(task?.urgency ?? "normal");
|
|
2522
|
-
const [mode, setMode] = (0, react.useState)(task?.execution.mode === "scheduled" ? "scheduled" : "claim");
|
|
2523
|
-
const [cron, setCron] = (0, react.useState)(task?.execution.cron ?? "0 9 * * *");
|
|
3171
|
+
const [urgency, setUrgency] = (0, react.useState)(task?.urgency ?? (prefill?.urgency === "urgent" || prefill?.urgency === "relaxed" ? prefill.urgency : "normal"));
|
|
3172
|
+
const [mode, setMode] = (0, react.useState)(task?.execution.mode === "scheduled" || prefill?.execution?.mode === "scheduled" ? "scheduled" : "claim");
|
|
3173
|
+
const [cron, setCron] = (0, react.useState)(task?.execution.cron ?? prefill?.execution?.cron ?? "0 9 * * *");
|
|
2524
3174
|
const [catalog, setCatalog] = (0, react.useState)([]);
|
|
2525
|
-
const [model, setModel] = (0, react.useState)(task?.model !== void 0 ? JSON.stringify(task
|
|
2526
|
-
const
|
|
3175
|
+
const [model, setModel] = (0, react.useState)(task?.model !== void 0 || prefill?.model !== void 0 ? JSON.stringify(task?.model ?? prefill?.model) : "");
|
|
3176
|
+
const initialPreset = task?.presetId ?? prefill?.presetId ?? "";
|
|
3177
|
+
const [presetId, setPresetId] = (0, react.useState)(initialPreset);
|
|
2527
3178
|
const [presets, setPresets] = (0, react.useState)([]);
|
|
2528
3179
|
const [presetDefault, setPresetDefault] = (0, react.useState)(void 0);
|
|
2529
|
-
const [isolation, setIsolation] = (0, react.useState)(task?.isolation ?? loadDefaultIsolation());
|
|
3180
|
+
const [isolation, setIsolation] = (0, react.useState)(task?.isolation ?? (prefill?.isolation === "none" ? "none" : prefill?.isolation === "worktree" ? "worktree" : loadDefaultIsolation()));
|
|
3181
|
+
const [checkRows, setCheckRows] = (0, react.useState)(task?.checklist !== void 0 && task.checklist.length > 0 ? task.checklist.map((i) => ({ ...i })) : (prefill?.checklist ?? []).map((text) => ({
|
|
3182
|
+
text,
|
|
3183
|
+
checked: false
|
|
3184
|
+
})));
|
|
2530
3185
|
const titleRef = (0, react.useRef)(null);
|
|
2531
3186
|
(0, react.useEffect)(() => {
|
|
2532
3187
|
titleRef.current?.focus();
|
|
@@ -2547,9 +3202,13 @@ window.__ModuleLoader__.load({
|
|
|
2547
3202
|
face().then((roster) => {
|
|
2548
3203
|
setPresets(roster.presets);
|
|
2549
3204
|
setPresetDefault(roster.defaultId);
|
|
2550
|
-
if (task?.presetId === void 0 && roster.defaultId !== void 0) setPresetId(roster.defaultId);
|
|
3205
|
+
if (task?.presetId === void 0 && initialPreset === "" && roster.defaultId !== void 0) setPresetId(roster.defaultId);
|
|
2551
3206
|
}).catch(() => setPresets([]));
|
|
2552
|
-
}, [
|
|
3207
|
+
}, [
|
|
3208
|
+
controller,
|
|
3209
|
+
task?.presetId,
|
|
3210
|
+
initialPreset
|
|
3211
|
+
]);
|
|
2553
3212
|
const cronMatch = mode === "scheduled" ? parseCron(cron.trim()) : null;
|
|
2554
3213
|
const nextRun = cronMatch !== null ? nextCronTime(cronMatch, Date.now()) : null;
|
|
2555
3214
|
const cronBad = mode === "scheduled" && (cronMatch === null || nextRun === null);
|
|
@@ -2566,11 +3225,17 @@ window.__ModuleLoader__.load({
|
|
|
2566
3225
|
};
|
|
2567
3226
|
/** Preset payload: '' = follow the deployment default (submit omits). */
|
|
2568
3227
|
const presetPayload = () => presetId.trim().length > 0 ? presetId.trim() : void 0;
|
|
3228
|
+
/** Checklist rows with non-empty text (blank rows are dropped on submit). */
|
|
3229
|
+
const filledRows = () => checkRows.map((r) => ({
|
|
3230
|
+
...r,
|
|
3231
|
+
text: r.text.trim()
|
|
3232
|
+
})).filter((r) => r.text.length > 0);
|
|
2569
3233
|
const submit = () => {
|
|
2570
3234
|
if (!valid) return;
|
|
2571
3235
|
const picked = model !== "" ? JSON.parse(model) : void 0;
|
|
2572
3236
|
const isolationOut = isolationPayload();
|
|
2573
3237
|
const presetOut = presetPayload();
|
|
3238
|
+
const rows = filledRows();
|
|
2574
3239
|
if (editing) controller.update(task.id, task.version, {
|
|
2575
3240
|
title,
|
|
2576
3241
|
description,
|
|
@@ -2583,7 +3248,8 @@ window.__ModuleLoader__.load({
|
|
|
2583
3248
|
} : { mode },
|
|
2584
3249
|
model: picked ?? null,
|
|
2585
3250
|
...isolationOut !== void 0 && !isolationLocked ? { isolation: isolationOut } : {},
|
|
2586
|
-
presetId: presetOut ?? null
|
|
3251
|
+
presetId: presetOut ?? null,
|
|
3252
|
+
checklist: rows.length > 0 ? rows : null
|
|
2587
3253
|
});
|
|
2588
3254
|
else controller.create({
|
|
2589
3255
|
title,
|
|
@@ -2597,7 +3263,8 @@ window.__ModuleLoader__.load({
|
|
|
2597
3263
|
} : { mode },
|
|
2598
3264
|
model: picked,
|
|
2599
3265
|
...isolationOut !== void 0 ? { isolation: isolationOut } : {},
|
|
2600
|
-
...presetOut !== void 0 ? { presetId: presetOut } : {}
|
|
3266
|
+
...presetOut !== void 0 ? { presetId: presetOut } : {},
|
|
3267
|
+
...rows.length > 0 ? { checklist: rows.map((r) => r.text) } : {}
|
|
2601
3268
|
});
|
|
2602
3269
|
};
|
|
2603
3270
|
/** Save the form, then immediately trigger a manual run of the task. */
|
|
@@ -2606,6 +3273,7 @@ window.__ModuleLoader__.load({
|
|
|
2606
3273
|
const picked = model !== "" ? JSON.parse(model) : void 0;
|
|
2607
3274
|
const isolationOut = isolationPayload();
|
|
2608
3275
|
const presetOut = presetPayload();
|
|
3276
|
+
const rows = filledRows();
|
|
2609
3277
|
if (editing) (async () => {
|
|
2610
3278
|
if (await controller.update(task.id, task.version, {
|
|
2611
3279
|
title,
|
|
@@ -2619,7 +3287,8 @@ window.__ModuleLoader__.load({
|
|
|
2619
3287
|
} : { mode },
|
|
2620
3288
|
model: picked ?? null,
|
|
2621
3289
|
...isolationOut !== void 0 && !isolationLocked ? { isolation: isolationOut } : {},
|
|
2622
|
-
presetId: presetOut ?? null
|
|
3290
|
+
presetId: presetOut ?? null,
|
|
3291
|
+
checklist: rows.length > 0 ? rows : null
|
|
2623
3292
|
})) await controller.run(task.id);
|
|
2624
3293
|
})();
|
|
2625
3294
|
else (async () => {
|
|
@@ -2635,7 +3304,8 @@ window.__ModuleLoader__.load({
|
|
|
2635
3304
|
} : { mode },
|
|
2636
3305
|
model: picked,
|
|
2637
3306
|
...isolationOut !== void 0 ? { isolation: isolationOut } : {},
|
|
2638
|
-
...presetOut !== void 0 ? { presetId: presetOut } : {}
|
|
3307
|
+
...presetOut !== void 0 ? { presetId: presetOut } : {},
|
|
3308
|
+
...rows.length > 0 ? { checklist: rows.map((r) => r.text) } : {}
|
|
2639
3309
|
});
|
|
2640
3310
|
if (id !== void 0) await controller.run(id);
|
|
2641
3311
|
})();
|
|
@@ -2874,6 +3544,15 @@ window.__ModuleLoader__.load({
|
|
|
2874
3544
|
className: "dsh-atb-isolation-note",
|
|
2875
3545
|
children: "当前项目非 git 仓库,将在原目录执行(任务仍按默认配置创建,运行时自动降级)"
|
|
2876
3546
|
})]
|
|
3547
|
+
}),
|
|
3548
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
3549
|
+
label: editing ? "验收清单(DoD)" : "验收清单(DoD,可选)",
|
|
3550
|
+
full: true,
|
|
3551
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChecklistEditor, {
|
|
3552
|
+
rows: checkRows,
|
|
3553
|
+
onChange: setCheckRows,
|
|
3554
|
+
editing
|
|
3555
|
+
})
|
|
2877
3556
|
})
|
|
2878
3557
|
]
|
|
2879
3558
|
}),
|
|
@@ -2916,6 +3595,431 @@ window.__ModuleLoader__.load({
|
|
|
2916
3595
|
});
|
|
2917
3596
|
}
|
|
2918
3597
|
|
|
3598
|
+
//#endregion
|
|
3599
|
+
//#region src/client/board/ImportModal.tsx
|
|
3600
|
+
/**
|
|
3601
|
+
* The ledger-import modal (0.4.0): pick a JSON file → dry-run preview
|
|
3602
|
+
* (create / overwrite / invalid classification) → commit as merge or
|
|
3603
|
+
* replace. Replace swaps the WHOLE ledger after an automatic backup and a
|
|
3604
|
+
* double confirmation. Files exported by ⬇ JSON import as-is.
|
|
3605
|
+
*
|
|
3606
|
+
* @module dsh-taskboard/client/board/ImportModal
|
|
3607
|
+
*/
|
|
3608
|
+
/** One classified row (create / overwrite). */
|
|
3609
|
+
function PlanRow({ row }) {
|
|
3610
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3611
|
+
className: "dsh-atb-imp-row",
|
|
3612
|
+
title: row.id,
|
|
3613
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3614
|
+
className: "dsh-atb-imp-row-title",
|
|
3615
|
+
children: row.title
|
|
3616
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3617
|
+
className: "dsh-atb-imp-row-status",
|
|
3618
|
+
children: row.status
|
|
3619
|
+
})]
|
|
3620
|
+
});
|
|
3621
|
+
}
|
|
3622
|
+
/**
|
|
3623
|
+
* The import modal.
|
|
3624
|
+
* @param controller - the controller.
|
|
3625
|
+
*/
|
|
3626
|
+
function ImportModal({ controller }) {
|
|
3627
|
+
const [fileName, setFileName] = (0, react.useState)("");
|
|
3628
|
+
const [parsed, setParsed] = (0, react.useState)(null);
|
|
3629
|
+
const [parseError, setParseError] = (0, react.useState)(void 0);
|
|
3630
|
+
const [plan, setPlan] = (0, react.useState)(void 0);
|
|
3631
|
+
const [mode, setMode] = (0, react.useState)("merge");
|
|
3632
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
3633
|
+
const [result, setResult] = (0, react.useState)(void 0);
|
|
3634
|
+
const [confirmReplace, setConfirmReplace] = (0, react.useState)(false);
|
|
3635
|
+
const fileRef = (0, react.useRef)(null);
|
|
3636
|
+
const { alert: showAlert, el: alertEl } = useAlert();
|
|
3637
|
+
/** Read + parse the picked file, then dry-run the preview. */
|
|
3638
|
+
const onFile = (file) => {
|
|
3639
|
+
setPlan(void 0);
|
|
3640
|
+
setParseError(void 0);
|
|
3641
|
+
setResult(void 0);
|
|
3642
|
+
setConfirmReplace(false);
|
|
3643
|
+
setFileName("");
|
|
3644
|
+
setParsed(null);
|
|
3645
|
+
if (file === void 0) return;
|
|
3646
|
+
file.text().then((text) => {
|
|
3647
|
+
try {
|
|
3648
|
+
const value = JSON.parse(text);
|
|
3649
|
+
setParsed(value);
|
|
3650
|
+
setFileName(file.name);
|
|
3651
|
+
controller.importPreview(value).then((p) => {
|
|
3652
|
+
if (p !== void 0) setPlan(p);
|
|
3653
|
+
});
|
|
3654
|
+
} catch {
|
|
3655
|
+
setParseError("文件不是合法 JSON");
|
|
3656
|
+
}
|
|
3657
|
+
});
|
|
3658
|
+
};
|
|
3659
|
+
/** Commit the import (replace requires the inline double confirmation). */
|
|
3660
|
+
const commit = () => {
|
|
3661
|
+
if (parsed === null || plan === void 0 || busy) return;
|
|
3662
|
+
if (mode === "replace" && !confirmReplace) {
|
|
3663
|
+
setConfirmReplace(true);
|
|
3664
|
+
return;
|
|
3665
|
+
}
|
|
3666
|
+
setBusy(true);
|
|
3667
|
+
controller.importCommit(mode, parsed).then((r) => {
|
|
3668
|
+
setBusy(false);
|
|
3669
|
+
setConfirmReplace(false);
|
|
3670
|
+
if (r === void 0) return;
|
|
3671
|
+
setResult(r.mode === "replace" ? `整册替换完成:导入 ${r.created + r.overwritten} 张(原 ${r.replacedTotal} 张已整册备份)` : `合并完成:新增 ${r.created} 张、覆盖 ${r.overwritten} 张`);
|
|
3672
|
+
});
|
|
3673
|
+
};
|
|
3674
|
+
const close = () => controller.closeImport();
|
|
3675
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3676
|
+
className: "dsh-atb-modal-backdrop",
|
|
3677
|
+
onClick: (e) => {
|
|
3678
|
+
if (e.target === e.currentTarget) close();
|
|
3679
|
+
},
|
|
3680
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3681
|
+
className: "dsh-atb-modal dsh-atb-imp",
|
|
3682
|
+
role: "dialog",
|
|
3683
|
+
"aria-modal": "true",
|
|
3684
|
+
"aria-label": "导入台账",
|
|
3685
|
+
children: [
|
|
3686
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3687
|
+
className: "dsh-atb-modal-head",
|
|
3688
|
+
children: [
|
|
3689
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3690
|
+
className: "dsh-atb-modal-headicon",
|
|
3691
|
+
children: "⬆"
|
|
3692
|
+
}),
|
|
3693
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3694
|
+
className: "dsh-atb-modal-headtext",
|
|
3695
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: "导入台账" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: "选择导出的 JSON 备份文件:先预览、再合并或整册替换" })]
|
|
3696
|
+
}),
|
|
3697
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3698
|
+
type: "button",
|
|
3699
|
+
className: "dsh-atb-modal-close",
|
|
3700
|
+
"aria-label": "关闭",
|
|
3701
|
+
onClick: close,
|
|
3702
|
+
children: "✕"
|
|
3703
|
+
})
|
|
3704
|
+
]
|
|
3705
|
+
}),
|
|
3706
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3707
|
+
className: "dsh-atb-modal-body",
|
|
3708
|
+
children: [
|
|
3709
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3710
|
+
className: "dsh-atb-imp-picker",
|
|
3711
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
3712
|
+
ref: fileRef,
|
|
3713
|
+
type: "file",
|
|
3714
|
+
accept: ".json,application/json",
|
|
3715
|
+
onChange: (e) => onFile(e.target.files?.[0])
|
|
3716
|
+
}), fileName.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3717
|
+
className: "dsh-atb-imp-filename",
|
|
3718
|
+
children: fileName
|
|
3719
|
+
})]
|
|
3720
|
+
}),
|
|
3721
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3722
|
+
className: "dsh-atb-imp-note",
|
|
3723
|
+
children: "⬇ JSON 导出的文件即为同格式备份,可直接导入恢复;导入文件的 schemaVersion 必须与当前版本一致。"
|
|
3724
|
+
}),
|
|
3725
|
+
parseError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3726
|
+
className: "dsh-atb-imp-error",
|
|
3727
|
+
children: parseError
|
|
3728
|
+
}),
|
|
3729
|
+
plan === void 0 && parseError === void 0 && fileName.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3730
|
+
className: "dsh-atb-empty2",
|
|
3731
|
+
children: "预览中…"
|
|
3732
|
+
}),
|
|
3733
|
+
plan !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
3734
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3735
|
+
className: "dsh-atb-imp-stats",
|
|
3736
|
+
children: [
|
|
3737
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3738
|
+
className: "dsh-atb-imp-stat",
|
|
3739
|
+
"data-tone": "ok",
|
|
3740
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: plan.create.length }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "新增" })]
|
|
3741
|
+
}),
|
|
3742
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3743
|
+
className: "dsh-atb-imp-stat",
|
|
3744
|
+
"data-tone": "warn",
|
|
3745
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: plan.overwrite.length }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "覆盖(同 id)" })]
|
|
3746
|
+
}),
|
|
3747
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3748
|
+
className: "dsh-atb-imp-stat",
|
|
3749
|
+
"data-tone": plan.invalid.length > 0 ? "bad" : void 0,
|
|
3750
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: plan.invalid.length }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "无效(跳过)" })]
|
|
3751
|
+
})
|
|
3752
|
+
]
|
|
3753
|
+
}),
|
|
3754
|
+
plan.create.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3755
|
+
className: "dsh-atb-imp-sec",
|
|
3756
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", { children: "新增任务" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3757
|
+
className: "dsh-atb-imp-list",
|
|
3758
|
+
children: plan.create.map((r) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlanRow, { row: r }, r.id))
|
|
3759
|
+
})]
|
|
3760
|
+
}),
|
|
3761
|
+
plan.overwrite.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3762
|
+
className: "dsh-atb-imp-sec",
|
|
3763
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", { children: "覆盖任务(整卡替换,含执行历史与评论)" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3764
|
+
className: "dsh-atb-imp-list",
|
|
3765
|
+
children: plan.overwrite.map((r) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlanRow, { row: r }, r.id))
|
|
3766
|
+
})]
|
|
3767
|
+
}),
|
|
3768
|
+
plan.invalid.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3769
|
+
className: "dsh-atb-imp-sec",
|
|
3770
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", { children: "无效条目(不会导入)" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3771
|
+
className: "dsh-atb-imp-list",
|
|
3772
|
+
children: plan.invalid.map((r, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3773
|
+
className: "dsh-atb-imp-row",
|
|
3774
|
+
"data-tone": "bad",
|
|
3775
|
+
title: r.id ?? "",
|
|
3776
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3777
|
+
className: "dsh-atb-imp-row-title",
|
|
3778
|
+
children: r.id ?? "(无 id)"
|
|
3779
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3780
|
+
className: "dsh-atb-imp-row-status",
|
|
3781
|
+
children: r.reason
|
|
3782
|
+
})]
|
|
3783
|
+
}, r.id ?? `invalid-${i}`))
|
|
3784
|
+
})]
|
|
3785
|
+
}),
|
|
3786
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3787
|
+
className: "dsh-atb-mode-picker",
|
|
3788
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
3789
|
+
type: "button",
|
|
3790
|
+
className: "dsh-atb-mode-opt",
|
|
3791
|
+
"data-on": mode === "merge",
|
|
3792
|
+
onClick: () => {
|
|
3793
|
+
setMode("merge");
|
|
3794
|
+
setConfirmReplace(false);
|
|
3795
|
+
},
|
|
3796
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3797
|
+
className: "dsh-atb-mode-name",
|
|
3798
|
+
children: "⊕ 合并"
|
|
3799
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3800
|
+
className: "dsh-atb-mode-hint",
|
|
3801
|
+
children: "新增 + 按 id 覆盖,其余不动"
|
|
3802
|
+
})]
|
|
3803
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
3804
|
+
type: "button",
|
|
3805
|
+
className: "dsh-atb-mode-opt",
|
|
3806
|
+
"data-on": mode === "replace",
|
|
3807
|
+
onClick: () => setMode("replace"),
|
|
3808
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3809
|
+
className: "dsh-atb-mode-name",
|
|
3810
|
+
children: "💣 整册替换"
|
|
3811
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3812
|
+
className: "dsh-atb-mode-hint",
|
|
3813
|
+
children: "清空当前台账,以导入文件为准(先自动备份)"
|
|
3814
|
+
})]
|
|
3815
|
+
})]
|
|
3816
|
+
}),
|
|
3817
|
+
result !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3818
|
+
className: "dsh-atb-imp-result",
|
|
3819
|
+
children: result
|
|
3820
|
+
})
|
|
3821
|
+
] })
|
|
3822
|
+
]
|
|
3823
|
+
}),
|
|
3824
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3825
|
+
className: "dsh-atb-modal-foot",
|
|
3826
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3827
|
+
className: "dsh-atb-modal-hint",
|
|
3828
|
+
children: mode === "replace" ? confirmReplace ? "⚠ 再次点击确认执行整册替换(不可撤销,已自动备份)" : "整册替换需要二次确认" : "合并只写入预览中列出的任务"
|
|
3829
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3830
|
+
className: "dsh-atb-modal-footbtns",
|
|
3831
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3832
|
+
type: "button",
|
|
3833
|
+
className: "dsh-atb-btn",
|
|
3834
|
+
onClick: close,
|
|
3835
|
+
children: result !== void 0 ? "关闭" : "取消"
|
|
3836
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3837
|
+
type: "button",
|
|
3838
|
+
className: "dsh-atb-btn",
|
|
3839
|
+
"data-primary": "true",
|
|
3840
|
+
"data-danger": mode === "replace" && confirmReplace ? "true" : void 0,
|
|
3841
|
+
disabled: plan === void 0 || busy || result !== void 0 && false,
|
|
3842
|
+
onClick: commit,
|
|
3843
|
+
children: mode === "replace" && confirmReplace ? "确认整册替换" : "执行导入"
|
|
3844
|
+
})]
|
|
3845
|
+
})]
|
|
3846
|
+
})
|
|
3847
|
+
]
|
|
3848
|
+
}), alertEl]
|
|
3849
|
+
});
|
|
3850
|
+
}
|
|
3851
|
+
|
|
3852
|
+
//#endregion
|
|
3853
|
+
//#region src/client/board/TemplateManager.tsx
|
|
3854
|
+
/**
|
|
3855
|
+
* The template-manager modal (0.4.0): rename / delete / use the stored task
|
|
3856
|
+
* templates. Templates live host-side (side file next to the ledger) and
|
|
3857
|
+
* prefill the create form from the + 新建任务 ▼ dropdown.
|
|
3858
|
+
*
|
|
3859
|
+
* @module dsh-taskboard/client/board/TemplateManager
|
|
3860
|
+
*/
|
|
3861
|
+
/**
|
|
3862
|
+
* The template manager modal.
|
|
3863
|
+
* @param controller - the controller.
|
|
3864
|
+
*/
|
|
3865
|
+
function TemplateManager({ controller }) {
|
|
3866
|
+
const state = controller.getSnapshot();
|
|
3867
|
+
const [edits, setEdits] = (0, react.useState)({});
|
|
3868
|
+
const [confirmId, setConfirmId] = (0, react.useState)(void 0);
|
|
3869
|
+
const { alert: showAlert, el: alertEl } = useAlert();
|
|
3870
|
+
const close = () => controller.closeTemplateManager();
|
|
3871
|
+
const nameOf = (id, fallback) => edits[id] ?? fallback;
|
|
3872
|
+
/** Save one template's rename. */
|
|
3873
|
+
const save = (id, name) => {
|
|
3874
|
+
const template = state.templates.find((t) => t.id === id);
|
|
3875
|
+
if (template === void 0 || name === template.name) return;
|
|
3876
|
+
controller.upsertTemplate({
|
|
3877
|
+
id,
|
|
3878
|
+
name,
|
|
3879
|
+
task: template.task
|
|
3880
|
+
}).then((ok) => {
|
|
3881
|
+
if (ok) {
|
|
3882
|
+
setEdits((prev) => {
|
|
3883
|
+
const next = { ...prev };
|
|
3884
|
+
delete next[id];
|
|
3885
|
+
return next;
|
|
3886
|
+
});
|
|
3887
|
+
showAlert("模板已改名");
|
|
3888
|
+
}
|
|
3889
|
+
});
|
|
3890
|
+
};
|
|
3891
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3892
|
+
className: "dsh-atb-modal-backdrop",
|
|
3893
|
+
onClick: (e) => {
|
|
3894
|
+
if (e.target === e.currentTarget) close();
|
|
3895
|
+
},
|
|
3896
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3897
|
+
className: "dsh-atb-modal dsh-atb-tplm",
|
|
3898
|
+
role: "dialog",
|
|
3899
|
+
"aria-modal": "true",
|
|
3900
|
+
"aria-label": "管理模板",
|
|
3901
|
+
children: [
|
|
3902
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3903
|
+
className: "dsh-atb-modal-head",
|
|
3904
|
+
children: [
|
|
3905
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3906
|
+
className: "dsh-atb-modal-headicon",
|
|
3907
|
+
children: "⌗"
|
|
3908
|
+
}),
|
|
3909
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3910
|
+
className: "dsh-atb-modal-headtext",
|
|
3911
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: "任务模板" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: "新建任务 ▼ 下拉的模板:改名 / 删除 / 直接使用;任务详情页「存为模板」可新增" })]
|
|
3912
|
+
}),
|
|
3913
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3914
|
+
type: "button",
|
|
3915
|
+
className: "dsh-atb-modal-close",
|
|
3916
|
+
"aria-label": "关闭",
|
|
3917
|
+
onClick: close,
|
|
3918
|
+
children: "✕"
|
|
3919
|
+
})
|
|
3920
|
+
]
|
|
3921
|
+
}),
|
|
3922
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3923
|
+
className: "dsh-atb-modal-body",
|
|
3924
|
+
children: state.templates.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3925
|
+
className: "dsh-atb-empty2",
|
|
3926
|
+
children: "暂无模板 — 在任务详情页点「存为模板」把常用配置沉淀下来"
|
|
3927
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3928
|
+
className: "dsh-atb-tplm-list",
|
|
3929
|
+
children: state.templates.map((t) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3930
|
+
className: "dsh-atb-tplm-row",
|
|
3931
|
+
children: [
|
|
3932
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
3933
|
+
className: "dsh-atb-tplm-name",
|
|
3934
|
+
value: nameOf(t.id, t.name),
|
|
3935
|
+
maxLength: 60,
|
|
3936
|
+
spellCheck: false,
|
|
3937
|
+
"aria-label": `模板名 ${t.name}`,
|
|
3938
|
+
onChange: (e) => setEdits((prev) => ({
|
|
3939
|
+
...prev,
|
|
3940
|
+
[t.id]: e.target.value
|
|
3941
|
+
})),
|
|
3942
|
+
onKeyDown: (e) => {
|
|
3943
|
+
if (e.key === "Enter") save(t.id, nameOf(t.id, t.name));
|
|
3944
|
+
}
|
|
3945
|
+
}),
|
|
3946
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3947
|
+
className: "dsh-atb-tplm-meta",
|
|
3948
|
+
children: [
|
|
3949
|
+
t.builtin === true ? "内置" : "自建",
|
|
3950
|
+
t.task.checklist !== void 0 && t.task.checklist.length > 0 ? ` · 清单 ${t.task.checklist.length} 项` : "",
|
|
3951
|
+
t.task.urgency !== void 0 ? ` · ${t.task.urgency}` : ""
|
|
3952
|
+
]
|
|
3953
|
+
}),
|
|
3954
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3955
|
+
className: "dsh-atb-tplm-btns",
|
|
3956
|
+
children: [
|
|
3957
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3958
|
+
type: "button",
|
|
3959
|
+
className: "dsh-atb-btn",
|
|
3960
|
+
disabled: nameOf(t.id, t.name) === t.name || nameOf(t.id, t.name).trim().length === 0,
|
|
3961
|
+
title: "保存改名",
|
|
3962
|
+
onClick: () => save(t.id, nameOf(t.id, t.name)),
|
|
3963
|
+
children: "改名"
|
|
3964
|
+
}),
|
|
3965
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3966
|
+
type: "button",
|
|
3967
|
+
className: "dsh-atb-btn",
|
|
3968
|
+
title: "用此模板打开新建表单",
|
|
3969
|
+
onClick: () => {
|
|
3970
|
+
close();
|
|
3971
|
+
controller.newFromTemplate(t.task);
|
|
3972
|
+
},
|
|
3973
|
+
children: "用此新建"
|
|
3974
|
+
}),
|
|
3975
|
+
confirmId === t.id ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3976
|
+
type: "button",
|
|
3977
|
+
className: "dsh-atb-btn",
|
|
3978
|
+
"data-danger": "true",
|
|
3979
|
+
onClick: () => {
|
|
3980
|
+
controller.deleteTemplate(t.id);
|
|
3981
|
+
setConfirmId(void 0);
|
|
3982
|
+
},
|
|
3983
|
+
children: "确认删除"
|
|
3984
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3985
|
+
type: "button",
|
|
3986
|
+
className: "dsh-atb-btn",
|
|
3987
|
+
onClick: () => setConfirmId(void 0),
|
|
3988
|
+
children: "取消"
|
|
3989
|
+
})] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3990
|
+
type: "button",
|
|
3991
|
+
className: "dsh-atb-btn",
|
|
3992
|
+
"data-danger": "true",
|
|
3993
|
+
title: "删除该模板",
|
|
3994
|
+
onClick: () => setConfirmId(t.id),
|
|
3995
|
+
children: "🗑"
|
|
3996
|
+
})
|
|
3997
|
+
]
|
|
3998
|
+
})
|
|
3999
|
+
]
|
|
4000
|
+
}, t.id))
|
|
4001
|
+
})
|
|
4002
|
+
}),
|
|
4003
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4004
|
+
className: "dsh-atb-modal-foot",
|
|
4005
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4006
|
+
className: "dsh-atb-modal-hint",
|
|
4007
|
+
children: "模板随台账一同保存在 DSH 主目录,升级不丢"
|
|
4008
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4009
|
+
className: "dsh-atb-modal-footbtns",
|
|
4010
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4011
|
+
type: "button",
|
|
4012
|
+
className: "dsh-atb-btn",
|
|
4013
|
+
onClick: close,
|
|
4014
|
+
children: "关闭"
|
|
4015
|
+
})
|
|
4016
|
+
})]
|
|
4017
|
+
})
|
|
4018
|
+
]
|
|
4019
|
+
}), alertEl]
|
|
4020
|
+
});
|
|
4021
|
+
}
|
|
4022
|
+
|
|
2919
4023
|
//#endregion
|
|
2920
4024
|
//#region src/client/board/TaskBoard.tsx
|
|
2921
4025
|
/**
|
|
@@ -2984,6 +4088,8 @@ window.__ModuleLoader__.load({
|
|
|
2984
4088
|
const live = filterTasks(state, state.ledger.tasks.filter((t) => t.trashedAt === void 0));
|
|
2985
4089
|
const selected = state.selectedId === void 0 ? void 0 : state.ledger.tasks.find((t) => t.id === state.selectedId);
|
|
2986
4090
|
const { alert: showAlert, el: alertEl } = useAlert();
|
|
4091
|
+
const [newMenuOpen, setNewMenuOpen] = (0, react.useState)(false);
|
|
4092
|
+
const closeMenu = () => setNewMenuOpen(false);
|
|
2987
4093
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2988
4094
|
className: "dsh-atb-board",
|
|
2989
4095
|
children: [
|
|
@@ -3002,12 +4108,55 @@ window.__ModuleLoader__.load({
|
|
|
3002
4108
|
state.ledger.revision
|
|
3003
4109
|
]
|
|
3004
4110
|
}),
|
|
3005
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
4111
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4112
|
+
className: "dsh-atb-newmenu",
|
|
4113
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4114
|
+
type: "button",
|
|
4115
|
+
className: "dsh-atb-btn",
|
|
4116
|
+
"data-primary": "true",
|
|
4117
|
+
onClick: () => {
|
|
4118
|
+
const next = !newMenuOpen;
|
|
4119
|
+
setNewMenuOpen(next);
|
|
4120
|
+
if (next) controller.prepareTemplateMenu();
|
|
4121
|
+
},
|
|
4122
|
+
children: "+ 新建任务 ▼"
|
|
4123
|
+
}), newMenuOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4124
|
+
className: "dsh-atb-newmenu-backdrop",
|
|
4125
|
+
onClick: closeMenu
|
|
4126
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4127
|
+
className: "dsh-atb-newmenu-list",
|
|
4128
|
+
children: [
|
|
4129
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4130
|
+
type: "button",
|
|
4131
|
+
className: "dsh-atb-newmenu-opt",
|
|
4132
|
+
onClick: () => {
|
|
4133
|
+
closeMenu();
|
|
4134
|
+
controller.setComposer(true);
|
|
4135
|
+
},
|
|
4136
|
+
children: "空白任务"
|
|
4137
|
+
}),
|
|
4138
|
+
state.templates.map((t) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
4139
|
+
type: "button",
|
|
4140
|
+
className: "dsh-atb-newmenu-opt",
|
|
4141
|
+
title: t.task.description !== void 0 && t.task.description.length > 0 ? t.task.description.slice(0, 120) : t.name,
|
|
4142
|
+
onClick: () => {
|
|
4143
|
+
closeMenu();
|
|
4144
|
+
controller.newFromTemplate(t.task);
|
|
4145
|
+
},
|
|
4146
|
+
children: [t.name, t.builtin === true ? "" : ""]
|
|
4147
|
+
}, t.id)),
|
|
4148
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "dsh-atb-newmenu-sep" }),
|
|
4149
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4150
|
+
type: "button",
|
|
4151
|
+
className: "dsh-atb-newmenu-opt",
|
|
4152
|
+
onClick: () => {
|
|
4153
|
+
closeMenu();
|
|
4154
|
+
controller.openTemplateManager();
|
|
4155
|
+
},
|
|
4156
|
+
children: "⌗ 管理模板…"
|
|
4157
|
+
})
|
|
4158
|
+
]
|
|
4159
|
+
})] })]
|
|
3011
4160
|
}),
|
|
3012
4161
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "dsh-atb-spacer" }),
|
|
3013
4162
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
@@ -3081,6 +4230,13 @@ window.__ModuleLoader__.load({
|
|
|
3081
4230
|
onClick: () => controller.openDiagnostics(),
|
|
3082
4231
|
children: "⚙ 诊断"
|
|
3083
4232
|
}),
|
|
4233
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4234
|
+
type: "button",
|
|
4235
|
+
className: "dsh-atb-btn",
|
|
4236
|
+
title: "从 JSON 备份文件导入台账(预览后合并或整册替换)",
|
|
4237
|
+
onClick: () => controller.openImport(),
|
|
4238
|
+
children: "⬆ 导入"
|
|
4239
|
+
}),
|
|
3084
4240
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3085
4241
|
type: "button",
|
|
3086
4242
|
className: "dsh-atb-btn",
|
|
@@ -3182,6 +4338,8 @@ window.__ModuleLoader__.load({
|
|
|
3182
4338
|
task: state.editingId === void 0 ? void 0 : state.ledger.tasks.find((t) => t.id === state.editingId)
|
|
3183
4339
|
}),
|
|
3184
4340
|
state.diagOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DiagnosticsPanel, { controller }),
|
|
4341
|
+
state.importOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImportModal, { controller }),
|
|
4342
|
+
state.tplManagerOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TemplateManager, { controller }),
|
|
3185
4343
|
alertEl
|
|
3186
4344
|
]
|
|
3187
4345
|
});
|
|
@@ -3436,6 +4594,7 @@ window.__ModuleLoader__.load({
|
|
|
3436
4594
|
if (!controller.getSnapshot().boardOpen) return;
|
|
3437
4595
|
const target = event.target;
|
|
3438
4596
|
if (target === null) return;
|
|
4597
|
+
if (target.closest("[data-dsh-atb-entry]") !== null) return;
|
|
3439
4598
|
if (target.closest(SIDEBAR_ROW_SELECTOR) !== null) controller.closeBoard();
|
|
3440
4599
|
};
|
|
3441
4600
|
document.addEventListener("click", onClickSidebarRow, true);
|