Files
gitmost/packages/editor-ext/src/lib/subpages/subpages.ts
claude code agent 227 f7b99f9fb3 feat(editor): recursive tree mode for the subpages node (#150)
The `subpages` node showed only one level of direct children. Add a `recursive`
attribute that renders the FULL descendant tree of the current page — fully
expanded, unlimited depth. Default `false`, so every previously-inserted node
stays flat (backward compatible). No backend changes: `POST /pages/tree` (via the
`getSpaceTree` wrapper) already returns the whole subtree as a flat `IPage[]`
(recursive CTE, permission-filtered); the nested tree is built on the client by
`parentPageId`.

- editor-ext `subpages.ts`: `recursive` attribute (parse/render `data-recursive`),
  shared by client + server so the collab ProseMirror schema keeps the attribute.
- `getSpaceTree`: arg loosened to `{ spaceId?; pageId? }` (the endpoint accepts
  either); new `useGetPageTreeQuery(pageId)` react-query hook.
- `subpages-view.tsx`: split into `FlatSubpages` (unchanged) and
  `RecursiveSubpages`; `buildSubtree` assembles the nested tree (cycle/self-parent
  guard, `sortPositionKeys` per level, root excluded) and a recursive `TreeNode`
  renders it (16px indent per depth, soft "showing N" note past 300 — data never
  capped). Shared/public context reads the already-nested shared tree, no
  `/pages/tree` request.
- toggles: bubble-menu flat⇄tree button + a second slash-menu item "Page tree".

Review follow-ups folded in: invalidate `["page-tree"]` from the create / update /
move / delete cache helpers so an open recursive tree refreshes (no stale data);
mode icon made reactive on editor transactions; `t` threaded into `TreeNode`
(no per-node useTranslation); shared-subtree hook deduped to a thin alias.

editor-ext build + client `tsc --noEmit` both clean. Backend untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 14:33:07 +03:00

87 lines
1.8 KiB
TypeScript

import { mergeAttributes, Node } from "@tiptap/core";
import { ReactNodeViewRenderer } from "@tiptap/react";
export interface SubpagesOptions {
HTMLAttributes: Record<string, any>;
view: any;
}
export interface SubpagesAttributes {
recursive?: boolean;
}
declare module "@tiptap/core" {
interface Commands<ReturnType> {
subpages: {
insertSubpages: (attributes?: SubpagesAttributes) => ReturnType;
};
}
}
export const Subpages = Node.create<SubpagesOptions>({
name: "subpages",
addOptions() {
return {
HTMLAttributes: {},
view: null,
};
},
group: "block",
atom: true,
draggable: true,
isolating: true,
addAttributes() {
return {
recursive: {
// Existing nodes stay flat -> backward compatible.
default: false,
parseHTML: (el) => el.getAttribute("data-recursive") === "true",
renderHTML: (attrs) =>
attrs.recursive ? { "data-recursive": "true" } : {},
},
};
},
parseHTML() {
return [
{
tag: `div[data-type="${this.name}"]`,
},
];
},
renderHTML({ HTMLAttributes }) {
return [
"div",
mergeAttributes(
{ "data-type": this.name },
this.options.HTMLAttributes,
HTMLAttributes
),
];
},
addCommands() {
return {
insertSubpages:
(attributes) =>
({ commands }) => {
return commands.insertContent({
type: this.name,
attrs: attributes,
});
},
};
},
addNodeView() {
// Force the react node view to render immediately using flush sync (https://github.com/ueberdosis/tiptap/blob/b4db352f839e1d82f9add6ee7fb45561336286d8/packages/react/src/ReactRenderer.tsx#L183-L191)
this.editor.isInitialized = true;
return ReactNodeViewRenderer(this.options.view);
},
});