mirror of
https://github.com/outline/outline.git
synced 2026-06-13 03:14:59 +03:00
feat: Add breadcrumb to docs in command menu (#12403)
This commit is contained in:
@@ -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]
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user