@runfusion/fusion 0.23.0 → 0.24.0
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/dist/bin.js +26610 -20597
- package/dist/client/assets/AgentDetailView-BwJaLqZh.css +1 -0
- package/dist/client/assets/AgentDetailView-gy_5SUj2.js +18 -0
- package/dist/client/assets/AgentsView-BkB9FiMT.js +29 -0
- package/dist/client/assets/{AgentsView-DSGQWObq.css → AgentsView-CV3vm7Qk.css} +1 -1
- package/dist/client/assets/ChatView-B_-B8fqu.js +1 -0
- package/dist/client/assets/ChatView-DwJAd5G1.css +1 -0
- package/dist/client/assets/{DevServerView-C9lzHrcT.js → DevServerView-BkvtjZBa.js} +1 -1
- package/dist/client/assets/{DirectoryPicker-aVdFaV37.js → DirectoryPicker-BK-KbnhP.js} +1 -1
- package/dist/client/assets/{DocumentsView-DIpg3NSP.js → DocumentsView-BEg1CQAk.js} +1 -1
- package/dist/client/assets/{DocumentsView-BrhyOdeE.css → DocumentsView-gv4zG3aT.css} +1 -1
- package/dist/client/assets/EvalsView-Berf9bQm.js +1 -0
- package/dist/client/assets/EvalsView-CUNJ1TLc.css +1 -0
- package/dist/client/assets/{agentSkills-DDHJnrkn.css → ExperimentalAgentOnboardingModal-B-APN_lM.css} +1 -1
- package/dist/client/assets/ExperimentalAgentOnboardingModal-jcInE50G.js +499 -0
- package/dist/client/assets/InsightsView-B0J4mhzV.css +1 -0
- package/dist/client/assets/InsightsView-BX5bSF1J.js +11 -0
- package/dist/client/assets/{MemoryView-nXlTqebk.js → MemoryView-CKElJY_3.js} +2 -2
- package/dist/client/assets/NodesView-DLUOBLf6.js +14 -0
- package/dist/client/assets/NodesView-DT4pXowv.css +1 -0
- package/dist/client/assets/{PiExtensionsManager-Buopv-jb.js → PiExtensionsManager-COlJf0Kx.js} +2 -2
- package/dist/client/assets/PluginManager-CfW55BF4.js +1 -0
- package/dist/client/assets/PluginManager-DtRQXia5.css +1 -0
- package/dist/client/assets/{ResearchView-_BHXUv2j.js → ResearchView-B256Lr8I.js} +1 -1
- package/dist/client/assets/SettingsModal-BeA_nQtW.js +31 -0
- package/dist/client/assets/SettingsModal-DzsLquBu.css +1 -0
- package/dist/client/assets/{SettingsModal-C89Ikhfm.js → SettingsModal-yRqM4DV8.js} +1 -1
- package/dist/client/assets/SetupWizardModal-uUZk3TKT.js +1 -0
- package/dist/client/assets/{SkillsView-hDpTBdFT.js → SkillsView-CP8JX0P_.js} +1 -1
- package/dist/client/assets/TodoView-Cx9cVhq7.css +1 -0
- package/dist/client/assets/TodoView-DCRIkDZ-.js +6 -0
- package/dist/client/assets/createLucideIcon-BazL2hk5.js +21 -0
- package/dist/client/assets/dashboard-view-BkTMSZYn.css +1 -0
- package/dist/client/assets/dashboard-view-CyWN-d02.js +63 -0
- package/dist/client/assets/dashboard-view-lR7YYmSC.js +21 -0
- package/dist/client/assets/{folder-open-usZkXdq2.js → folder-open-DHjELt8-.js} +1 -1
- package/dist/client/assets/index-CQyVRLOb.js +692 -0
- package/dist/client/assets/index-CxA2Nn0_.css +1 -0
- package/dist/client/assets/projectDetection-G3XuxD2X.js +1 -0
- package/dist/client/assets/{star-BAT_ObKE.js → star-DYesq1AV.js} +1 -1
- package/dist/client/assets/{upload-BC2YKNEV.js → upload-DTWF3Db5.js} +1 -1
- package/dist/client/assets/{users-Dkd4rtrN.js → users--syrel4l.js} +1 -1
- package/dist/client/index.html +12 -20
- package/dist/client/theme-data.css +106 -0
- package/dist/client/version.json +1 -1
- package/dist/droid-cli/package.json +1 -1
- package/dist/extension.js +14287 -9568
- package/dist/pi-claude-cli/package.json +1 -1
- package/dist/plugins/fusion-plugin-cursor-runtime/bundled.js +218 -0
- package/dist/plugins/fusion-plugin-cursor-runtime/manifest.json +6 -0
- package/dist/plugins/fusion-plugin-cursor-runtime/package.json +11 -0
- package/dist/plugins/fusion-plugin-dependency-graph/manifest.json +1 -1
- package/dist/plugins/fusion-plugin-dependency-graph/package.json +6 -4
- package/dist/plugins/fusion-plugin-dependency-graph/src/DependencyGraph.css +58 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/DependencyGraph.tsx +301 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/GraphHighlight.css +27 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/GraphTaskNode.css +157 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/GraphTaskNode.tsx +126 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/GraphToolbar.css +35 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/GraphToolbar.tsx +36 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/DependencyGraph.highlighting.test.tsx +112 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/DependencyGraph.persistence.test.tsx +115 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/DependencyGraph.test.tsx +128 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/GraphTaskNode.drag.test.tsx +82 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/GraphTaskNode.test.tsx +307 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/GraphToolbar.test.tsx +60 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/edges.test.tsx +75 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/filtering.test.tsx +62 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/filters.test.ts +78 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/graphPositionStorage.test.ts +95 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/host-integration.test.ts +74 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/index.test.ts +58 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/interactions.test.tsx +121 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/layout.test.ts +70 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/persistence.test.tsx +89 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/useGraphData.test.ts +86 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/useGraphInteraction.test.ts +167 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/useGraphPositions.test.ts +66 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/useNodeDrag.test.ts +81 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/dashboard-interop.d.ts +35 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/dashboard-view.tsx +19 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/edges.tsx +70 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/filters.ts +8 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/hooks/__tests__/useDependencyChain.test.ts +53 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/hooks/useDependencyChain.ts +60 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/hooks/useGraphPositions.ts +45 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/hooks/useNodeDrag.ts +114 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/index.ts +1 -2
- package/dist/plugins/fusion-plugin-dependency-graph/src/layout.ts +91 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/styles/drag.css +15 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/types.ts +21 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/useGraphData.ts +17 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/useGraphInteraction.ts +292 -0
- package/dist/plugins/fusion-plugin-dependency-graph/src/utils/graphPositionStorage.ts +65 -0
- package/dist/plugins/fusion-plugin-droid-runtime/bundled.js +136680 -0
- package/dist/plugins/fusion-plugin-droid-runtime/manifest.json +13 -0
- package/dist/plugins/fusion-plugin-droid-runtime/mcp-schema-server.cjs +49 -0
- package/dist/plugins/fusion-plugin-droid-runtime/package.json +11 -0
- package/dist/plugins/fusion-plugin-hermes-runtime/package.json +1 -1
- package/dist/plugins/fusion-plugin-openclaw-runtime/bundled.js +93 -6
- package/dist/plugins/fusion-plugin-openclaw-runtime/mcp-schema-server.cjs +59 -0
- package/dist/plugins/fusion-plugin-openclaw-runtime/package.json +1 -1
- package/dist/plugins/fusion-plugin-paperclip-runtime/package.json +1 -1
- package/dist/plugins/fusion-plugin-reports/manifest.json +33 -0
- package/dist/plugins/fusion-plugin-reports/package.json +26 -0
- package/dist/plugins/fusion-plugin-reports/src/__tests__/manifest.test.ts +51 -0
- package/dist/plugins/fusion-plugin-reports/src/__tests__/review-panel.test.ts +166 -0
- package/dist/plugins/fusion-plugin-reports/src/__tests__/settings.test.ts +157 -0
- package/dist/plugins/fusion-plugin-reports/src/index.ts +41 -0
- package/dist/plugins/fusion-plugin-reports/src/review-panel.ts +294 -0
- package/dist/plugins/fusion-plugin-reports/src/review-types.ts +75 -0
- package/dist/plugins/fusion-plugin-reports/src/settings.ts +105 -0
- package/dist/plugins/fusion-plugin-roadmap/manifest.json +16 -0
- package/dist/plugins/fusion-plugin-roadmap/package.json +48 -0
- package/dist/plugins/fusion-plugin-roadmap/src/__tests__/api-client.test.ts +101 -0
- package/dist/plugins/fusion-plugin-roadmap/src/__tests__/index.test.ts +92 -0
- package/dist/plugins/fusion-plugin-roadmap/src/__tests__/roadmap-routes.test.ts +48 -0
- package/dist/plugins/fusion-plugin-roadmap/src/__tests__/roadmap-suggestions.test.ts +31 -0
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/RoadmapsView.css +1299 -0
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/RoadmapsView.tsx +2559 -0
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/__tests__/RoadmapsView.test.tsx +1144 -0
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/__tests__/useRoadmaps.test.ts +1756 -0
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/api.ts +70 -0
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/test-setup.ts +7 -0
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/types.ts +1 -0
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/useConfirm.ts +8 -0
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/useRoadmaps.ts +1188 -0
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard/useViewportMode.ts +20 -0
- package/dist/plugins/fusion-plugin-roadmap/src/dashboard-view.tsx +6 -0
- package/dist/plugins/fusion-plugin-roadmap/src/index.ts +74 -0
- package/dist/plugins/fusion-plugin-roadmap/src/roadmap-routes.ts +1 -0
- package/dist/plugins/fusion-plugin-roadmap/src/roadmap-schema.ts +41 -0
- package/dist/plugins/fusion-plugin-roadmap/src/roadmap-suggestions.d.ts +15 -0
- package/dist/plugins/fusion-plugin-roadmap/src/roadmap-suggestions.ts +15 -0
- package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.d.ts +283 -0
- package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.d.ts.map +1 -0
- package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.js +21 -0
- package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.js.map +1 -0
- package/dist/plugins/fusion-plugin-roadmap/src/roadmap-types.ts +310 -0
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.d.ts +5 -0
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.d.ts.map +1 -0
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.js +361 -0
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.js.map +1 -0
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-routes.ts +408 -0
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.d.ts +68 -0
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.d.ts.map +1 -0
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.js +300 -0
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.js.map +1 -0
- package/dist/plugins/fusion-plugin-roadmap/src/routes/roadmap-suggestions.ts +381 -0
- package/dist/plugins/fusion-plugin-roadmap/src/server/index.d.ts +3 -0
- package/dist/plugins/fusion-plugin-roadmap/src/server/index.ts +1 -0
- package/dist/plugins/fusion-plugin-roadmap/src/store/__tests__/roadmap-handoff.test.ts +445 -0
- package/dist/plugins/fusion-plugin-roadmap/src/store/__tests__/roadmap-ordering.test.ts +334 -0
- package/dist/plugins/fusion-plugin-roadmap/src/store/__tests__/roadmap-store.test.ts +1318 -0
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-handoff.ts +163 -0
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.d.ts +37 -0
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.d.ts.map +1 -0
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.js +188 -0
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.js.map +1 -0
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-ordering.ts +311 -0
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.d.ts +299 -0
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.d.ts.map +1 -0
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.js +765 -0
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.js.map +1 -0
- package/dist/plugins/fusion-plugin-roadmap/src/store/roadmap-store.ts +1001 -0
- package/dist/plugins/fusion-plugin-whatsapp-chat/manifest.json +8 -0
- package/dist/plugins/fusion-plugin-whatsapp-chat/package.json +34 -0
- package/dist/plugins/fusion-plugin-whatsapp-chat/src/__tests__/auth-state.test.ts +99 -0
- package/dist/plugins/fusion-plugin-whatsapp-chat/src/__tests__/connection.test.ts +145 -0
- package/dist/plugins/fusion-plugin-whatsapp-chat/src/__tests__/index.test.ts +216 -0
- package/dist/plugins/fusion-plugin-whatsapp-chat/src/__tests__/reply.test.ts +52 -0
- package/dist/plugins/fusion-plugin-whatsapp-chat/src/auth-state.ts +89 -0
- package/dist/plugins/fusion-plugin-whatsapp-chat/src/connection.ts +253 -0
- package/dist/plugins/fusion-plugin-whatsapp-chat/src/index.ts +262 -0
- package/dist/plugins/fusion-plugin-whatsapp-chat/src/qrcode.d.ts +1 -0
- package/dist/plugins/fusion-plugin-whatsapp-chat/src/reply.ts +37 -0
- package/package.json +2 -2
- package/skill/fusion/SKILL.md +2 -2
- package/skill/fusion/references/engine-tools.md +3 -0
- package/skill/fusion/references/extension-tools.md +39 -0
- package/skill/fusion/references/fusion-capabilities.md +3 -0
- package/dist/client/assets/AgentDetailView-C1XceMgi.js +0 -18
- package/dist/client/assets/AgentDetailView-CeO_1MK7.css +0 -1
- package/dist/client/assets/AgentsView-Deh125ss.js +0 -527
- package/dist/client/assets/ChatView-7D_RQDqT.js +0 -1
- package/dist/client/assets/InsightsView-AWo5o_81.css +0 -1
- package/dist/client/assets/InsightsView-jKjEFAx_.js +0 -11
- package/dist/client/assets/NodesView-Di2SvOhg.js +0 -14
- package/dist/client/assets/NodesView-fXqDk9ur.css +0 -1
- package/dist/client/assets/PluginManager-B9-NbQ8f.js +0 -1
- package/dist/client/assets/PluginManager-C1DbPaar.css +0 -1
- package/dist/client/assets/RoadmapsView-DHWjUoc8.js +0 -6
- package/dist/client/assets/SettingsModal-DHitIpsa.css +0 -1
- package/dist/client/assets/SettingsModal-DR_yirvK.js +0 -31
- package/dist/client/assets/SetupWizardModal-BtDMY9pa.js +0 -1
- package/dist/client/assets/agentSkills-B-w5wFHh.js +0 -1
- package/dist/client/assets/index-Bc6ZdGMz.css +0 -1
- package/dist/client/assets/index-D__RMku8.js +0 -694
- package/dist/plugins/fusion-plugin-dependency-graph/src/DependencyGraphView.css +0 -141
- package/dist/plugins/fusion-plugin-dependency-graph/src/DependencyGraphView.tsx +0 -428
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/DependencyGraphView.test.tsx +0 -261
- package/dist/plugins/fusion-plugin-dependency-graph/src/__tests__/storage.test.ts +0 -41
- package/dist/plugins/fusion-plugin-dependency-graph/src/storage.ts +0 -22
- /package/dist/client/assets/{RoadmapsView-DdGlfuu-.css → dashboard-view-DdGlfuu-.css} +0 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
.graph-task-node {
|
|
2
|
+
position: absolute;
|
|
3
|
+
z-index: 1;
|
|
4
|
+
width: min(100%, var(--graph-task-node-width, calc(var(--space-2xl) * 9)));
|
|
5
|
+
max-width: var(--graph-task-node-max-width, calc(var(--space-2xl) * 9.5));
|
|
6
|
+
transition:
|
|
7
|
+
box-shadow var(--transition-fast),
|
|
8
|
+
z-index var(--transition-fast),
|
|
9
|
+
opacity var(--transition-fast),
|
|
10
|
+
border-color var(--transition-fast);
|
|
11
|
+
border: var(--btn-border-width) solid transparent;
|
|
12
|
+
border-radius: var(--radius-sm);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.graph-task-node:hover {
|
|
16
|
+
box-shadow: var(--shadow-md);
|
|
17
|
+
z-index: 2;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.graph-task-node--highlighted {
|
|
21
|
+
box-shadow: 0 0 0 var(--btn-border-width) var(--todo), var(--shadow-md);
|
|
22
|
+
z-index: 2;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.graph-task-node--dimmed {
|
|
26
|
+
opacity: 0.4;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.graph-task-node--active {
|
|
30
|
+
border-color: rgba(var(--in-progress-rgb), 0.5);
|
|
31
|
+
box-shadow: 0 0 0 var(--btn-border-width) rgba(var(--in-progress-rgb), 0.45), 0 0 0.75rem rgba(var(--in-progress-rgb), 0.5), 0 0 1.5rem rgba(var(--in-progress-rgb), 0.2);
|
|
32
|
+
transform: scale(1.01);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.graph-task-node--in-review {
|
|
36
|
+
border-inline-start: calc(var(--btn-border-width) * 3) solid var(--in-review);
|
|
37
|
+
border-start-start-radius: var(--radius-md);
|
|
38
|
+
border-end-start-radius: var(--radius-md);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.graph-task-active-indicator {
|
|
42
|
+
display: flex;
|
|
43
|
+
align-items: center;
|
|
44
|
+
min-height: 1.25rem;
|
|
45
|
+
width: 100%;
|
|
46
|
+
padding-inline: var(--space-sm);
|
|
47
|
+
background: rgba(var(--in-progress-rgb), 0.85);
|
|
48
|
+
border-radius: var(--radius-sm) var(--radius-sm) 0 0;
|
|
49
|
+
animation: graph-task-active-pulse calc(var(--transition-normal) * 8) ease-in-out infinite;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.graph-task-active-indicator-text {
|
|
53
|
+
font-family: var(--font-mono);
|
|
54
|
+
font-size: 0.625rem;
|
|
55
|
+
font-weight: 600;
|
|
56
|
+
line-height: 1;
|
|
57
|
+
letter-spacing: 0.04em;
|
|
58
|
+
text-transform: uppercase;
|
|
59
|
+
color: var(--card);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.graph-task-node .card {
|
|
63
|
+
width: 100%;
|
|
64
|
+
height: 100%;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.graph-task-node[data-current-step="0"] .card-steps-list .card-step-item:nth-child(1),
|
|
68
|
+
.graph-task-node[data-current-step="1"] .card-steps-list .card-step-item:nth-child(2),
|
|
69
|
+
.graph-task-node[data-current-step="2"] .card-steps-list .card-step-item:nth-child(3),
|
|
70
|
+
.graph-task-node[data-current-step="3"] .card-steps-list .card-step-item:nth-child(4),
|
|
71
|
+
.graph-task-node[data-current-step="4"] .card-steps-list .card-step-item:nth-child(5),
|
|
72
|
+
.graph-task-node[data-current-step="5"] .card-steps-list .card-step-item:nth-child(6),
|
|
73
|
+
.graph-task-node[data-current-step="6"] .card-steps-list .card-step-item:nth-child(7),
|
|
74
|
+
.graph-task-node[data-current-step="7"] .card-steps-list .card-step-item:nth-child(8),
|
|
75
|
+
.graph-task-node[data-current-step="8"] .card-steps-list .card-step-item:nth-child(9),
|
|
76
|
+
.graph-task-node[data-current-step="9"] .card-steps-list .card-step-item:nth-child(10),
|
|
77
|
+
.graph-task-node[data-current-step="10"] .card-steps-list .card-step-item:nth-child(11),
|
|
78
|
+
.graph-task-node[data-current-step="11"] .card-steps-list .card-step-item:nth-child(12),
|
|
79
|
+
.graph-task-node[data-current-step="12"] .card-steps-list .card-step-item:nth-child(13),
|
|
80
|
+
.graph-task-node[data-current-step="13"] .card-steps-list .card-step-item:nth-child(14),
|
|
81
|
+
.graph-task-node[data-current-step="14"] .card-steps-list .card-step-item:nth-child(15),
|
|
82
|
+
.graph-task-node[data-current-step="15"] .card-steps-list .card-step-item:nth-child(16),
|
|
83
|
+
.graph-task-node[data-current-step="16"] .card-steps-list .card-step-item:nth-child(17),
|
|
84
|
+
.graph-task-node[data-current-step="17"] .card-steps-list .card-step-item:nth-child(18),
|
|
85
|
+
.graph-task-node[data-current-step="18"] .card-steps-list .card-step-item:nth-child(19),
|
|
86
|
+
.graph-task-node[data-current-step="19"] .card-steps-list .card-step-item:nth-child(20) {
|
|
87
|
+
background: rgba(var(--in-progress-rgb), 0.15);
|
|
88
|
+
border-left: calc(var(--btn-border-width) * 2) solid var(--in-progress);
|
|
89
|
+
padding-left: var(--space-xs);
|
|
90
|
+
font-weight: 500;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.graph-task-node[data-current-step="0"] .card-steps-list .card-step-item:nth-child(1) .card-step-dot,
|
|
94
|
+
.graph-task-node[data-current-step="1"] .card-steps-list .card-step-item:nth-child(2) .card-step-dot,
|
|
95
|
+
.graph-task-node[data-current-step="2"] .card-steps-list .card-step-item:nth-child(3) .card-step-dot,
|
|
96
|
+
.graph-task-node[data-current-step="3"] .card-steps-list .card-step-item:nth-child(4) .card-step-dot,
|
|
97
|
+
.graph-task-node[data-current-step="4"] .card-steps-list .card-step-item:nth-child(5) .card-step-dot,
|
|
98
|
+
.graph-task-node[data-current-step="5"] .card-steps-list .card-step-item:nth-child(6) .card-step-dot,
|
|
99
|
+
.graph-task-node[data-current-step="6"] .card-steps-list .card-step-item:nth-child(7) .card-step-dot,
|
|
100
|
+
.graph-task-node[data-current-step="7"] .card-steps-list .card-step-item:nth-child(8) .card-step-dot,
|
|
101
|
+
.graph-task-node[data-current-step="8"] .card-steps-list .card-step-item:nth-child(9) .card-step-dot,
|
|
102
|
+
.graph-task-node[data-current-step="9"] .card-steps-list .card-step-item:nth-child(10) .card-step-dot,
|
|
103
|
+
.graph-task-node[data-current-step="10"] .card-steps-list .card-step-item:nth-child(11) .card-step-dot,
|
|
104
|
+
.graph-task-node[data-current-step="11"] .card-steps-list .card-step-item:nth-child(12) .card-step-dot,
|
|
105
|
+
.graph-task-node[data-current-step="12"] .card-steps-list .card-step-item:nth-child(13) .card-step-dot,
|
|
106
|
+
.graph-task-node[data-current-step="13"] .card-steps-list .card-step-item:nth-child(14) .card-step-dot,
|
|
107
|
+
.graph-task-node[data-current-step="14"] .card-steps-list .card-step-item:nth-child(15) .card-step-dot,
|
|
108
|
+
.graph-task-node[data-current-step="15"] .card-steps-list .card-step-item:nth-child(16) .card-step-dot,
|
|
109
|
+
.graph-task-node[data-current-step="16"] .card-steps-list .card-step-item:nth-child(17) .card-step-dot,
|
|
110
|
+
.graph-task-node[data-current-step="17"] .card-steps-list .card-step-item:nth-child(18) .card-step-dot,
|
|
111
|
+
.graph-task-node[data-current-step="18"] .card-steps-list .card-step-item:nth-child(19) .card-step-dot,
|
|
112
|
+
.graph-task-node[data-current-step="19"] .card-steps-list .card-step-item:nth-child(20) .card-step-dot {
|
|
113
|
+
width: 0.5rem;
|
|
114
|
+
height: 0.5rem;
|
|
115
|
+
animation: graph-task-step-pulse calc(var(--transition-normal) * 10) ease-in-out infinite;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
@keyframes graph-task-active-pulse {
|
|
119
|
+
0%,
|
|
120
|
+
100% {
|
|
121
|
+
background: rgba(var(--in-progress-rgb), 0.85);
|
|
122
|
+
}
|
|
123
|
+
50% {
|
|
124
|
+
background: rgba(var(--in-progress-rgb), 0.65);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@keyframes graph-task-step-pulse {
|
|
129
|
+
0%,
|
|
130
|
+
100% {
|
|
131
|
+
transform: scale(1);
|
|
132
|
+
opacity: 1;
|
|
133
|
+
}
|
|
134
|
+
50% {
|
|
135
|
+
transform: scale(1.2);
|
|
136
|
+
opacity: 0.75;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
@media (max-width: 768px) {
|
|
141
|
+
.graph-task-node {
|
|
142
|
+
width: min(100%, var(--graph-task-node-mobile-width, calc(var(--space-2xl) * 8)));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.graph-task-node--active {
|
|
146
|
+
transform: scale(1.005);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.graph-task-active-indicator {
|
|
150
|
+
min-height: 1.5rem;
|
|
151
|
+
padding-inline: var(--space-md);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.graph-task-active-indicator-text {
|
|
155
|
+
font-size: 0.6875rem;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { CSSProperties, ComponentProps, HTMLAttributes } from "react";
|
|
2
|
+
import type { GraphPosition } from "./types";
|
|
3
|
+
import { useNodeDrag } from "./hooks/useNodeDrag";
|
|
4
|
+
import { TaskCard } from "@fusion/dashboard/app/components/TaskCard";
|
|
5
|
+
import { isTaskStuck } from "@fusion/dashboard/app/utils/taskStuck";
|
|
6
|
+
import "./GraphTaskNode.css";
|
|
7
|
+
import "./GraphHighlight.css";
|
|
8
|
+
import "./styles/drag.css";
|
|
9
|
+
|
|
10
|
+
type TaskCardComponentProps = ComponentProps<typeof TaskCard>;
|
|
11
|
+
|
|
12
|
+
type TaskCardBridgeProps = Pick<
|
|
13
|
+
TaskCardComponentProps,
|
|
14
|
+
| "task"
|
|
15
|
+
| "projectId"
|
|
16
|
+
| "onOpenDetail"
|
|
17
|
+
| "addToast"
|
|
18
|
+
| "globalPaused"
|
|
19
|
+
| "onUpdateTask"
|
|
20
|
+
| "onArchiveTask"
|
|
21
|
+
| "onUnarchiveTask"
|
|
22
|
+
| "onDeleteTask"
|
|
23
|
+
| "onRetryTask"
|
|
24
|
+
| "onOpenDetailWithTab"
|
|
25
|
+
| "taskStuckTimeoutMs"
|
|
26
|
+
| "onOpenMission"
|
|
27
|
+
| "onMoveTask"
|
|
28
|
+
| "lastFetchTimeMs"
|
|
29
|
+
| "workflowStepNameLookup"
|
|
30
|
+
>;
|
|
31
|
+
|
|
32
|
+
export interface GraphTaskNodeProps extends TaskCardBridgeProps, Pick<HTMLAttributes<HTMLDivElement>, "onMouseEnter" | "onMouseLeave" | "onClick"> {
|
|
33
|
+
style?: CSSProperties;
|
|
34
|
+
position: GraphPosition;
|
|
35
|
+
scale: number;
|
|
36
|
+
isHighlighted?: boolean;
|
|
37
|
+
isDimmed?: boolean;
|
|
38
|
+
onNodePositionChange: (taskId: string, position: GraphPosition) => void;
|
|
39
|
+
onNodeDragStateChange?: (isDragging: boolean) => void;
|
|
40
|
+
onNodeDragEnd?: () => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const ACTIVE_STATUSES = new Set(["planning", "researching", "executing", "finalizing", "merging", "merging-fix"]);
|
|
44
|
+
|
|
45
|
+
function getStatusLabel(status?: string): string {
|
|
46
|
+
if (!status) {
|
|
47
|
+
return "Executing";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return status.charAt(0).toUpperCase() + status.slice(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function GraphTaskNode({
|
|
54
|
+
style,
|
|
55
|
+
position,
|
|
56
|
+
scale,
|
|
57
|
+
isHighlighted = false,
|
|
58
|
+
isDimmed = false,
|
|
59
|
+
onMouseEnter,
|
|
60
|
+
onMouseLeave,
|
|
61
|
+
onClick,
|
|
62
|
+
onNodePositionChange,
|
|
63
|
+
onNodeDragStateChange,
|
|
64
|
+
onNodeDragEnd,
|
|
65
|
+
...taskCardProps
|
|
66
|
+
}: GraphTaskNodeProps) {
|
|
67
|
+
const { task, globalPaused, taskStuckTimeoutMs, lastFetchTimeMs, onOpenDetail } = taskCardProps;
|
|
68
|
+
const isFailed = task.status === "failed";
|
|
69
|
+
const isPaused = task.paused === true;
|
|
70
|
+
const isStuck = isTaskStuck(task, taskStuckTimeoutMs, lastFetchTimeMs);
|
|
71
|
+
const isAwaitingApproval = task.column === "triage" && task.status === "awaiting-approval";
|
|
72
|
+
const isActive =
|
|
73
|
+
!globalPaused &&
|
|
74
|
+
!isFailed &&
|
|
75
|
+
!isPaused &&
|
|
76
|
+
!isStuck &&
|
|
77
|
+
!isAwaitingApproval &&
|
|
78
|
+
(task.column === "in-progress" || ACTIVE_STATUSES.has(task.status as string));
|
|
79
|
+
|
|
80
|
+
const hasValidCurrentStep =
|
|
81
|
+
typeof task.currentStep === "number" &&
|
|
82
|
+
task.currentStep >= 0 &&
|
|
83
|
+
Array.isArray(task.steps) &&
|
|
84
|
+
task.currentStep < task.steps.length;
|
|
85
|
+
const isInReview = task.column === "in-review";
|
|
86
|
+
|
|
87
|
+
const drag = useNodeDrag({
|
|
88
|
+
taskId: task.id,
|
|
89
|
+
position,
|
|
90
|
+
scale,
|
|
91
|
+
onPositionChange: onNodePositionChange,
|
|
92
|
+
onDragStateChange: onNodeDragStateChange,
|
|
93
|
+
onDragEnd: onNodeDragEnd,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<div
|
|
98
|
+
className={`graph-task-node graph-node--draggable${drag.isDragging ? " graph-node--dragging" : ""}${isHighlighted ? " graph-task-node--highlighted graph-node--highlighted" : ""}${isDimmed ? " graph-task-node--dimmed graph-node--dimmed" : ""}${isActive ? " graph-task-node--active" : ""}${isInReview ? " graph-task-node--in-review" : ""}`}
|
|
99
|
+
style={style}
|
|
100
|
+
draggable={false}
|
|
101
|
+
data-testid={`graph-task-node-${task.id}`}
|
|
102
|
+
data-current-step={isActive && hasValidCurrentStep ? String(task.currentStep) : undefined}
|
|
103
|
+
onMouseEnter={onMouseEnter}
|
|
104
|
+
onMouseLeave={onMouseLeave}
|
|
105
|
+
onClick={(event) => {
|
|
106
|
+
onClick?.(event);
|
|
107
|
+
if (event.defaultPrevented) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
onOpenDetail(task);
|
|
111
|
+
}}
|
|
112
|
+
onClickCapture={drag.onClickCapture}
|
|
113
|
+
onPointerDown={drag.onPointerDown}
|
|
114
|
+
onPointerMove={drag.onPointerMove}
|
|
115
|
+
onPointerUp={drag.onPointerUp}
|
|
116
|
+
onPointerCancel={drag.onPointerCancel}
|
|
117
|
+
>
|
|
118
|
+
{isActive ? (
|
|
119
|
+
<div className="graph-task-active-indicator">
|
|
120
|
+
<span className="graph-task-active-indicator-text">{getStatusLabel(task.status)}</span>
|
|
121
|
+
</div>
|
|
122
|
+
) : null}
|
|
123
|
+
<TaskCard {...taskCardProps} onOpenDetail={() => {}} disableDrag={true} />
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
.graph-toolbar {
|
|
2
|
+
position: absolute;
|
|
3
|
+
right: var(--space-md);
|
|
4
|
+
bottom: var(--space-md);
|
|
5
|
+
display: flex;
|
|
6
|
+
flex-direction: column;
|
|
7
|
+
gap: var(--space-xs);
|
|
8
|
+
padding: var(--space-sm);
|
|
9
|
+
background: var(--surface);
|
|
10
|
+
border: var(--btn-border-width) solid var(--border);
|
|
11
|
+
border-radius: var(--radius-md);
|
|
12
|
+
box-shadow: var(--shadow-md);
|
|
13
|
+
z-index: 10;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.graph-toolbar__zoom-label {
|
|
17
|
+
min-width: 3.5ch;
|
|
18
|
+
color: var(--text-muted);
|
|
19
|
+
font-family: var(--font-mono);
|
|
20
|
+
font-size: 0.75rem;
|
|
21
|
+
text-align: center;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@media (max-width: 768px) {
|
|
25
|
+
.graph-toolbar {
|
|
26
|
+
right: var(--space-sm);
|
|
27
|
+
bottom: var(--space-sm);
|
|
28
|
+
padding: var(--space-md);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.graph-toolbar .btn-icon {
|
|
32
|
+
min-width: calc(var(--space-xs) * 11);
|
|
33
|
+
min-height: calc(var(--space-xs) * 11);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Maximize, RotateCcw, ZoomIn, ZoomOut } from "lucide-react";
|
|
2
|
+
import "./GraphToolbar.css";
|
|
3
|
+
|
|
4
|
+
export interface GraphToolbarProps {
|
|
5
|
+
zoom: number;
|
|
6
|
+
onZoomIn: () => void;
|
|
7
|
+
onZoomOut: () => void;
|
|
8
|
+
onFitToGraph: () => void;
|
|
9
|
+
onResetView: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function GraphToolbar({
|
|
13
|
+
zoom,
|
|
14
|
+
onZoomIn,
|
|
15
|
+
onZoomOut,
|
|
16
|
+
onFitToGraph,
|
|
17
|
+
onResetView,
|
|
18
|
+
}: GraphToolbarProps) {
|
|
19
|
+
return (
|
|
20
|
+
<div className="graph-toolbar" data-testid="graph-toolbar">
|
|
21
|
+
<button className="btn btn-icon" title="Zoom in (Ctrl+=)" aria-label="Zoom in" onClick={onZoomIn}>
|
|
22
|
+
<ZoomIn size={16} />
|
|
23
|
+
</button>
|
|
24
|
+
<button className="btn btn-icon" title="Zoom out (Ctrl+-)" aria-label="Zoom out" onClick={onZoomOut}>
|
|
25
|
+
<ZoomOut size={16} />
|
|
26
|
+
</button>
|
|
27
|
+
<div className="graph-toolbar__zoom-label" aria-live="polite">{Math.round(zoom * 100)}%</div>
|
|
28
|
+
<button className="btn btn-icon" title="Fit to graph (Ctrl+Shift+F)" aria-label="Fit to graph" onClick={onFitToGraph}>
|
|
29
|
+
<Maximize size={16} />
|
|
30
|
+
</button>
|
|
31
|
+
<button className="btn btn-icon" title="Reset view (Ctrl+0)" aria-label="Reset view" onClick={onResetView}>
|
|
32
|
+
<RotateCcw size={16} />
|
|
33
|
+
</button>
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
|
3
|
+
import type { Task } from "@fusion/core";
|
|
4
|
+
import { DependencyGraph } from "../DependencyGraph";
|
|
5
|
+
|
|
6
|
+
vi.mock("@fusion/dashboard/app/components/TaskCard", () => ({
|
|
7
|
+
TaskCard: ({ task, onOpenDetail }: { task: Task; onOpenDetail: (task: Task) => void }) => (
|
|
8
|
+
<button data-testid={`task-${task.id}`} onClick={() => onOpenDetail(task)}>{task.id}</button>
|
|
9
|
+
),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
vi.mock("../useGraphInteraction", () => ({
|
|
13
|
+
useGraphInteraction: () => ({
|
|
14
|
+
transform: "translate(0px, 0px) scale(1)",
|
|
15
|
+
zoom: 1,
|
|
16
|
+
transitioning: false,
|
|
17
|
+
zoomIn: vi.fn(),
|
|
18
|
+
zoomOut: vi.fn(),
|
|
19
|
+
resetView: vi.fn(),
|
|
20
|
+
fitToGraph: vi.fn(),
|
|
21
|
+
onPointerDown: vi.fn(),
|
|
22
|
+
onPointerMove: vi.fn(),
|
|
23
|
+
onPointerUp: vi.fn(),
|
|
24
|
+
onWheelZoom: vi.fn(),
|
|
25
|
+
handleKeyDown: vi.fn(),
|
|
26
|
+
}),
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
function createTask(id: string, dependencies: string[] = []): Task {
|
|
30
|
+
return {
|
|
31
|
+
id,
|
|
32
|
+
description: id,
|
|
33
|
+
column: "todo",
|
|
34
|
+
dependencies,
|
|
35
|
+
steps: [],
|
|
36
|
+
currentStep: 0,
|
|
37
|
+
log: [],
|
|
38
|
+
createdAt: new Date().toISOString(),
|
|
39
|
+
updatedAt: new Date().toISOString(),
|
|
40
|
+
} as Task;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
cleanup();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("DependencyGraph highlighting", () => {
|
|
48
|
+
const tasks = [createTask("A"), createTask("B", ["A"]), createTask("C", ["B"]), createTask("D")];
|
|
49
|
+
|
|
50
|
+
it("highlights chain on hover and returns to neutral on mouse leave", () => {
|
|
51
|
+
render(<DependencyGraph tasks={tasks} onOpenDetail={vi.fn()} />);
|
|
52
|
+
|
|
53
|
+
fireEvent.mouseEnter(screen.getByTestId("graph-task-node-C"));
|
|
54
|
+
expect(screen.getByTestId("graph-task-node-A").className).toContain("graph-task-node--highlighted");
|
|
55
|
+
expect(screen.getByTestId("graph-task-node-B").className).toContain("graph-task-node--highlighted");
|
|
56
|
+
expect(screen.getByTestId("graph-task-node-C").className).toContain("graph-task-node--highlighted");
|
|
57
|
+
expect(screen.getByTestId("graph-task-node-D").className).toContain("graph-task-node--dimmed");
|
|
58
|
+
|
|
59
|
+
fireEvent.mouseLeave(screen.getByTestId("graph-task-node-C"));
|
|
60
|
+
expect(screen.getByTestId("graph-task-node-A").className).not.toContain("graph-task-node--highlighted");
|
|
61
|
+
expect(screen.getByTestId("graph-task-node-D").className).not.toContain("graph-task-node--dimmed");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("keeps selection until toggled or pane clicked", () => {
|
|
65
|
+
render(<DependencyGraph tasks={tasks} onOpenDetail={vi.fn()} />);
|
|
66
|
+
|
|
67
|
+
fireEvent.click(screen.getByTestId("graph-task-node-B"));
|
|
68
|
+
expect(screen.getByTestId("graph-task-node-A").className).toContain("graph-task-node--highlighted");
|
|
69
|
+
|
|
70
|
+
fireEvent.click(screen.getByTestId("graph-task-node-B"));
|
|
71
|
+
expect(screen.getByTestId("graph-task-node-A").className).not.toContain("graph-task-node--highlighted");
|
|
72
|
+
|
|
73
|
+
fireEvent.click(screen.getByTestId("graph-task-node-B"));
|
|
74
|
+
fireEvent.click(document.querySelector(".dependency-graph__viewport") as Element);
|
|
75
|
+
expect(screen.getByTestId("graph-task-node-B").className).not.toContain("graph-task-node--highlighted");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("hover overrides selection and reverts when hover leaves", () => {
|
|
79
|
+
render(<DependencyGraph tasks={tasks} onOpenDetail={vi.fn()} />);
|
|
80
|
+
|
|
81
|
+
fireEvent.click(screen.getByTestId("graph-task-node-B"));
|
|
82
|
+
fireEvent.mouseEnter(screen.getByTestId("graph-task-node-D"));
|
|
83
|
+
expect(screen.getByTestId("graph-task-node-D").className).toContain("graph-task-node--highlighted");
|
|
84
|
+
expect(screen.getByTestId("graph-task-node-A").className).toContain("graph-task-node--dimmed");
|
|
85
|
+
|
|
86
|
+
fireEvent.mouseLeave(screen.getByTestId("graph-task-node-D"));
|
|
87
|
+
expect(screen.getByTestId("graph-task-node-A").className).toContain("graph-task-node--highlighted");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("applies edge dimming/highlighting and preserves click-to-detail", () => {
|
|
91
|
+
const onOpenDetail = vi.fn();
|
|
92
|
+
render(<DependencyGraph tasks={tasks} onOpenDetail={onOpenDetail} />);
|
|
93
|
+
|
|
94
|
+
fireEvent.mouseEnter(screen.getByTestId("graph-task-node-C"));
|
|
95
|
+
const edges = screen.getAllByTestId("dependency-edge");
|
|
96
|
+
const edgeAB = edges.find((edge) => edge.getAttribute("data-edge-id") === "B->A");
|
|
97
|
+
const edgeCB = edges.find((edge) => edge.getAttribute("data-edge-id") === "C->B");
|
|
98
|
+
|
|
99
|
+
expect(edgeAB?.className.baseVal || edgeAB?.className).toContain("graph-edge--highlighted");
|
|
100
|
+
expect(edgeCB?.className.baseVal || edgeCB?.className).toContain("graph-edge--highlighted");
|
|
101
|
+
|
|
102
|
+
fireEvent.click(screen.getByTestId("task-C"));
|
|
103
|
+
expect(onOpenDetail).toHaveBeenCalledTimes(1);
|
|
104
|
+
expect(onOpenDetail).toHaveBeenCalledWith(expect.objectContaining({ id: "C" }));
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("highlights only isolated node with no dependencies", () => {
|
|
108
|
+
render(<DependencyGraph tasks={[createTask("X")]} onOpenDetail={vi.fn()} />);
|
|
109
|
+
fireEvent.mouseEnter(screen.getByTestId("graph-task-node-X"));
|
|
110
|
+
expect(screen.getByTestId("graph-task-node-X").className).toContain("graph-task-node--highlighted");
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
|
3
|
+
import type { Task } from "@fusion/core";
|
|
4
|
+
import { DependencyGraph } from "../DependencyGraph";
|
|
5
|
+
|
|
6
|
+
const fitToGraph = vi.fn();
|
|
7
|
+
|
|
8
|
+
vi.mock("@fusion/dashboard/app/components/TaskCard", () => ({
|
|
9
|
+
TaskCard: ({ task, onOpenDetail }: { task: Task; onOpenDetail: (task: Task) => void }) => (
|
|
10
|
+
<button data-testid={`task-${task.id}`} onClick={() => onOpenDetail(task)}>{task.id}</button>
|
|
11
|
+
),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
vi.mock("../useGraphInteraction", () => ({
|
|
15
|
+
useGraphInteraction: () => ({
|
|
16
|
+
transform: "translate(0px, 0px) scale(1)",
|
|
17
|
+
zoom: 1,
|
|
18
|
+
transitioning: false,
|
|
19
|
+
zoomIn: vi.fn(),
|
|
20
|
+
zoomOut: vi.fn(),
|
|
21
|
+
resetView: vi.fn(),
|
|
22
|
+
fitToGraph,
|
|
23
|
+
onPointerDown: vi.fn(),
|
|
24
|
+
onPointerMove: vi.fn(),
|
|
25
|
+
onPointerUp: vi.fn(),
|
|
26
|
+
onWheelZoom: vi.fn(),
|
|
27
|
+
handleKeyDown: vi.fn(),
|
|
28
|
+
}),
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
vi.mock("../layout", () => ({
|
|
32
|
+
computeAutoLayout: ({ nodes }: { nodes: Array<{ task: { id: string } }> }) => {
|
|
33
|
+
const map = new Map<string, { x: number; y: number }>();
|
|
34
|
+
for (const node of nodes) {
|
|
35
|
+
if (node.task.id === "A") map.set("A", { x: 0, y: 0 });
|
|
36
|
+
if (node.task.id === "B") map.set("B", { x: 200, y: 0 });
|
|
37
|
+
}
|
|
38
|
+
return map;
|
|
39
|
+
},
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
function createStorage() {
|
|
43
|
+
const store = new Map<string, string>();
|
|
44
|
+
return {
|
|
45
|
+
getItem: (key: string) => store.get(key) ?? null,
|
|
46
|
+
setItem: (key: string, value: string) => {
|
|
47
|
+
store.set(key, value);
|
|
48
|
+
},
|
|
49
|
+
removeItem: (key: string) => {
|
|
50
|
+
store.delete(key);
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function createTask(id: string, column: Task["column"] = "todo"): Task {
|
|
56
|
+
return { id, description: id, column, dependencies: [], steps: [], currentStep: 0, log: [] } as Task;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
describe("DependencyGraph persistence", () => {
|
|
60
|
+
afterEach(() => {
|
|
61
|
+
cleanup();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
beforeEach(() => {
|
|
65
|
+
Object.defineProperty(window, "localStorage", { value: createStorage(), configurable: true });
|
|
66
|
+
fitToGraph.mockReset();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("persists dragged node position across remount", () => {
|
|
70
|
+
const { unmount } = render(<DependencyGraph tasks={[createTask("A")]} projectId="p1" />);
|
|
71
|
+
|
|
72
|
+
const node = screen.getByTestId("graph-task-node-A");
|
|
73
|
+
fireEvent.pointerDown(node, { pointerId: 1, isPrimary: true, clientX: 10, clientY: 10 });
|
|
74
|
+
fireEvent.pointerMove(node, { pointerId: 1, isPrimary: true, clientX: 30, clientY: 40 });
|
|
75
|
+
fireEvent.pointerUp(node, { pointerId: 1, isPrimary: true, clientX: 30, clientY: 40 });
|
|
76
|
+
|
|
77
|
+
expect(window.localStorage.getItem("kb:p1:fusion-plugin-dependency-graph:positions")).toContain('"A":{"x":20,"y":30}');
|
|
78
|
+
|
|
79
|
+
unmount();
|
|
80
|
+
render(<DependencyGraph tasks={[createTask("A")]} projectId="p1" />);
|
|
81
|
+
|
|
82
|
+
expect(screen.getByTestId("graph-task-node-A").getAttribute("style")).toContain("left: 20px");
|
|
83
|
+
expect(screen.getByTestId("graph-task-node-A").getAttribute("style")).toContain("top: 30px");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("merges saved positions with auto-layout for new tasks", () => {
|
|
87
|
+
window.localStorage.setItem("kb:p1:fusion-plugin-dependency-graph:positions", JSON.stringify({ A: { x: 25, y: 35 } }));
|
|
88
|
+
|
|
89
|
+
render(<DependencyGraph tasks={[createTask("A"), createTask("B")]} projectId="p1" />);
|
|
90
|
+
|
|
91
|
+
expect(screen.getByTestId("graph-task-node-A").getAttribute("style")).toContain("left: 25px");
|
|
92
|
+
expect(screen.getByTestId("graph-task-node-B").getAttribute("style")).toContain("left: 200px");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("fit to graph clears saved positions and reapplies auto-layout", () => {
|
|
96
|
+
window.localStorage.setItem("kb:p1:fusion-plugin-dependency-graph:positions", JSON.stringify({ A: { x: 25, y: 35 } }));
|
|
97
|
+
|
|
98
|
+
render(<DependencyGraph tasks={[createTask("A")]} projectId="p1" />);
|
|
99
|
+
fireEvent.click(screen.getByRole("button", { name: "Fit to graph" }));
|
|
100
|
+
|
|
101
|
+
expect(window.localStorage.getItem("kb:p1:fusion-plugin-dependency-graph:positions")).toBeNull();
|
|
102
|
+
expect(screen.getByTestId("graph-task-node-A").getAttribute("style")).toContain("left: 0px");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("switching projects loads project-scoped positions", () => {
|
|
106
|
+
window.localStorage.setItem("kb:p1:fusion-plugin-dependency-graph:positions", JSON.stringify({ A: { x: 11, y: 22 } }));
|
|
107
|
+
window.localStorage.setItem("kb:p2:fusion-plugin-dependency-graph:positions", JSON.stringify({ A: { x: 33, y: 44 } }));
|
|
108
|
+
|
|
109
|
+
const { rerender } = render(<DependencyGraph tasks={[createTask("A")]} projectId="p1" />);
|
|
110
|
+
expect(screen.getByTestId("graph-task-node-A").getAttribute("style")).toContain("left: 11px");
|
|
111
|
+
|
|
112
|
+
rerender(<DependencyGraph tasks={[createTask("A")]} projectId="p2" />);
|
|
113
|
+
expect(screen.getByTestId("graph-task-node-A").getAttribute("style")).toContain("left: 33px");
|
|
114
|
+
});
|
|
115
|
+
});
|