Merge remote-tracking branch 'gitea/develop' into feat/page-templates
# Conflicts: # apps/server/src/integrations/throttle/throttle.module.ts # apps/server/src/integrations/throttle/throttler-names.ts
This commit is contained in:
@@ -16,6 +16,11 @@ import { treeModel } from '../model/tree-model';
|
||||
import { DocTreeRow } from './doc-tree-row';
|
||||
import styles from '../styles/tree.module.css';
|
||||
|
||||
// Page-tree row heights. STANDARD is the safe default density; COMPACT is the
|
||||
// denser layout gated behind the COMPACT_PAGE_TREE feature flag.
|
||||
export const ROW_HEIGHT_STANDARD = 32;
|
||||
export const ROW_HEIGHT_COMPACT = 26;
|
||||
|
||||
export type RenderRowProps<T extends object> = {
|
||||
node: TreeNode<T>;
|
||||
level: number;
|
||||
@@ -122,11 +127,11 @@ function DocTreeInner<T extends object>(
|
||||
selectedId,
|
||||
renderRow,
|
||||
indentPerLevel = 8,
|
||||
// Compact vertical density: each virtualized row occupies exactly this
|
||||
// many px (the virtualizer stride). Row content is ~22px (18px icon /
|
||||
// 14px text / 20px action icons), so 26px keeps a small, even gap between
|
||||
// nodes without clipping. Lower => denser tree.
|
||||
rowHeight = 26,
|
||||
// Each virtualized row occupies exactly this many px (the virtualizer
|
||||
// stride). Default is standard density (32px); the denser compact layout
|
||||
// (26px) is opt-in and driven by the COMPACT_PAGE_TREE feature flag in
|
||||
// consumers. Lower => denser tree.
|
||||
rowHeight = ROW_HEIGHT_STANDARD,
|
||||
onMove,
|
||||
onToggle,
|
||||
onSelect,
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
import { useAtom } from "jotai";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
forwardRef,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Text } from "@mantine/core";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import {
|
||||
fetchAllAncestorChildren,
|
||||
useGetRootSidebarPagesQuery,
|
||||
@@ -16,13 +25,23 @@ import {
|
||||
buildTree,
|
||||
buildTreeWithChildren,
|
||||
mergeRootTrees,
|
||||
collectAllIds,
|
||||
collectBranchIds,
|
||||
} from "@/features/page/tree/utils/utils.ts";
|
||||
import { SpaceTreeNode } from "@/features/page/tree/types.ts";
|
||||
import { treeModel } from "@/features/page/tree/model/tree-model";
|
||||
import { getPageBreadcrumbs } from "@/features/page/services/page-service.ts";
|
||||
import {
|
||||
getPageBreadcrumbs,
|
||||
getSpaceTree,
|
||||
} from "@/features/page/services/page-service.ts";
|
||||
import { IPage } from "@/features/page/types/page.types.ts";
|
||||
import { extractPageSlugId } from "@/lib";
|
||||
import { DocTree } from "./doc-tree";
|
||||
import { isCompactPageTreeEnabled } from "@/lib/config.ts";
|
||||
import {
|
||||
DocTree,
|
||||
ROW_HEIGHT_COMPACT,
|
||||
ROW_HEIGHT_STANDARD,
|
||||
} from "./doc-tree";
|
||||
import { SpaceTreeRow } from "./space-tree-row";
|
||||
|
||||
interface SpaceTreeProps {
|
||||
@@ -30,10 +49,21 @@ interface SpaceTreeProps {
|
||||
readOnly: boolean;
|
||||
}
|
||||
|
||||
export default function SpaceTree({ spaceId, readOnly }: SpaceTreeProps) {
|
||||
export type SpaceTreeApi = {
|
||||
expandAll: () => Promise<void>;
|
||||
collapseAll: () => void;
|
||||
isExpanding: boolean;
|
||||
};
|
||||
|
||||
const SpaceTree = forwardRef<SpaceTreeApi, SpaceTreeProps>(function SpaceTree(
|
||||
{ spaceId, readOnly },
|
||||
ref,
|
||||
) {
|
||||
const { t } = useTranslation();
|
||||
const { pageSlug } = useParams();
|
||||
const compactTree = isCompactPageTreeEnabled();
|
||||
const [data, setData] = useAtom(treeDataAtom);
|
||||
const [isExpanding, setIsExpanding] = useState(false);
|
||||
const { handleMove } = useTreeMutation(spaceId);
|
||||
const {
|
||||
data: pagesData,
|
||||
@@ -186,6 +216,64 @@ export default function SpaceTree({ spaceId, readOnly }: SpaceTreeProps) {
|
||||
[data, spaceId],
|
||||
);
|
||||
|
||||
const expandAll = useCallback(async () => {
|
||||
const startSpaceId = spaceIdRef.current;
|
||||
setIsExpanding(true);
|
||||
try {
|
||||
// One request: the entire space tree, permission-filtered server-side.
|
||||
const items = await getSpaceTree({ spaceId: startSpaceId });
|
||||
// Space switched mid-flight — abort merge/expand.
|
||||
if (spaceIdRef.current !== startSpaceId) return;
|
||||
|
||||
const fullTree = buildTreeWithChildren(buildTree(items));
|
||||
|
||||
setData((prev) => {
|
||||
// Replace current-space nodes with the full tree; keep other spaces intact.
|
||||
const others = prev.filter((n) => n?.spaceId !== startSpaceId);
|
||||
return [...others, ...fullTree];
|
||||
});
|
||||
|
||||
// Open every branch node (node with children) of the current space only.
|
||||
const branchIds = collectBranchIds(fullTree);
|
||||
|
||||
setOpenTreeNodes((prev) => {
|
||||
const next = { ...prev };
|
||||
for (const id of branchIds) next[id] = true;
|
||||
return next;
|
||||
});
|
||||
} catch (err: any) {
|
||||
// Never swallow: log full error + surface the real reason.
|
||||
console.error("[tree] expandAll failed", err);
|
||||
notifications.show({
|
||||
color: "red",
|
||||
message: t("Couldn't expand the tree: {{reason}}", {
|
||||
reason:
|
||||
err?.response?.data?.message ?? err?.message ?? String(err),
|
||||
}),
|
||||
});
|
||||
} finally {
|
||||
setIsExpanding(false);
|
||||
}
|
||||
}, [setData, setOpenTreeNodes, t]);
|
||||
|
||||
const collapseAll = useCallback(() => {
|
||||
// The open-map is shared across spaces; collapse only current-space ids so
|
||||
// other spaces' expanded state is left intact.
|
||||
const ids = collectAllIds(filteredData);
|
||||
|
||||
setOpenTreeNodes((prev) => {
|
||||
const next = { ...prev };
|
||||
for (const id of ids) next[id] = false;
|
||||
return next;
|
||||
});
|
||||
}, [filteredData, setOpenTreeNodes]);
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({ expandAll, collapseAll, isExpanding }),
|
||||
[expandAll, collapseAll, isExpanding],
|
||||
);
|
||||
|
||||
// Stable callbacks for DocTree. Without these, every parent render recreates
|
||||
// the props and tears down every row's draggable/dropTarget subscription,
|
||||
// defeating memo(DocTreeRow).
|
||||
@@ -219,6 +307,7 @@ export default function SpaceTree({ spaceId, readOnly }: SpaceTreeProps) {
|
||||
renderRow={renderRow}
|
||||
onMove={handleMove}
|
||||
onToggle={handleToggle}
|
||||
rowHeight={compactTree ? ROW_HEIGHT_COMPACT : ROW_HEIGHT_STANDARD}
|
||||
readOnly={readOnly}
|
||||
disableDrag={disableDragDrop}
|
||||
disableDrop={disableDragDrop}
|
||||
@@ -228,4 +317,6 @@ export default function SpaceTree({ spaceId, readOnly }: SpaceTreeProps) {
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default SpaceTree;
|
||||
|
||||
Reference in New Issue
Block a user