mirror of
https://github.com/docmost/docmost.git
synced 2026-09-11 07:56:54 +08:00
fix: page tree reordering
This commit is contained in:
@@ -33,6 +33,10 @@ import {
|
||||
|
||||
import { treeDataAtom } from "@/features/page/tree/atoms/tree-data-atom.ts";
|
||||
import { treeModel } from "@/features/page/tree/model/tree-model";
|
||||
import {
|
||||
spaceRoots,
|
||||
updateSpaceRoots,
|
||||
} from "@/features/page/tree/utils/utils.ts";
|
||||
import { useTreeMutation } from "@/features/page/tree/hooks/use-tree-mutation.ts";
|
||||
import type { SpaceTreeNode } from "@/features/page/tree/types.ts";
|
||||
import classes from "@/features/page/tree/styles/tree.module.css";
|
||||
@@ -77,7 +81,10 @@ export function NodeMenu({ node, canEdit }: NodeMenuProps) {
|
||||
const duplicatedPage = await duplicatePage({ pageId: node.id });
|
||||
|
||||
// figure out parent + insertion index
|
||||
const siblings = treeModel.siblingsOf(data, node.id);
|
||||
const siblings = treeModel.siblingsOf(
|
||||
spaceRoots(data, node.spaceId),
|
||||
node.id,
|
||||
);
|
||||
const parentId = siblings?.parentId ?? null;
|
||||
const currentIndex = siblings?.index ?? 0;
|
||||
const newIndex = currentIndex + 1;
|
||||
@@ -96,7 +103,9 @@ export function NodeMenu({ node, canEdit }: NodeMenuProps) {
|
||||
};
|
||||
|
||||
setData((prev) =>
|
||||
treeModel.insert(prev, parentId, treeNodeData, newIndex),
|
||||
updateSpaceRoots(prev, node.spaceId, (roots) =>
|
||||
treeModel.insert(roots, parentId, treeNodeData, newIndex),
|
||||
),
|
||||
);
|
||||
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -16,6 +16,8 @@ import {
|
||||
buildTree,
|
||||
buildTreeWithChildren,
|
||||
mergeRootTrees,
|
||||
spaceRoots,
|
||||
updateSpaceRoots,
|
||||
} from "@/features/page/tree/utils/utils.ts";
|
||||
import { SpaceTreeNode } from "@/features/page/tree/types.ts";
|
||||
import { getPageTitle } from "@/features/page/page.utils";
|
||||
@@ -66,18 +68,14 @@ export default function SpaceTree({ spaceId, readOnly }: SpaceTreeProps) {
|
||||
const allItems = pagesData.pages.flatMap((page) => page.items);
|
||||
const treeData = buildTree(allItems);
|
||||
|
||||
setData((prev) => {
|
||||
// Keep nodes belonging to other spaces — filteredData filters by spaceId
|
||||
// for rendering, so accumulating is safe. Preserves lazy-loaded children
|
||||
// and open-state when the user returns to a previously-visited space.
|
||||
const otherSpaces = prev.filter((n) => n?.spaceId !== spaceId);
|
||||
const currentSpace = prev.filter((n) => n?.spaceId === spaceId);
|
||||
const refreshed =
|
||||
currentSpace.length > 0
|
||||
? mergeRootTrees(currentSpace, treeData)
|
||||
: treeData;
|
||||
return [...otherSpaces, ...refreshed];
|
||||
});
|
||||
// Keep nodes belonging to other spaces — filteredData filters by spaceId
|
||||
// for rendering, so accumulating is safe. Preserves lazy-loaded children
|
||||
// and open-state when the user returns to a previously-visited space.
|
||||
setData((prev) =>
|
||||
updateSpaceRoots(prev, spaceId, (roots) =>
|
||||
roots.length > 0 ? mergeRootTrees(roots, treeData) : treeData,
|
||||
),
|
||||
);
|
||||
setIsDataLoaded(true);
|
||||
}, [pagesData, hasNextPage, spaceId]);
|
||||
|
||||
@@ -183,7 +181,7 @@ export default function SpaceTree({ spaceId, readOnly }: SpaceTreeProps) {
|
||||
);
|
||||
|
||||
const filteredData = useMemo(
|
||||
() => data.filter((node) => node?.spaceId === spaceId),
|
||||
() => spaceRoots(data, spaceId),
|
||||
[data, spaceId],
|
||||
);
|
||||
|
||||
|
||||
@@ -7,6 +7,10 @@ import { useNavigate, useParams } from "react-router-dom";
|
||||
import { treeDataAtom } from "@/features/page/tree/atoms/tree-data-atom.ts";
|
||||
import { treeModel } from "@/features/page/tree/model/tree-model";
|
||||
import type { DropOp } from "@/features/page/tree/model/tree-model.types";
|
||||
import {
|
||||
spaceRoots,
|
||||
updateSpaceRoots,
|
||||
} from "@/features/page/tree/utils/utils.ts";
|
||||
import { dropOpToMovePayload } from "./drop-op-to-move-payload";
|
||||
import { SpaceTreeNode } from "@/features/page/tree/types.ts";
|
||||
import { IPage } from "@/features/page/types/page.types.ts";
|
||||
@@ -45,7 +49,7 @@ export function useTreeMutation(spaceId: string): UseTreeMutation {
|
||||
|
||||
const handleMove = useCallback(
|
||||
async (sourceId: string, op: DropOp) => {
|
||||
const before = store.get(treeDataAtom);
|
||||
const before = spaceRoots(store.get(treeDataAtom), spaceId);
|
||||
const { tree: after, result } = treeModel.move(before, sourceId, op);
|
||||
if (after === before) return;
|
||||
|
||||
@@ -80,12 +84,12 @@ export function useTreeMutation(spaceId: string): UseTreeMutation {
|
||||
} as Partial<SpaceTreeNode>);
|
||||
}
|
||||
|
||||
setData(optimistic);
|
||||
setData((prev) => updateSpaceRoots(prev, spaceId, () => optimistic));
|
||||
|
||||
try {
|
||||
await movePageMutation.mutateAsync(payload);
|
||||
} catch {
|
||||
setData(before);
|
||||
setData((prev) => updateSpaceRoots(prev, spaceId, () => before));
|
||||
notifications.show({
|
||||
message: t("Failed to move page"),
|
||||
color: "red",
|
||||
@@ -157,7 +161,7 @@ export function useTreeMutation(spaceId: string): UseTreeMutation {
|
||||
// tree (e.g. lazy-load children on expand) immediately before calling
|
||||
// handleCreate hit a stale closure and compute lastIndex against the
|
||||
// pre-load tree, requiring a setTimeout-based wait at the call site.
|
||||
const current = store.get(treeDataAtom);
|
||||
const current = spaceRoots(store.get(treeDataAtom), spaceId);
|
||||
let lastIndex: number;
|
||||
if (parentId === null) {
|
||||
lastIndex = current.length;
|
||||
@@ -166,7 +170,11 @@ export function useTreeMutation(spaceId: string): UseTreeMutation {
|
||||
lastIndex = parent?.children?.length ?? 0;
|
||||
}
|
||||
|
||||
setData((prev) => treeModel.insert(prev, parentId, newNode, lastIndex));
|
||||
setData((prev) =>
|
||||
updateSpaceRoots(prev, spaceId, (roots) =>
|
||||
treeModel.insert(roots, parentId, newNode, lastIndex),
|
||||
),
|
||||
);
|
||||
|
||||
setTimeout(() => {
|
||||
emit({
|
||||
|
||||
@@ -220,3 +220,21 @@ export function mergeRootTrees(
|
||||
|
||||
return sortPositionKeys(merged);
|
||||
}
|
||||
|
||||
export function spaceRoots(
|
||||
tree: SpaceTreeNode[],
|
||||
spaceId: string,
|
||||
): SpaceTreeNode[] {
|
||||
return tree.filter((node) => node?.spaceId === spaceId);
|
||||
}
|
||||
|
||||
export function updateSpaceRoots(
|
||||
tree: SpaceTreeNode[],
|
||||
spaceId: string,
|
||||
update: (roots: SpaceTreeNode[]) => SpaceTreeNode[],
|
||||
): SpaceTreeNode[] {
|
||||
const roots = spaceRoots(tree, spaceId);
|
||||
const next = update(roots);
|
||||
if (next === roots) return tree;
|
||||
return [...tree.filter((node) => node?.spaceId !== spaceId), ...next];
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { WebSocketEvent } from "@/features/websocket/types";
|
||||
import { SpaceTreeNode } from "@/features/page/tree/types.ts";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { treeModel } from "@/features/page/tree/model/tree-model";
|
||||
import { updateSpaceRoots } from "@/features/page/tree/utils/utils.ts";
|
||||
import localEmitter from "@/lib/local-emitter.ts";
|
||||
|
||||
export const useTreeSocket = () => {
|
||||
@@ -61,65 +62,69 @@ export const useTreeSocket = () => {
|
||||
setTreeData((prev) => {
|
||||
if (treeModel.find(prev, event.payload.data.id)) return prev;
|
||||
const newParentId = event.payload.parentId as string | null;
|
||||
let next = treeModel.insert(
|
||||
prev,
|
||||
newParentId,
|
||||
event.payload.data,
|
||||
event.payload.index,
|
||||
);
|
||||
// Mirror the emitter: flip new parent's hasChildren to true so
|
||||
// the chevron renders on the receiver.
|
||||
if (newParentId) {
|
||||
next = treeModel.update(next, newParentId, {
|
||||
hasChildren: true,
|
||||
} as Partial<SpaceTreeNode>);
|
||||
}
|
||||
return next;
|
||||
return updateSpaceRoots(prev, event.spaceId, (roots) => {
|
||||
let next = treeModel.insert(
|
||||
roots,
|
||||
newParentId,
|
||||
event.payload.data,
|
||||
event.payload.index,
|
||||
);
|
||||
// Mirror the emitter: flip new parent's hasChildren to true so
|
||||
// the chevron renders on the receiver.
|
||||
if (newParentId) {
|
||||
next = treeModel.update(next, newParentId, {
|
||||
hasChildren: true,
|
||||
} as Partial<SpaceTreeNode>);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
});
|
||||
break;
|
||||
case "moveTreeNode":
|
||||
setTreeData((prev) => {
|
||||
const sourceBefore = treeModel.find(prev, event.payload.id);
|
||||
if (!sourceBefore) return prev;
|
||||
const oldParentId =
|
||||
(sourceBefore as SpaceTreeNode).parentPageId ?? null;
|
||||
const newParentId = event.payload.parentId as string | null;
|
||||
setTreeData((prev) =>
|
||||
updateSpaceRoots(prev, event.spaceId, (roots) => {
|
||||
const sourceBefore = treeModel.find(roots, event.payload.id);
|
||||
if (!sourceBefore) return roots;
|
||||
const oldParentId =
|
||||
(sourceBefore as SpaceTreeNode).parentPageId ?? null;
|
||||
const newParentId = event.payload.parentId as string | null;
|
||||
|
||||
const placed = treeModel.place(prev, event.payload.id, {
|
||||
parentId: newParentId,
|
||||
index: event.payload.index,
|
||||
});
|
||||
// `place` silently returns the same reference if the destination
|
||||
// parent isn't loaded on this client. Falling back to removing the
|
||||
// source keeps the UI consistent (the source will reappear when
|
||||
// the user expands the new parent and lazy-load fetches it).
|
||||
if (placed === prev) {
|
||||
return treeModel.remove(prev, event.payload.id);
|
||||
}
|
||||
const placed = treeModel.place(roots, event.payload.id, {
|
||||
parentId: newParentId,
|
||||
index: event.payload.index,
|
||||
});
|
||||
// `place` silently returns the same reference if the destination
|
||||
// parent isn't loaded on this client. Falling back to removing the
|
||||
// source keeps the UI consistent (the source will reappear when
|
||||
// the user expands the new parent and lazy-load fetches it).
|
||||
if (placed === roots) {
|
||||
return treeModel.remove(roots, event.payload.id);
|
||||
}
|
||||
|
||||
let next = treeModel.update(placed, event.payload.id, {
|
||||
position: event.payload.position,
|
||||
parentPageId: newParentId,
|
||||
} as Partial<SpaceTreeNode>);
|
||||
let next = treeModel.update(placed, event.payload.id, {
|
||||
position: event.payload.position,
|
||||
parentPageId: newParentId,
|
||||
} as Partial<SpaceTreeNode>);
|
||||
|
||||
// Mirror the emitter's hasChildren bookkeeping so both clients
|
||||
// converge to the same chevron state.
|
||||
if (oldParentId) {
|
||||
const oldParent = treeModel.find(next, oldParentId);
|
||||
if (!oldParent?.children?.length) {
|
||||
next = treeModel.update(next, oldParentId, {
|
||||
hasChildren: false,
|
||||
// Mirror the emitter's hasChildren bookkeeping so both clients
|
||||
// converge to the same chevron state.
|
||||
if (oldParentId) {
|
||||
const oldParent = treeModel.find(next, oldParentId);
|
||||
if (!oldParent?.children?.length) {
|
||||
next = treeModel.update(next, oldParentId, {
|
||||
hasChildren: false,
|
||||
} as Partial<SpaceTreeNode>);
|
||||
}
|
||||
}
|
||||
if (newParentId) {
|
||||
next = treeModel.update(next, newParentId, {
|
||||
hasChildren: true,
|
||||
} as Partial<SpaceTreeNode>);
|
||||
}
|
||||
}
|
||||
if (newParentId) {
|
||||
next = treeModel.update(next, newParentId, {
|
||||
hasChildren: true,
|
||||
} as Partial<SpaceTreeNode>);
|
||||
}
|
||||
|
||||
return next;
|
||||
});
|
||||
return next;
|
||||
}),
|
||||
);
|
||||
break;
|
||||
case "deleteTreeNode":
|
||||
setTreeData((prev) => {
|
||||
|
||||
Reference in New Issue
Block a user