@qnote/q-ai-note 1.0.9 → 1.0.11

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/web/app.js CHANGED
@@ -45,6 +45,7 @@ const state = {
45
45
  changesSandboxFilter: '',
46
46
  changesTypeFilter: 'all',
47
47
  changesQuickFilter: 'all',
48
+ workTreeExpandInitialized: false,
48
49
  };
49
50
 
50
51
  const expandedNodes = new Set();
@@ -331,6 +332,91 @@ function expandAllNodes() {
331
332
  items.forEach(item => expandedNodes.add(item.id));
332
333
  }
333
334
 
335
+ function buildWorkItemDepthMap(items) {
336
+ const byId = new Map(items.map((item) => [item.id, item]));
337
+ const memo = new Map();
338
+ const visiting = new Set();
339
+ const walk = (id) => {
340
+ if (memo.has(id)) return memo.get(id);
341
+ if (visiting.has(id)) return 0;
342
+ visiting.add(id);
343
+ const item = byId.get(id);
344
+ if (!item) {
345
+ visiting.delete(id);
346
+ return 0;
347
+ }
348
+ const parentId = item.parent_id;
349
+ const depth = parentId && byId.has(parentId) ? walk(parentId) + 1 : 0;
350
+ memo.set(id, depth);
351
+ visiting.delete(id);
352
+ return depth;
353
+ };
354
+ items.forEach((item) => walk(item.id));
355
+ return memo;
356
+ }
357
+
358
+ function getWorkTreeMaxDepth(items) {
359
+ const depthMap = buildWorkItemDepthMap(items || []);
360
+ let maxDepth = 0;
361
+ depthMap.forEach((depth) => {
362
+ if (Number.isFinite(depth)) maxDepth = Math.max(maxDepth, Number(depth));
363
+ });
364
+ return maxDepth;
365
+ }
366
+
367
+ function expandNodesToLevel(level, items) {
368
+ const safeLevel = Math.max(1, Number(level) || 1);
369
+ const depthMap = buildWorkItemDepthMap(items || []);
370
+ expandedNodes.clear();
371
+ (items || []).forEach((item) => {
372
+ const depth = Number(depthMap.get(item.id) || 0);
373
+ if (depth < safeLevel) expandedNodes.add(item.id);
374
+ });
375
+ }
376
+
377
+ function applyWorkTreeCollapseAction(action) {
378
+ if (!state.currentSandbox) return;
379
+ const items = state.currentSandbox.items || [];
380
+ if (!items.length) return;
381
+ if (action === 'expand-all') {
382
+ expandedNodes.clear();
383
+ expandAllNodes();
384
+ } else if (action === 'collapse-all') {
385
+ expandedNodes.clear();
386
+ } else if (String(action || '').startsWith('collapse-level:')) {
387
+ const level = Number(String(action).split(':')[1] || '1');
388
+ expandNodesToLevel(level, items);
389
+ }
390
+ state.workTreeExpandInitialized = true;
391
+ }
392
+
393
+ function updateWorkTreeCollapseMenu(items) {
394
+ const selector = document.getElementById('work-tree-collapse-action');
395
+ if (!(selector instanceof HTMLSelectElement)) return;
396
+ const denseMode = state.workTreeViewMode === 'dense';
397
+ const hasItems = (items || []).length > 0;
398
+ selector.classList.toggle('hidden', !denseMode);
399
+ selector.disabled = !denseMode || !hasItems;
400
+ if (!denseMode || !hasItems) {
401
+ selector.innerHTML = '<option value="">层级操作</option>';
402
+ selector.value = '';
403
+ return;
404
+ }
405
+ const maxDepth = getWorkTreeMaxDepth(items);
406
+ const maxLevel = Math.max(1, maxDepth + 1);
407
+ const levelOptions = Array.from({ length: maxLevel }, (_v, idx) => {
408
+ const level = idx + 1;
409
+ return `<option value="collapse-level:${level}">折叠到 L${level}</option>`;
410
+ }).join('');
411
+ selector.innerHTML = `
412
+ <option value="">层级操作</option>
413
+ <option value="expand-all">全部展开</option>
414
+ <option value="collapse-all">全部折叠</option>
415
+ ${levelOptions}
416
+ `;
417
+ selector.value = '';
418
+ }
419
+
334
420
  function getRootNodeIds(items) {
335
421
  return items.filter((item) => !item.parent_id).map((item) => item.id);
336
422
  }
@@ -348,6 +434,8 @@ function applyWorkTreeViewMode(mode) {
348
434
  } else {
349
435
  expandAllNodes();
350
436
  }
437
+ state.workTreeExpandInitialized = true;
438
+ updateWorkTreeCollapseMenu(items);
351
439
  }
352
440
 
