@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 +147 -12
- package/dist/web/styles.css +5 -0
- package/dist/web/vueRenderers.js +15 -1
- package/package.json +1 -1
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
|
|
1179
|
-
|
|
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
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
parent_id
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
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
|
}
|
package/dist/web/styles.css
CHANGED
|
@@ -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); }
|
package/dist/web/vueRenderers.js
CHANGED
|
@@ -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
|
|
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) => {
|