feat: Add breadcrumb to docs in command menu (#12403)

This commit is contained in:
Tom Moor
2026-05-21 17:42:04 -04:00
committed by GitHub
parent fcf26e4b9b
commit f06defaa14
3 changed files with 77 additions and 17 deletions
+8 -5
View File
@@ -81,6 +81,7 @@ import {
trashPath,
documentEditPath,
} from "~/utils/routeHelpers";
import { documentBreadcrumbText } from "~/components/DocumentBreadcrumb";
import CollectionIcon from "~/components/Icons/CollectionIcon";
import type {
Action,
@@ -108,19 +109,21 @@ export const openDocument = createActionWithChildren({
shortcut: ["o", "d"],
keywords: "go to",
icon: <DocumentIcon />,
children: ({ stores }) => {
children: ({ stores, t }) => {
const nodes = stores.collections.navigationNodes.reduce(
(acc, node) => [...acc, ...node.children],
[] as NavigationNode[]
);
const documents = stores.documents.orderedData;
return uniqBy([...documents, ...nodes], "id").map((item) =>
createInternalLinkAction({
return uniqBy([...documents, ...nodes], "id").map((item) => {
const document = stores.documents.get(item.id);
return createInternalLinkAction({
// Note: using url which includes the slug rather than id here to bust
// cache if the document is renamed
id: item.url,
name: item.title,
description: document ? documentBreadcrumbText(document, t) : undefined,
icon: item.icon ? (
<Icon
value={item.icon}
@@ -132,8 +135,8 @@ export const openDocument = createActionWithChildren({
),
section: DocumentSection,
to: item.url,
})
);
});
});
},
});
@@ -1,13 +1,16 @@
import { DocumentIcon } from "outline-icons";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import Icon from "@shared/components/Icon";
import { createInternalLinkAction } from "~/actions";
import { RecentSection } from "~/actions/sections";
import { documentBreadcrumbText } from "~/components/DocumentBreadcrumb";
import useStores from "~/hooks/useStores";
import { documentPath } from "~/utils/routeHelpers";
const useRecentDocumentActions = (count = 6) => {
const { documents, ui } = useStores();
const { t } = useTranslation();
return useMemo(
() =>
@@ -19,6 +22,7 @@ const useRecentDocumentActions = (count = 6) => {
name: item.titleWithDefault,
analyticsName: "Recently viewed document",
section: RecentSection,
description: documentBreadcrumbText(item, t),
icon: item.icon ? (
<Icon
value={item.icon}
@@ -31,7 +35,7 @@ const useRecentDocumentActions = (count = 6) => {
to: documentPath(item),
})
),
[count, ui.activeDocumentId, documents.recentlyViewed]
[count, ui.activeDocumentId, documents.recentlyViewed, t]
);
};
+64 -11
View File
@@ -1,10 +1,10 @@
import type { TFunction } from "i18next";
import { observer } from "mobx-react";
import { ArchiveIcon, GoToIcon, TrashIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import Icon from "@shared/components/Icon";
import type { NavigationNode } from "@shared/types";
import type Collection from "~/models/Collection";
import type Document from "~/models/Document";
import Breadcrumb from "~/components/Breadcrumb";
@@ -20,6 +20,56 @@ import { archivePath, trashPath } from "~/utils/routeHelpers";
import { createInternalLinkAction } from "~/actions";
import { ActiveDocumentSection } from "~/actions/sections";
/**
* Returns the breadcrumb parts leading up to a document, separating the
* (possibly deleted) collection label from ancestor document titles. The
* document itself is not included.
*
* @param document - the document to compute the breadcrumb for.
* @param t - translation function for fallback titles.
* @returns the collection label and ancestor titles.
*/
export function documentBreadcrumbParts(
document: Document,
t: TFunction
): { collection: string | undefined; ancestors: string[] } {
let collectionLabel: string | undefined;
if (document.isCollectionDeleted) {
collectionLabel = t("Deleted Collection");
} else if (document.collection?.name) {
collectionLabel = document.collection.name;
}
return {
collection: collectionLabel,
ancestors: document.pathTo
.slice(0, -1)
.map((node) => node.title || t("Untitled")),
};
}
/**
* Returns the breadcrumb path leading up to a document as a plain text
* string. Includes the collection name (or "Deleted Collection" fallback)
* and any ancestor document titles, slash-separated.
*
* @param document - the document to compute the breadcrumb for.
* @param t - translation function for fallback titles.
* @returns the breadcrumb as a slash-separated string, or undefined if the
* document has no resolvable parent context.
*/
export function documentBreadcrumbText(
document: Document,
t: TFunction
): string | undefined {
const parts = documentBreadcrumbParts(document, t);
const segments = [
...(parts.collection ? [parts.collection] : []),
...parts.ancestors,
];
return segments.length ? segments.join(" / ") : undefined;
}
type Props = {
children?: React.ReactNode;
document: Document;
@@ -147,22 +197,25 @@ function DocumentBreadcrumb(
return <></>;
}
const slicedPath = reverse
? path.slice(depth && -depth)
: path.slice(0, depth);
const { collection: collectionLabel, ancestors: ancestorLabels } =
documentBreadcrumbParts(document, t);
const slicedAncestors = reverse
? ancestorLabels.slice(depth && -depth)
: ancestorLabels.slice(0, depth);
const showCollection =
collection &&
(!reverse || depth === undefined || slicedPath.length < depth);
!!collectionLabel &&
(!reverse || depth === undefined || slicedAncestors.length < depth);
return (
<>
{showCollection && collection.name}
{slicedPath.map((node: NavigationNode, index: number) => (
<React.Fragment key={node.id}>
{showCollection && collectionLabel}
{slicedAncestors.map((label, index) => (
<React.Fragment key={index}>
{showCollection && <SmallSlash />}
{node.title || t("Untitled")}
{!showCollection && index !== slicedPath.length - 1 && (
{label}
{!showCollection && index !== slicedAncestors.length - 1 && (
<SmallSlash />
)}
</React.Fragment>