353
441
  function applyWorkItemAssigneeToggle() {
@@ -374,13 +462,15 @@ function renderWorkTree() {
374
462
  const entitySummaryByNodeId = buildNodeEntitySummaryByNodeId();
375
463
  const entityRowsByNodeId = buildNodeEntityRowsByNodeId();
376
464
 
377
- if (expandedNodes.size === 0 && allItems.length > 0) {
465
+ if (!state.workTreeExpandInitialized && allItems.length > 0) {
378
466
  if (state.workTreeViewMode === 'report') {
379
467
  getRootNodeIds(allItems).forEach((id) => expandedNodes.add(id));
380
468
  } else {
381
469
  expandAllNodes();
382
470
  }
471
+ state.workTreeExpandInitialized = true;
383
472
  }
473
+ updateWorkTreeCollapseMenu(allItems);
384
474
 
385
475
  if (allItems.length === 0) {
386
476
  tree.innerHTML = `<div class="empty-state"><p>${treeReadonly ? '暂无任务' : '点击上方"添加"按钮创建第一个任务'}</p></div>`;
@@ -1453,6 +1543,7 @@ async function loadSandbox(id) {
1453
1543
  apiRequest(`${API_BASE}/sandboxes/${id}/entities/stats`),
1454
1544
  ]);
1455
1545
  state.currentSandbox = { ...sandbox, items, diaries };
1546
+ state.workTreeExpandInitialized = false;
1456
1547
  state.nodeEntities = entities;
1457
1548
  state.nodeEntityStats = entityStats;
1458
1549
 
@@ -3012,6 +3103,13 @@ async function initApp() {
3012
3103
  renderWorkTree();
3013
3104
  });
3014
3105
 
3106
+ document.getElementById('work-tree-collapse-action')?.addEventListener('change', (e) => {
3107
+ const action = e.target.value || '';
3108
+ if (!action) return;
3109
+ applyWorkTreeCollapseAction(action);
3110
+ renderWorkTree();
3111
+ });
3112
+
3015
3113
  document.getElementById('work-item-element-preview-mode')?.addEventListener('change', (e) => {
3016
3114
  state.workItemElementPreviewMode = e.target.value || 'none';
3017
3115
  renderWorkTree();
@@ -63,6 +63,11 @@
63
63
  <option value="report">汇报折叠模式</option>
64
64
  <option value="dense">树模式</option>
65
65
  </select>
66
+ <select id="work-tree-collapse-action" title="树模式层级操作">
67
+ <option value="">层级操作</option>
68
+ <option value="expand-all">全部展开</option>
69
+ <option value="collapse-all">全部折叠</option>
70
+ </select>
66
71
  <select id="work-item-element-preview-mode">
67
72
  <option value="none">能力要素:不显示</option>
68
73
  <option value="issue">能力要素:Issue</option>
@@ -221,6 +221,10 @@ h2 {
221
221
  gap: 8px;
222
222
  }
223
223
 
224
+ #work-tree-collapse-action {
225
+ min-width: 132px;
226
+ }
227
+
224
228
  .inline-checkbox {
225
229
  display: inline-flex;
226
230
  align-items: center;
@@ -770,21 +774,26 @@ h2 {
770
774
  }
771
775
 
772
776
  .lane-tree-title-line {
773
- display: inline-flex;
777
+ display: flex;
778
+ flex-wrap: wrap;
774
779
  align-items: center;
775
780
  gap: 4px;
776
781
  min-width: 0;
777
782
  }
778
783
 
779
- .lane-tree-summary-line {
780
- display: inline-flex;
781
- align-items: center;
782
- gap: 4px;
783
- min-height: 14px;
784
+ .lane-tree-title-line .dense-node-name {
785
+ flex: 1 1 auto;
786
+ min-width: 96px;
787
+ }
788
+
789
+ .lane-tree-title-line .node-entity-mini-badges,
790
+ .lane-tree-title-line .node-meta {
791
+ flex: 0 0 auto;
792
+ max-width: 100%;
784
793
  }
785
794
 
786
- .lane-tree-node-main.stack-summary .dense-node-name {
787
- max-width: calc(var(--lane-name-max, 154px) + 28px);
795
+ .lane-tree-title-line .node-meta {
796
+ white-space: nowrap;
788
797
  }
789
798
 
790
799
  .lane-tree-node {
@@ -318,7 +318,6 @@ function renderDenseLaneNode(node, byParent, expandedIdSet, entitySummaryByNodeI
318
318
  const hasChildren = children.length > 0;
319
319
  const isExpanded = expandedIdSet.has(node.id);
320
320
  const nodeSummary = entitySummaryByNodeId?.[node.id] || { issue: 0, knowledge: 0, capability: 0 };
321
- const useStackSummary = depth >= 3;
322
321
  const summaryBadgeHtml = renderEntityBadges(nodeSummary);
323
322
  const summaryAssigneeHtml = showAssignee && node.assignee ? `<span class="node-meta">@${esc(node.assignee)}</span>` : '';
324
323
  const previewHtml = renderEntityPreviewBoxes(node.id, entityRowsByNodeId, elementPreviewMode);
@@ -342,18 +341,12 @@ function renderDenseLaneNode(node, byParent, expandedIdSet, entitySummaryByNodeI
342
341
  </button>
343
342
  ` : ''}
344
343
  <span class="node-status ${esc(node.status)}"></span>
345
- <div class="lane-tree-node-main ${useStackSummary ? 'stack-summary' : ''}">
344
+ <div class="lane-tree-node-main">
346
345
  <div class="lane-tree-title-line">
347
346
  <span class="dense-node-name">${esc(node.name)}</span>
348
- ${!useStackSummary ? summaryBadgeHtml : ''}
349
- ${!useStackSummary ? summaryAssigneeHtml : ''}
347
+ ${summaryBadgeHtml}
348
+ ${summaryAssigneeHtml}
350
349
  </div>
351
- ${useStackSummary && (summaryBadgeHtml || summaryAssigneeHtml) ? `
352
- <div class="lane-tree-summary-line">
353
- ${summaryBadgeHtml}
354
- ${summaryAssigneeHtml}
355
- </div>
356
- ` : ''}
357
350
  </div>
358
351
  ${readonly ? '' : `
359
352
  <div class="node-actions">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qnote/q-ai-note",
3
- "version": "1.0.9",
3
+ "version": "1.0.11",
4
4
  "type": "module",
5
5
  "description": "AI-assisted personal work sandbox and diary system",
6
6
  "main": "dist/server/index.js",