@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aicupa/plugin-todo-dependency",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Set and visualize dependencies between todo items",
5
5
  "description_zh": "设置并可视化待办事项之间的依赖关系",
6
6
  "main": "./service",
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 (depIds.length > 0) {
61
- node.todo.depIds = 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 { position: relative; min-width: 100%; min-height: 100%; }
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
- height: 100%; color: #aaa; padding: 40px 24px; text-align: center; user-select: none;
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
- <div class="empty-state" id="emptyState">
132
- <svg class="empty-icon" viewBox="0 0 80 80" fill="none" stroke="currentColor" stroke-width="2">
133
- <rect x="8" y="10" width="24" height="16" rx="4"/>
134
- <rect x="48" y="10" width="24" height="16" rx="4"/>
135
- <rect x="28" y="50" width="24" height="16" rx="4"/>
136
- <line x1="26" y1="26" x2="36" y2="50" stroke-dasharray="3,2"/>
137
- <line x1="54" y1="26" x2="44" y2="50" stroke-dasharray="3,2"/>
138
- <polygon points="36,50 33,45 39,45" fill="currentColor" stroke="none"/>
139
- <polygon points="44,50 41,45 47,45" fill="currentColor" stroke="none"/>
140
- </svg>
141
- <div class="empty-title" data-i18n="emptyTitle"></div>
142
- <div class="empty-hint" data-i18n="emptyHint"></div>
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
- const nodeIds = Object.keys(todos).map(Number)
246
- if (!nodeIds.length) { showEmpty(); return }
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
- const c = document.getElementById('graphContainer')
298
+ function showEmpty(allDone) {
299
+ document.getElementById('graphContainer').innerHTML = ''
258
300
  const e = document.getElementById('emptyState')
259
- c.innerHTML = ''; c.style.width = ''; c.style.height = ''
260
- c.appendChild(e); e.style.display = 'flex'
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
- container.style.width = layout.width + 'px'
327
- container.style.height = layout.height + 'px'
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
- container.appendChild(svg)
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); container.appendChild(el)
491
+ el.appendChild(body); inner.appendChild(el)
435
492
  }
436
493
  }
437
494