@qnote/q-ai-note 1.0.20 → 1.0.22

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
@@ -1157,6 +1157,81 @@ function renderWorkTree() {
1157
1157
  });
1158
1158
  await loadSandbox(state.currentSandbox.id);
1159
1159
  },
1160
+ onMoveSibling: treeReadonly ? undefined : async (nodeId, direction) => {
1161
+ if (!state.currentSandbox) return;
1162
+ const delta = direction === 'up' ? -1 : direction === 'down' ? 1 : 0;
1163
+ if (!delta) return;
1164
+ const byId = new Map((state.currentSandbox.items || []).map((item) => [item.id, item]));
1165
+ const node = byId.get(nodeId);
1166
+ if (!node) return;
1167
+ if (!node.parent_id) {
1168
+ if (state.laneReorderPending) return;
1169
+ const roots = (state.currentSandbox.items || []).filter((item) => !item.parent_id);
1170
+ const orderedRoots = [...roots].sort((a, b) => compareSiblingOrder(a, b, 'lane_order_key'));
1171
+ const fromIdx = orderedRoots.findIndex((item) => item.id === nodeId);
1172
+ if (fromIdx < 0) return;
1173
+ const toIdx = fromIdx + delta;
1174
+ if (toIdx < 0 || toIdx >= orderedRoots.length) return;
1175
+ const [movedRoot] = orderedRoots.splice(fromIdx, 1);
1176
+ orderedRoots.splice(toIdx, 0, movedRoot);
1177
+ const rootIds = orderedRoots.map((item) => item.id);
1178
+ const previousRootIds = roots
1179
+ .sort((a, b) => compareSiblingOrder(a, b, 'lane_order_key'))
1180
+ .map((item) => item.id);
1181
+ if (JSON.stringify(rootIds) === JSON.stringify(previousRootIds)) return;
1182
+ state.laneReorderPending = true;
1183
+ renderWorkTree();
1184
+ try {
1185
+ const result = await apiRequest(`${API_BASE}/sandboxes/${state.currentSandbox.id}/items/reorder-roots`, {
1186
+ method: 'POST',
1187
+ body: JSON.stringify({ root_ids: rootIds }),
1188
+ });
1189
+ const hasChangedCount = Object.prototype.hasOwnProperty.call(result || {}, 'changed_count');
1190
+ if (hasChangedCount && Number(result?.changed_count || 0) === 0) {
1191
+ state.laneReorderPending = false;
1192
+ renderWorkTree();
1193
+ return;
1194
+ }
1195
+ await loadSandbox(state.currentSandbox.id);
1196
+ } catch (error) {
1197
+ alert(`泳道排序失败:${error?.message || error}`);
1198
+ } finally {
1199
+ state.laneReorderPending = false;
1200
+ renderWorkTree();
1201
+ }
1202
+ return;
1203
+ }
1204
+ const parentId = node.parent_id || null;
1205
+ const siblings = (state.currentSandbox.items || [])
1206
+ .filter((item) => (item.parent_id || null) === parentId)
1207
+ .sort((a, b) => compareSiblingOrder(a, b, 'order_key'));
1208
+ const fromIdx = siblings.findIndex((item) => item.id === nodeId);
1209
+ if (fromIdx < 0) return;
1210
+ const toIdx = fromIdx + delta;
1211
+ if (toIdx < 0 || toIdx >= siblings.length) return;
1212
+ const reordered = [...siblings];
1213
+ const [movedNode] = reordered.splice(fromIdx, 1);
1214
+ reordered.splice(toIdx, 0, movedNode);
1215
+ const left = reordered[toIdx - 1] || null;
1216
+ const right = reordered[toIdx + 1] || null;
1217
+ const nextOrderKey = rankBetween(getNodeOrderKey(left, 'order_key'), getNodeOrderKey(right, 'order_key'));
1218
+ const canDirectUpdate = nextOrderKey !== getNodeOrderKey(node, 'order_key')
1219
+ && isOrderKeyBetween(getNodeOrderKey(left, 'order_key'), getNodeOrderKey(right, 'order_key'), nextOrderKey);
1220
+ if (canDirectUpdate) {
1221
+ await apiRequest(`${API_BASE}/items/${nodeId}`, {
1222
+ method: 'PUT',
1223
+ body: JSON.stringify({
1224
+ extra_data: {
1225
+ ...(node.extra_data || {}),
1226
+ order_key: nextOrderKey,
1227
+ },
1228
+ }),
1229
+ });
1230
+ } else {
1231
+ await persistSiblingOrderByList(reordered, parentId);
1232
+ }
1233
+ await loadSandbox(state.currentSandbox.id);
1234
+ },
1160
1235
  onReorderSiblings: treeReadonly ? undefined : async (dragNodeId, targetNodeId, position) => {
1161
1236
  if (!state.currentSandbox) return;
1162
1237
  if (!dragNodeId || !targetNodeId || dragNodeId === targetNodeId) return;
@@ -1175,19 +1250,30 @@ function renderWorkTree() {
1175
1250
  const targetIndex = siblings.findIndex((item) => item.id === targetNodeId);
1176
1251
  if (targetIndex < 0) return;
1177
1252
  const insertIndex = position === 'after' ? targetIndex + 1 : targetIndex;
1178
- const left = siblings[insertIndex - 1] || null;
1179
- const right = siblings[insertIndex] || null;
1253
+ const reordered = [...siblings];
1254
+ reordered.splice(insertIndex, 0, dragNode);
1255
+ const left = reordered[insertIndex - 1] || null;
1256
+ const right = reordered[insertIndex + 1] || null;
1180
1257
  const nextOrderKey = rankBetween(getNodeOrderKey(left, 'order_key'), getNodeOrderKey(right, 'order_key'));
1181
- await apiRequest(`${API_BASE}/items/${dragNodeId}`, {
1182
- method: 'PUT',
1183
- body: JSON.stringify({
1184
- parent_id: nextParentId,
1185
- extra_data: {
1186
- ...(dragNode.extra_data || {}),
1187
- order_key: nextOrderKey,
1188
- },
1189
- }),
1190
- });
1258
+ const canDirectUpdate = isOrderKeyBetween(getNodeOrderKey(left, 'order_key'), getNodeOrderKey(right, 'order_key'), nextOrderKey)
1259
+ && (
1260
+ nextOrderKey !== getNodeOrderKey(dragNode, 'order_key')
1261
+ || (dragNode.parent_id || null) !== nextParentId
1262
+ );
1263
+ if (canDirectUpdate) {
1264
+ await apiRequest(`${API_BASE}/items/${dragNodeId}`, {
1265
+ method: 'PUT',
1266
+ body: JSON.stringify({
1267
+ parent_id: nextParentId,
1268
+ extra_data: {
1269
+ ...(dragNode.extra_data || {}),
1270
+ order_key: nextOrderKey,
1271
+ },
1272
+ }),
1273
+ });
1274
+ } else {
1275
+ await persistSiblingOrderByList(reordered, nextParentId, dragNodeId);
1276
+ }
1191
1277
  await loadSandbox(state.currentSandbox.id);
1192
1278
  },
1193
1279
  onReorderLanes: treeReadonly ? undefined : async (dragRootId, targetRootId, position = 'before') => {
@@ -1341,6 +1427,55 @@ function rankBetween(left, right) {
1341
1427
  return `${prefix}${ORDER_ALPHABET[Math.floor(ORDER_BASE / 2)]}`;
1342
1428
  }
1343
1429
 
1430
+ function encodeOrderIndex(index, width = 6) {
1431
+ let value = Number.isFinite(index) ? Math.max(0, Math.floor(index)) : 0;
1432
+ let encoded = '';
1433
+ do {
1434
+ const digit = value % ORDER_BASE;
1435
+ encoded = `${ORDER_ALPHABET[digit]}${encoded}`;
1436
+ value = Math.floor(value / ORDER_BASE);
1437
+ } while (value > 0);
1438
+ return encoded.padStart(width, ORDER_ALPHABET[0]);
1439
+ }
1440
+
1441
+ function isOrderKeyBetween(leftKey, rightKey, candidateKey) {
1442
+ const left = String(leftKey || '');
1443
+ const right = String(rightKey || '');
1444
+ const candidate = String(candidateKey || '');
1445
+ if (!candidate) return false;
1446
+ if (left && candidate <= left) return false;
1447
+ if (right && candidate >= right) return false;
1448
+ return true;
1449
+ }
1450
+
1451
+ async function persistSiblingOrderByList(orderedItems, nextParentId = null, movedNodeId = '') {
1452
+ const items = Array.isArray(orderedItems) ? orderedItems : [];
1453
+ for (let index = 0; index < items.length; index += 1) {
1454
+ const item = items[index];
1455
+ if (!item?.id) continue;
1456
+ const targetKey = encodeOrderIndex((index + 1) * 1024);
1457
+ const currentKey = getNodeOrderKey(item, 'order_key');
1458
+ const isMovedNode = Boolean(movedNodeId) && String(item.id) === String(movedNodeId);
1459
+ const currentParentId = item.parent_id || null;
1460
+ const needsParentUpdate = isMovedNode && currentParentId !== nextParentId;
1461
+ const needsOrderUpdate = currentKey !== targetKey;
1462
+ if (!needsParentUpdate && !needsOrderUpdate) continue;
1463
+ const body = {
1464
+ extra_data: {
1465
+ ...(item.extra_data || {}),
1466
+ order_key: targetKey,
1467
+ },
1468
+ };
1469
+ if (needsParentUpdate) {
1470
+ body.parent_id = nextParentId;
1471
+ }
1472
+ await apiRequest(`${API_BASE}/items/${item.id}`, {
1473
+ method: 'PUT',
1474
+ body: JSON.stringify(body),
1475
+ });
1476
+ }
1477
+ }
1478
+
1344
1479
  function getNodeOrderKey(item, keyName = 'order_key') {
1345
1480
  return String(item?.extra_data?.[keyName] || '').trim();
1346
1481
  }
@@ -2487,6 +2487,11 @@ h2 {
2487
2487
  background: #dbe8ff;
2488
2488
  }
2489
2489
 
2490
+ .node-action-btn.move {
2491
+ font-size: 11px;
2492
+ font-weight: 700;
2493
+ }
2494
+
2490
2495
  .node-status.pending { background: #9aa0a6; }
2491
2496
  .node-status.in_progress { background: var(--primary); }
2492
2497
  .node-status.done { background: var(--success); }
@@ -212,6 +212,13 @@ function renderEntityPreviewBoxes(nodeId, entityRowsByNodeId, mode) {
212
212
  `;
213
213
  }
214
214
 
215
+ function renderSiblingMoveActions(node) {
216
+ return `
217
+ <button class="node-action-btn move" data-action="move-up" data-id="${esc(node.id)}" title="上移">↑</button>
218
+ <button class="node-action-btn move" data-action="move-down" data-id="${esc(node.id)}" title="下移">↓</button>
219
+ `;
220
+ }
221
+
215
222
  function renderTreeNode(node, byParent, expandedIdSet, entitySummaryByNodeId, showAssignee = false, entityRowsByNodeId = {}, elementPreviewMode = 'none', readonly = false, selectedId = '') {
216
223
  const children = byParent.get(node.id) || [];
217
224
  const hasChildren = children.length > 0;
@@ -234,6 +241,7 @@ function renderTreeNode(node, byParent, expandedIdSet, entitySummaryByNodeId, sh
234
241
  <button class="node-action-btn add-child" data-action="add-child" data-id="${esc(node.id)}" title="添加子任务">+</button>
235
242
  <button class="node-action-btn" data-action="edit" data-id="${esc(node.id)}" title="编辑">✎</button>
236
243
  <button class="node-action-btn delete" data-action="delete" data-id="${esc(node.id)}" title="删除">✕</button>
244
+ ${renderSiblingMoveActions(node)}
237
245
  </div>
238
246
  `}
239
247
  </div>
@@ -266,6 +274,7 @@ function renderTreeNode(node, byParent, expandedIdSet, entitySummaryByNodeId, sh
266
274
  <button class="node-action-btn add-child" data-action="add-child" data-id="${esc(node.id)}" title="添加子任务">+</button>
267
275
  <button class="node-action-btn" data-action="edit" data-id="${esc(node.id)}" title="编辑">✎</button>
268
276
  <button class="node-action-btn delete" data-action="delete" data-id="${esc(node.id)}" title="删除">✕</button>
277
+ ${renderSiblingMoveActions(node)}
269
278
  </div>
270
279
  `}
271
280
  </div>
@@ -292,6 +301,7 @@ function renderTreeNode(node, byParent, expandedIdSet, entitySummaryByNodeId, sh
292
301
  <button class="node-action-btn add-child" data-action="add-child" data-id="${esc(child.id)}" title="添加子任务">+</button>
293
302
  <button class="node-action-btn" data-action="edit" data-id="${esc(child.id)}" title="编辑">✎</button>
294
303
  <button class="node-action-btn delete" data-action="delete" data-id="${esc(child.id)}" title="删除">✕</button>
304
+ ${renderSiblingMoveActions(child)}
295
305
  </div>
296
306
  `}
297
307
  </div>
@@ -390,6 +400,7 @@ function renderDenseLaneNode(node, byParent, expandedIdSet, entitySummaryByNodeI
390
400
  <button class="node-action-btn add-child" data-action="add-child" data-id="${esc(node.id)}" title="添加子任务">+</button>
391
401
  <button class="node-action-btn" data-action="edit" data-id="${esc(node.id)}" title="编辑">✎</button>
392
402
  <button class="node-action-btn delete" data-action="delete" data-id="${esc(node.id)}" title="删除">✕</button>
403
+ ${renderSiblingMoveActions(node)}
393
404
  </div>
394
405
  `}
395
406
  </div>
@@ -469,6 +480,7 @@ export function mountWorkTree(targetId, options) {
469
480
  onSelect,
470
481
  onSelectEntity,
471
482
  onMoveNode,
483
+ onMoveSibling,
472
484
  onReorderSiblings,
473
485
  onReorderLanes,
474
486
  entitySummaryByNodeId = {},
@@ -499,7 +511,7 @@ export function mountWorkTree(targetId, options) {
499
511
  } else {
500
512
  const orderKeyA = keyOf(a, 'order_key');
501
513
  const orderKeyB = keyOf(b, 'order_key');
502
- if (orderKeyA && orderKeyB && orderKeyA !== orderKeyB) return orderKeyA.localeCompare(orderKeyB);
514
+ if (orderKeyA && orderKeyB && orderKeyA !== orderKeyB) return orderKeyA < orderKeyB ? -1 : 1;
503
515
  if (orderKeyA && !orderKeyB) return -1;
504
516
  if (!orderKeyA && orderKeyB) return 1;
505
517
  }
@@ -547,6 +559,8 @@ export function mountWorkTree(targetId, options) {
547
559
  if (action === 'edit') onEdit?.(id);
548
560
  if (action === 'delete') onDelete?.(id);
549
561
  if (action === 'quick-chat') onQuickChat?.(id, el);
562
+ if (action === 'move-up') onMoveSibling?.(id, 'up');
563
+ if (action === 'move-down') onMoveSibling?.(id, 'down');
550
564
  });
551
565
  });
552
566
  container.querySelectorAll('[data-select-id]').forEach((el) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qnote/q-ai-note",
3
- "version": "1.0.20",
3
+ "version": "1.0.22",
4
4
  "type": "module",
5
5
  "description": "AI-assisted personal work sandbox and diary system",
6
6
  "main": "dist/server/index.js",