mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-25 04:20:02 +00:00
Core & UI: Introduce page tree fallback API
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
---
|
||||
'fumadocs-core': minor
|
||||
'fumadocs-ui': minor
|
||||
---
|
||||
|
||||
**Introduce page tree `fallback` API**
|
||||
|
||||
Page tree is a tree structure.
|
||||
|
||||
Previously, when an item is excluded from page tree, it is isolated entirely that you cannot display it at all.
|
||||
|
||||
With the new fallback API, isolated pages will go into `fallback` page tree instead:
|
||||
|
||||
```json
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"type": "page",
|
||||
"name": "Introduction"
|
||||
}
|
||||
],
|
||||
"fallback": {
|
||||
"children": [
|
||||
{
|
||||
"type": "page",
|
||||
"name": "Hidden Page"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Items in `fallback` are invisible unless you've opened its item.
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"pages": ["ui", "headless", "mdx", "cli", "openapi"]
|
||||
"pages": ["ui", "headless", "mdx", "cli"]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { type ReactNode, useMemo } from 'react';
|
||||
import type * as PageTree from '@/server/page-tree';
|
||||
import type * as PageTree from '@/source/page-tree/definitions';
|
||||
|
||||
export interface BreadcrumbItem {
|
||||
name: ReactNode;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * from './get-toc';
|
||||
export * from '../utils/page-tree';
|
||||
export * as PageTree from './page-tree';
|
||||
export * as PageTree from '../source/page-tree/definitions';
|
||||
export * from './git-api';
|
||||
export * from './metadata';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type * as PageTree from '@/server/page-tree';
|
||||
import type * as PageTree from '@/source/page-tree/definitions';
|
||||
import type { I18nConfig } from '@/i18n';
|
||||
import {
|
||||
type ContentStorage,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ReactElement } from 'react';
|
||||
import type { I18nConfig } from '@/i18n';
|
||||
import type * as PageTree from '../../server/page-tree';
|
||||
import type * as PageTree from './definitions';
|
||||
import { joinPath } from '@/utils/path';
|
||||
import type { MetaData, PageData, UrlFn } from '../types';
|
||||
import type { ContentStorage } from '@/source/load-files';
|
||||
|
||||
+4
@@ -5,6 +5,10 @@ export interface Root {
|
||||
|
||||
name: ReactNode;
|
||||
children: Node[];
|
||||
/**
|
||||
* Another page tree that won't be displayed unless being opened.
|
||||
*/
|
||||
fallback?: Root;
|
||||
}
|
||||
|
||||
export type Node = Item | Separator | Folder;
|
||||
@@ -5,7 +5,7 @@ import type {
|
||||
PageFile,
|
||||
PageTreeTransformer,
|
||||
} from '@/source';
|
||||
import type * as PageTree from '@/server/page-tree';
|
||||
import type * as PageTree from './definitions';
|
||||
|
||||
export interface LegacyTransformerOptions<
|
||||
Page extends PageData,
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { ContentStorage, PageTreeTransformer } from '@/source';
|
||||
import { FileSystem } from '@/source';
|
||||
|
||||
export function transformerFallback(): PageTreeTransformer {
|
||||
const addedFiles = new Set<string>();
|
||||
|
||||
return {
|
||||
name: 'fumadocs:fallback',
|
||||
root(root) {
|
||||
const isolatedStorage: ContentStorage = new FileSystem();
|
||||
|
||||
for (const file of this.storage.getFiles()) {
|
||||
if (addedFiles.has(file)) continue;
|
||||
|
||||
const content =
|
||||
this.localeStorage?.read(file) ?? this.storage.read(file);
|
||||
if (!content) continue;
|
||||
isolatedStorage.write(file, content);
|
||||
}
|
||||
|
||||
if (isolatedStorage.getFiles().length === 0) return root;
|
||||
|
||||
root.fallback = this.builder.build({
|
||||
...this.options,
|
||||
id: `fallback-${root.$id ?? ''}`,
|
||||
storage: isolatedStorage,
|
||||
generateFallback: false,
|
||||
});
|
||||
|
||||
addedFiles.clear();
|
||||
return root;
|
||||
},
|
||||
file(node, file) {
|
||||
if (file) addedFiles.add(file);
|
||||
|
||||
return node;
|
||||
},
|
||||
folder(node, _dir, metaPath) {
|
||||
if (metaPath) addedFiles.add(metaPath);
|
||||
|
||||
return node;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,21 +1,21 @@
|
||||
import type * as PageTree from '@/server/page-tree';
|
||||
import type * as PageTree from '@/source/page-tree/definitions';
|
||||
|
||||
/**
|
||||
* Flatten tree to an array of page nodes
|
||||
*/
|
||||
export function flattenTree(tree: PageTree.Node[]): PageTree.Item[] {
|
||||
return tree.flatMap((node) => {
|
||||
if (node.type === 'separator') return [];
|
||||
export function flattenTree(nodes: PageTree.Node[]): PageTree.Item[] {
|
||||
const out: PageTree.Item[] = [];
|
||||
|
||||
for (const node of nodes) {
|
||||
if (node.type === 'folder') {
|
||||
const child = flattenTree(node.children);
|
||||
|
||||
if (node.index) return [node.index, ...child];
|
||||
|
||||
return child;
|
||||
if (node.index) out.push(node.index);
|
||||
out.push(...flattenTree(node.children));
|
||||
} else if (node.type === 'page') {
|
||||
out.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
return [node];
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -33,18 +33,17 @@ export function findNeighbour(
|
||||
} {
|
||||
const { separateRoot = true } = options ?? {};
|
||||
const roots = separateRoot ? getPageTreeRoots(tree) : [tree];
|
||||
if (tree.fallback) roots.push(tree.fallback);
|
||||
|
||||
for (const root of roots) {
|
||||
const list = flattenTree(root.children);
|
||||
const idx = list.findIndex((item) => item.url === url);
|
||||
if (idx === -1) continue;
|
||||
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
if (list[i].url === url) {
|
||||
return {
|
||||
next: list[i + 1],
|
||||
previous: list[i - 1],
|
||||
};
|
||||
}
|
||||
}
|
||||
return {
|
||||
previous: list[idx - 1],
|
||||
next: list[idx + 1],
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
@@ -57,10 +56,7 @@ export function getPageTreeRoots(
|
||||
if (child.type !== 'folder') return [];
|
||||
const roots = getPageTreeRoots(child);
|
||||
|
||||
if (child.root) {
|
||||
roots.push(child);
|
||||
}
|
||||
|
||||
if (child.root) roots.push(child);
|
||||
return roots;
|
||||
});
|
||||
|
||||
@@ -70,6 +66,8 @@ export function getPageTreeRoots(
|
||||
|
||||
/**
|
||||
* Separate the folder nodes of a root into multiple roots
|
||||
*
|
||||
* @deprecated it's useless
|
||||
*/
|
||||
export function separatePageTree(pageTree: PageTree.Root): PageTree.Root[] {
|
||||
return pageTree.children.flatMap((child) => {
|
||||
@@ -116,4 +114,8 @@ function findParentFromTree(
|
||||
return node;
|
||||
}
|
||||
}
|
||||
|
||||
if ('fallback' in node && node.fallback) {
|
||||
return findParentFromTree(node.fallback, url);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,5 +59,17 @@
|
||||
"url": "https://google.com",
|
||||
},
|
||||
],
|
||||
"fallback": {
|
||||
"$id": "fallback-root",
|
||||
"children": [
|
||||
{
|
||||
"$id": "hidden.mdx",
|
||||
"name": "Hidden",
|
||||
"type": "page",
|
||||
"url": "/hidden",
|
||||
},
|
||||
],
|
||||
"name": "",
|
||||
},
|
||||
"name": "",
|
||||
}
|
||||
@@ -17,6 +17,7 @@ export interface Option {
|
||||
icon?: ReactNode;
|
||||
title: ReactNode;
|
||||
description?: ReactNode;
|
||||
unlisted?: boolean;
|
||||
|
||||
/**
|
||||
* Detect from a list of urls
|
||||
@@ -83,7 +84,8 @@ export function RootToggle({
|
||||
)}
|
||||
<PopoverContent className="flex flex-col gap-1 w-(--radix-popover-trigger-width) overflow-hidden p-1">
|
||||
{options.map((item) => {
|
||||
const isActive = item === selected;
|
||||
const isActive = selected && item.url === selected.url;
|
||||
if (!isActive && item.unlisted) return;
|
||||
|
||||
return (
|
||||
<Link
|
||||
|
||||
@@ -19,13 +19,16 @@ const defaultTransform: GetSidebarTabsOptions['transform'] = (option, node) => {
|
||||
};
|
||||
|
||||
export function getSidebarTabs(
|
||||
pageTree: PageTree.Root,
|
||||
tree: PageTree.Root,
|
||||
{ transform = defaultTransform }: GetSidebarTabsOptions = {},
|
||||
): Option[] {
|
||||
function findOptions(node: PageTree.Folder): Option[] {
|
||||
function findOptions(
|
||||
node: PageTree.Root | PageTree.Folder,
|
||||
unlisted?: boolean,
|
||||
): Option[] {
|
||||
const results: Option[] = [];
|
||||
|
||||
if (node.root) {
|
||||
if ('root' in node && node.root) {
|
||||
const urls = getFolderUrls(node);
|
||||
|
||||
if (urls.size > 0) {
|
||||
@@ -33,6 +36,7 @@ export function getSidebarTabs(
|
||||
url: urls.values().next().value ?? '',
|
||||
title: node.name,
|
||||
icon: node.icon,
|
||||
unlisted,
|
||||
description: node.description,
|
||||
urls,
|
||||
};
|
||||
@@ -43,13 +47,19 @@ export function getSidebarTabs(
|
||||
}
|
||||
|
||||
for (const child of node.children) {
|
||||
if (child.type === 'folder') results.push(...findOptions(child));
|
||||
if (child.type === 'folder')
|
||||
results.push(...findOptions(child, unlisted));
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
return findOptions(pageTree as PageTree.Folder);
|
||||
const options = findOptions(tree);
|
||||
if (tree.fallback) {
|
||||
options.push(...findOptions(tree.fallback, true));
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
function getFolderUrls(
|
||||
|
||||
Reference in New Issue
Block a user