4-26 树rust-2
This commit is contained in:
@@ -35,6 +35,32 @@ export function pickCanonicalDocumentRecord<T extends DocumentRecordLike>(record
|
||||
return [...records].sort(compareDocumentCanonicalOrder)[0] ?? null;
|
||||
}
|
||||
|
||||
export function pickCanonicalDocumentRecordsByBusinessId<
|
||||
T extends DocumentRecordLike & { id?: string | null },
|
||||
>(records: readonly T[]): T[] {
|
||||
const grouped = new Map<string, T[]>();
|
||||
for (const record of records) {
|
||||
const businessId = String(record.id ?? "").trim();
|
||||
if (!businessId) continue;
|
||||
|
||||
const bucket = grouped.get(businessId);
|
||||
if (bucket) {
|
||||
bucket.push(record);
|
||||
} else {
|
||||
grouped.set(businessId, [record]);
|
||||
}
|
||||
}
|
||||
|
||||
const result: T[] = [];
|
||||
for (const bucket of grouped.values()) {
|
||||
const canonical = pickCanonicalDocumentRecord([...bucket]);
|
||||
if (canonical) {
|
||||
result.push(canonical);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function getCanonicalDocumentByBusinessId<T extends DocumentRecordLike>(
|
||||
ctx: any,
|
||||
documentId: string,
|
||||
|
||||
@@ -3,6 +3,29 @@ export type ParentLinkedRow = {
|
||||
parent_id?: string | null;
|
||||
};
|
||||
|
||||
export function buildParentById<T extends ParentLinkedRow>(rows: readonly T[]): Map<string, string | null> {
|
||||
const map = new Map<string, string | null>();
|
||||
for (const row of rows) {
|
||||
map.set(row.id, row.parent_id ?? null);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
export function isAncestorOf(
|
||||
ancestorId: string,
|
||||
nodeId: string,
|
||||
parentById: Map<string, string | null>,
|
||||
): boolean {
|
||||
let current: string | null | undefined = nodeId;
|
||||
while (current) {
|
||||
const parentId = parentById.get(current);
|
||||
if (!parentId) return false;
|
||||
if (parentId === ancestorId) return true;
|
||||
current = parentId;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集以 rootId 为根的整棵子树(包含根节点本身)。
|
||||
*
|
||||
@@ -41,4 +64,3 @@ export function collectSubtree<T extends ParentLinkedRow>(rows: readonly T[], ro
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,10 +3,14 @@ import { api } from "./_generated/api";
|
||||
import { v } from "convex/values";
|
||||
import { requireUserId } from "./_utils/auth";
|
||||
import { nowIso } from "./_utils/time";
|
||||
import { collectSubtree } from "./_utils/documentTree";
|
||||
import { buildParentById, collectSubtree, isAncestorOf } from "./_utils/documentTree";
|
||||
import { enqueueIngestDocumentJob } from "./_utils/ingestJobs";
|
||||
import { extractTextFromDocumentContent } from "./_utils/text";
|
||||
import { getCanonicalDocumentByBusinessId, getCanonicalParentDocumentId } from "./_utils/documentRecord";
|
||||
import {
|
||||
getCanonicalDocumentByBusinessId,
|
||||
getCanonicalParentDocumentId,
|
||||
pickCanonicalDocumentRecordsByBusinessId,
|
||||
} from "./_utils/documentRecord";
|
||||
|
||||
const accessScope = v.union(v.literal("private"), v.literal("shared"), v.literal("public"));
|
||||
|
||||
@@ -1378,8 +1382,35 @@ export const move = mutation({
|
||||
|
||||
const doc = await getCanonicalDocumentByBusinessId(ctx, args.id);
|
||||
if (!doc) throw new Error("页面不存在");
|
||||
if (doc.deleted_at != null) throw new Error("页面不存在");
|
||||
if (doc.user_id !== userId) throw new Error("无权限");
|
||||
|
||||
const toParentId = args.parentId;
|
||||
if (toParentId === doc.id) {
|
||||
throw new Error("不能把页面移动到自身下面");
|
||||
}
|
||||
if (toParentId) {
|
||||
const targetParentDoc = await getCanonicalDocumentByBusinessId<any>(ctx, toParentId);
|
||||
if (!targetParentDoc || targetParentDoc.deleted_at != null || targetParentDoc.user_id !== userId) {
|
||||
throw new Error("目标父页面不存在或无权限");
|
||||
}
|
||||
if (targetParentDoc.workspace_id !== doc.workspace_id) {
|
||||
throw new Error("暂不支持跨工作空间移动页面");
|
||||
}
|
||||
|
||||
const workspaceDocs = await ctx.db
|
||||
.query("documents")
|
||||
.withIndex("by_workspace", (q) => q.eq("workspace_id", doc.workspace_id))
|
||||
.collect();
|
||||
const canonicalWorkspaceDocs = pickCanonicalDocumentRecordsByBusinessId(workspaceDocs)
|
||||
.filter((row) => row.user_id === userId)
|
||||
.filter((row) => row.deleted_at == null);
|
||||
const parentById = buildParentById(canonicalWorkspaceDocs);
|
||||
if (isAncestorOf(doc.id, toParentId, parentById)) {
|
||||
throw new Error("不能把页面移动到自己的后代下面");
|
||||
}
|
||||
}
|
||||
|
||||
// 说明:仅更新当前节点的 sort_order 会导致兄弟节点出现重复 sort_order,
|
||||
// 前端会按 sort_order + created_at 排序,结果常常表现为“拖拽无效/拖完又回弹”。
|
||||
// 这里统一对目标父节点(以及跨父移动时的原父节点)做重新编号,保证 sort_order 唯一且连续。
|
||||
@@ -1433,7 +1464,6 @@ export const move = mutation({
|
||||
};
|
||||
|
||||
const fromParentId = (doc.parent_id ?? null) as string | null;
|
||||
const toParentId = args.parentId;
|
||||
|
||||
if (fromParentId === toParentId) {
|
||||
const siblings = await fetchSiblings(toParentId);
|
||||
@@ -1441,7 +1471,13 @@ export const move = mutation({
|
||||
const position = clampIndex(args.sortOrder, list.length);
|
||||
list.splice(position, 0, doc);
|
||||
await applyOrder(list, toParentId, doc._id);
|
||||
return { ok: true, updated_at: ts };
|
||||
return {
|
||||
ok: true,
|
||||
parent_id: toParentId,
|
||||
sort_order: position,
|
||||
workspace_id: doc.workspace_id,
|
||||
updated_at: ts,
|
||||
};
|
||||
}
|
||||
|
||||
// 先重排原父节点,确保移动后原列表连续。
|
||||
@@ -1454,7 +1490,13 @@ export const move = mutation({
|
||||
newSiblings.splice(position, 0, doc);
|
||||
await applyOrder(newSiblings, toParentId, doc._id);
|
||||
|
||||
return { ok: true, updated_at: ts };
|
||||
return {
|
||||
ok: true,
|
||||
parent_id: toParentId,
|
||||
sort_order: position,
|
||||
workspace_id: doc.workspace_id,
|
||||
updated_at: ts,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user