@aicupa/plugin-todo-dependency 1.0.3 → 1.0.5
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/package.json +1 -1
- package/service.js +7 -2
- package/view/index.html +84 -27
package/package.json
CHANGED
package/service.js
CHANGED
|
@@ -54,11 +54,16 @@ module.exports = (api) => {
|
|
|
54
54
|
const data = JSON.parse(content)
|
|
55
55
|
const todotree = data.todotree
|
|
56
56
|
|
|
57
|
+
const allTodos = []
|
|
58
|
+
flattenTodos(todotree.tree, allTodos)
|
|
59
|
+
const existingIds = new Set(allTodos.map(t => t.id))
|
|
60
|
+
const validDepIds = depIds.filter(id => existingIds.has(id))
|
|
61
|
+
|
|
57
62
|
function findAndUpdate(nodes) {
|
|
58
63
|
for (const node of nodes) {
|
|
59
64
|
if (node.todo && node.todo.id === todoId) {
|
|
60
|
-
if (
|
|
61
|
-
node.todo.depIds =
|
|
65
|
+
if (validDepIds.length > 0) {
|
|
66
|
+
node.todo.depIds = validDepIds
|
|
62
67
|
} else {
|
|
63
68
|
delete node.todo.depIds
|
|
64
69
|
}
|
package/view/index.html
CHANGED
|
@@ -43,7 +43,8 @@
|
|
|
43
43
|
|
|
44
44
|
#graphArea { flex: 1; overflow: auto; position: relative; background: #f7f8fa; }
|
|
45
45
|
.dark #graphArea { background: #1e1e1e; }
|
|
46
|
-
#graphContainer {
|
|
46
|
+
#graphContainer { min-width: 100%; min-height: 100%; display: flex; align-items: center; justify-content: center; }
|
|
47
|
+
#graphInner { position: relative; flex-shrink: 0; }
|
|
47
48
|
|
|
48
49
|
.node {
|
|
49
50
|
position: absolute; width: 200px; background: #fff;
|
|
@@ -108,10 +109,16 @@
|
|
|
108
109
|
|
|
109
110
|
#edgeSvg { position: absolute; top: 0; left: 0; pointer-events: none; }
|
|
110
111
|
|
|
112
|
+
#graphInner { transition: opacity 0.35s ease; }
|
|
113
|
+
#graphInner.fade-out { opacity: 0; }
|
|
114
|
+
|
|
111
115
|
.empty-state {
|
|
112
116
|
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
|
113
|
-
|
|
117
|
+
position: absolute; inset: 0;
|
|
118
|
+
color: #aaa; padding: 40px 24px; text-align: center; user-select: none;
|
|
119
|
+
opacity: 0; pointer-events: none; transition: opacity 0.35s ease;
|
|
114
120
|
}
|
|
121
|
+
.empty-state.visible { opacity: 1; pointer-events: auto; }
|
|
115
122
|
.empty-icon { width: 64px; height: 64px; margin-bottom: 16px; opacity: 0.35; }
|
|
116
123
|
.empty-title { font-size: 15px; font-weight: 500; margin-bottom: 10px; color: #888; }
|
|
117
124
|
.dark .empty-title { color: #777; }
|
|
@@ -127,20 +134,19 @@
|
|
|
127
134
|
</div>
|
|
128
135
|
<div id="warnings"></div>
|
|
129
136
|
<div id="graphArea">
|
|
130
|
-
<div id="graphContainer">
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
</div>
|
|
137
|
+
<div id="graphContainer"></div>
|
|
138
|
+
<div class="empty-state visible" id="emptyState">
|
|
139
|
+
<svg class="empty-icon" viewBox="0 0 80 80" fill="none" stroke="currentColor" stroke-width="2">
|
|
140
|
+
<rect x="8" y="10" width="24" height="16" rx="4"/>
|
|
141
|
+
<rect x="48" y="10" width="24" height="16" rx="4"/>
|
|
142
|
+
<rect x="28" y="50" width="24" height="16" rx="4"/>
|
|
143
|
+
<line x1="26" y1="26" x2="36" y2="50" stroke-dasharray="3,2"/>
|
|
144
|
+
<line x1="54" y1="26" x2="44" y2="50" stroke-dasharray="3,2"/>
|
|
145
|
+
<polygon points="36,50 33,45 39,45" fill="currentColor" stroke="none"/>
|
|
146
|
+
<polygon points="44,50 41,45 47,45" fill="currentColor" stroke="none"/>
|
|
147
|
+
</svg>
|
|
148
|
+
<div class="empty-title" data-i18n="emptyTitle"></div>
|
|
149
|
+
<div class="empty-hint" data-i18n="emptyHint"></div>
|
|
144
150
|
</div>
|
|
145
151
|
</div>
|
|
146
152
|
</div>
|
|
@@ -152,6 +158,8 @@
|
|
|
152
158
|
title: '依赖图谱', refresh: '刷新',
|
|
153
159
|
emptyTitle: '暂无依赖关系',
|
|
154
160
|
emptyHint: '右键 Todo 节点,选择「设置依赖」<br>即可建立待办之间的依赖关系',
|
|
161
|
+
allDoneTitle: '所有依赖任务已完成 🎉',
|
|
162
|
+
allDoneHint: '当前依赖链路中的任务均已完成<br>如有新的依赖关系,请重新创建',
|
|
155
163
|
done: '已完成', pending: '待完成', blocked: '阻塞中', notFound: '未找到该 Todo',
|
|
156
164
|
cycleWarning: '检测到循环依赖,部分节点的层级可能不准确',
|
|
157
165
|
},
|
|
@@ -159,6 +167,8 @@
|
|
|
159
167
|
title: 'Dependency Graph', refresh: 'Refresh',
|
|
160
168
|
emptyTitle: 'No dependencies yet',
|
|
161
169
|
emptyHint: 'Right-click a Todo and select "Set Dependencies"<br>to create dependency relationships',
|
|
170
|
+
allDoneTitle: 'All dependency tasks completed 🎉',
|
|
171
|
+
allDoneHint: 'All tasks in dependency chains are done<br>Create new dependencies if needed',
|
|
162
172
|
done: 'Done', pending: 'Pending', blocked: 'Blocked', notFound: 'Todo not found',
|
|
163
173
|
cycleWarning: 'Cycle detected — some nodes may be positioned incorrectly',
|
|
164
174
|
},
|
|
@@ -241,9 +251,41 @@
|
|
|
241
251
|
} catch { return }
|
|
242
252
|
if (!data) return
|
|
243
253
|
|
|
244
|
-
const { todos, edges } = data
|
|
245
|
-
|
|
246
|
-
|
|
254
|
+
const { todos, edges: allEdges } = data
|
|
255
|
+
hideEmpty()
|
|
256
|
+
|
|
257
|
+
// Build adjacency (both directions) to find connected components
|
|
258
|
+
const allIds = new Set()
|
|
259
|
+
const neighbors = {}
|
|
260
|
+
for (const [from, to] of allEdges) {
|
|
261
|
+
allIds.add(from); allIds.add(to)
|
|
262
|
+
if (!neighbors[from]) neighbors[from] = []
|
|
263
|
+
if (!neighbors[to]) neighbors[to] = []
|
|
264
|
+
neighbors[from].push(to)
|
|
265
|
+
neighbors[to].push(from)
|
|
266
|
+
}
|
|
267
|
+
// BFS to find connected components, keep components that have any undone node
|
|
268
|
+
const visited = new Set()
|
|
269
|
+
const visibleIds = new Set()
|
|
270
|
+
for (const start of allIds) {
|
|
271
|
+
if (visited.has(start)) continue
|
|
272
|
+
const comp = []
|
|
273
|
+
const queue = [start]
|
|
274
|
+
visited.add(start)
|
|
275
|
+
while (queue.length) {
|
|
276
|
+
const cur = queue.shift()
|
|
277
|
+
comp.push(cur)
|
|
278
|
+
for (const nb of (neighbors[cur] || [])) {
|
|
279
|
+
if (!visited.has(nb)) { visited.add(nb); queue.push(nb) }
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (comp.some(id => !todos[id]?.done)) {
|
|
283
|
+
for (const id of comp) visibleIds.add(id)
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
const edges = allEdges.filter(([from, to]) => visibleIds.has(from) && visibleIds.has(to))
|
|
287
|
+
const nodeIds = [...visibleIds]
|
|
288
|
+
if (!nodeIds.length) { fadeToEmpty(allEdges.length > 0); return }
|
|
247
289
|
|
|
248
290
|
const { layers, hasCycle } = assignLayers(nodeIds, edges)
|
|
249
291
|
if (hasCycle) {
|
|
@@ -253,11 +295,23 @@
|
|
|
253
295
|
renderGraph(nodeIds, edges, layout.positions, todos, layout)
|
|
254
296
|
}
|
|
255
297
|
|
|
256
|
-
function showEmpty() {
|
|
257
|
-
|
|
298
|
+
function showEmpty(allDone) {
|
|
299
|
+
document.getElementById('graphContainer').innerHTML = ''
|
|
258
300
|
const e = document.getElementById('emptyState')
|
|
259
|
-
|
|
260
|
-
|
|
301
|
+
e.querySelector('.empty-title').innerHTML = allDone ? t.allDoneTitle : t.emptyTitle
|
|
302
|
+
e.querySelector('.empty-hint').innerHTML = allDone ? t.allDoneHint : t.emptyHint
|
|
303
|
+
e.classList.add('visible')
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function hideEmpty() {
|
|
307
|
+
document.getElementById('emptyState').classList.remove('visible')
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function fadeToEmpty(allDone) {
|
|
311
|
+
const inner = document.getElementById('graphInner')
|
|
312
|
+
if (!inner) { showEmpty(allDone); return }
|
|
313
|
+
inner.classList.add('fade-out')
|
|
314
|
+
inner.addEventListener('transitionend', () => showEmpty(allDone), { once: true })
|
|
261
315
|
}
|
|
262
316
|
|
|
263
317
|
// ── Layout algorithm ──
|
|
@@ -323,8 +377,11 @@
|
|
|
323
377
|
function renderGraph(nodeIds, edges, positions, todoData, layout) {
|
|
324
378
|
const container = document.getElementById('graphContainer')
|
|
325
379
|
container.innerHTML = ''
|
|
326
|
-
|
|
327
|
-
|
|
380
|
+
const inner = document.createElement('div')
|
|
381
|
+
inner.id = 'graphInner'
|
|
382
|
+
inner.style.width = layout.width + 'px'
|
|
383
|
+
inner.style.height = layout.height + 'px'
|
|
384
|
+
container.appendChild(inner)
|
|
328
385
|
|
|
329
386
|
// Build dependency map: nodeId -> [depIds]
|
|
330
387
|
const depsOf = {}
|
|
@@ -382,7 +439,7 @@
|
|
|
382
439
|
'marker-end': isBlockingEdge ? 'url(#arrow-blocked)' : 'url(#arrow)',
|
|
383
440
|
}))
|
|
384
441
|
}
|
|
385
|
-
|
|
442
|
+
inner.appendChild(svg)
|
|
386
443
|
|
|
387
444
|
for (const id of nodeIds) {
|
|
388
445
|
const pos = positions[id]; if (!pos) continue
|
|
@@ -431,7 +488,7 @@
|
|
|
431
488
|
body.appendChild(sb)
|
|
432
489
|
}
|
|
433
490
|
|
|
434
|
-
el.appendChild(body);
|
|
491
|
+
el.appendChild(body); inner.appendChild(el)
|
|
435
492
|
}
|
|
436
493
|
}
|
|
437
494
|
|