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