fix: Improve shared command bar search results and add recent docs (#11849)

Show all search results by passing keywords to Fuse.js, display search
context as subtitle, track recently viewed documents for empty state,
and move SharedSearchActions outside KBarPortal to prevent mount flicker.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Tom Moor
2026-03-22 18:49:43 -04:00
committed by GitHub
parent 0ba310e027
commit 4a40712dcc
7 changed files with 101 additions and 24 deletions
+3
View File
@@ -210,6 +210,7 @@ export function actionToKBar(
const name = resolve<string>(action.name, context);
const icon = resolve<React.ReactElement>(action.icon, context);
const section = resolve<string>(action.section, context);
const subtitle = resolve<string>(action.description, context);
const sectionPriority =
typeof action.section !== "string" && "priority" in action.section
@@ -229,6 +230,7 @@ export function actionToKBar(
section,
keywords: action.keywords,
shortcut: action.shortcut,
subtitle,
icon,
priority,
perform: () => performAction(action, context),
@@ -254,6 +256,7 @@ export function actionToKBar(
keywords: action.keywords,
shortcut: action.shortcut,
icon,
subtitle,
priority,
},
...children.map((child) => ({
+4 -1
View File
@@ -15,6 +15,9 @@ export const DeveloperSection = ({ t }: ActionContext) => t("Debug");
export const DocumentSection = ({ t }: ActionContext) => t("Document");
export const SearchResultsSection = ({ t }: ActionContext) =>
t("Search results");
export const DocumentsSection = ({ t }: ActionContext) => t("Documents");
export const ActiveDocumentSection = ({ t, stores }: ActionContext) => {
@@ -58,7 +61,7 @@ export const ShareSection = ({ t }: ActionContext) => t("Share");
export const TeamSection = ({ t }: ActionContext) => t("Workspace");
export const RecentSearchesSection = ({ t }: ActionContext) =>
t("Recent searches");
t("Recently viewed");
RecentSearchesSection.priority = -0.1;
@@ -4,6 +4,7 @@ import * as React from "react";
import styled, { css, useTheme } from "styled-components";
import { s, ellipsis } from "@shared/styles";
import { normalizeKeyDisplay } from "@shared/utils/keyboard";
import Highlight from "~/components/Highlight";
import Flex from "~/components/Flex";
import Key from "~/components/Key";
import Text from "~/components/Text";
@@ -15,6 +16,14 @@ type Props = {
currentRootActionId: string | null | undefined;
};
const SEARCH_RESULT_REGEX = /<b\b[^>]*>(.*?)<\/b>/gi;
function replaceResultMarks(tag: string) {
// don't use SEARCH_RESULT_REGEX here as it causes
// an infinite loop to trigger a regex inside it's own callback
return tag.replace(/<b\b[^>]*>(.*?)<\/b>/gi, "$1");
}
function CommandBarItem(
{ action, active, currentRootActionId }: Props,
ref: React.RefObject<HTMLDivElement>
@@ -56,6 +65,16 @@ function CommandBarItem(
))}
{action.name}
{action.children?.length ? "…" : ""}
{action.subtitle && (
<Text type="secondary" ellipsis>
&nbsp;&nbsp;
<Highlight
text={action.subtitle}
highlight={SEARCH_RESULT_REGEX}
processResult={replaceResultMarks}
/>
</Text>
)}
</Content>
{action.shortcut?.length ? (
<Shortcut>
+11 -9
View File
@@ -16,15 +16,17 @@ function SharedCommandBar() {
const { t } = useTranslation();
return (
<KBarPortal>
<Positioner>
<Animator>
<SharedSearchActions />
<SearchInput defaultPlaceholder={`${t("Search")}`} />
<CommandBarResults />
</Animator>
</Positioner>
</KBarPortal>
<>
<SharedSearchActions />
<KBarPortal>
<Positioner>
<Animator>
<SearchInput defaultPlaceholder={`${t("Search")}`} />
<CommandBarResults />
</Animator>
</Positioner>
</KBarPortal>
</>
);
}
@@ -6,9 +6,13 @@ import Icon from "@shared/components/Icon";
import useShare from "@shared/hooks/useShare";
import { Minute } from "@shared/utils/time";
import { createAction } from "~/actions";
import { DocumentSection } from "~/actions/sections";
import {
RecentSearchesSection,
SearchResultsSection,
} from "~/actions/sections";
import useCommandBarActions from "~/hooks/useCommandBarActions";
import useStores from "~/hooks/useStores";
import type Document from "~/models/Document";
import history from "~/utils/history";
import { sharedModelPath } from "~/utils/routeHelpers";
import type { SearchResult } from "~/types";
@@ -19,6 +23,7 @@ interface CacheEntry {
}
const cacheTTL = Minute.ms * 5;
const maxRecentDocs = 5;
/**
* Registers search result actions in the command bar scoped to a public share.
@@ -28,6 +33,8 @@ function SharedSearchActions() {
const { shareId } = useShare();
const searchCache = React.useRef<Map<string, CacheEntry>>(new Map());
const [results, setResults] = React.useState<SearchResult[]>([]);
const recentDocsRef = React.useRef<Document[]>([]);
const [recentDocs, setRecentDocs] = React.useState<Document[]>([]);
const { searchQuery } = useKBar((state) => ({
searchQuery: state.searchQuery,
@@ -57,25 +64,42 @@ function SharedSearchActions() {
});
}, [documents, searchQuery, shareId]);
const addRecentDoc = React.useCallback((doc: Document) => {
const prev = recentDocsRef.current;
const filtered = prev.filter((d) => d.id !== doc.id);
const next = [doc, ...filtered].slice(0, maxRecentDocs);
recentDocsRef.current = next;
setRecentDocs(next);
}, []);
const documentIcon = React.useCallback(
(doc: Document) =>
doc.icon ? (
<Icon
value={doc.icon}
initial={doc.initial}
color={doc.color ?? undefined}
/>
) : (
<DocumentIcon />
),
[]
);
const actions = React.useMemo(
() =>
results.map((result) =>
createAction({
id: `shared-search-${result.document.id}`,
name: result.document.titleWithDefault,
description: result.context,
keywords: searchQuery,
analyticsName: "Open shared search result",
section: DocumentSection,
icon: result.document.icon ? (
<Icon
value={result.document.icon}
initial={result.document.initial}
color={result.document.color ?? undefined}
/>
) : (
<DocumentIcon />
),
section: SearchResultsSection,
icon: documentIcon(result.document),
perform: () => {
if (shareId) {
addRecentDoc(result.document);
history.push({
pathname: sharedModelPath(shareId, result.document.url),
search: searchQuery
@@ -86,10 +110,34 @@ function SharedSearchActions() {
},
})
),
[results, shareId, searchQuery]
[results, shareId, searchQuery, addRecentDoc, documentIcon]
);
useCommandBarActions(actions, [actions.map((a) => a.id).join("")]);
const recentDocActions = React.useMemo(
() =>
recentDocs.map((doc) =>
createAction({
id: `shared-recent-doc-${doc.id}`,
name: doc.titleWithDefault,
analyticsName: "Open recent shared document",
section: RecentSearchesSection,
icon: documentIcon(doc),
perform: () => {
if (shareId) {
history.push(sharedModelPath(shareId, doc.url));
}
},
})
),
[recentDocs, shareId, documentIcon]
);
useCommandBarActions(searchQuery ? actions : recentDocActions, [
searchQuery
? actions.map((a) => a.id).join("")
: recentDocActions.map((a) => a.id).join(""),
searchQuery,
]);
return null;
}
+1
View File
@@ -138,6 +138,7 @@ type BaseAction = {
analyticsName?: string;
name: ((context: ActionContext) => React.ReactNode) | React.ReactNode;
section: ((context: ActionContext) => string) | string;
description?: ((context: ActionContext) => string) | string;
shortcut?: string[];
keywords?: string;
/** Higher number is higher in results, default is 0. */
+2 -1
View File
@@ -187,6 +187,7 @@
"Collections": "Collections",
"Debug": "Debug",
"Document": "Document",
"Search results": "Search results",
"Documents": "Documents",
"Template": "Template",
"Recently viewed": "Recently viewed",
@@ -198,7 +199,6 @@
"People": "People",
"Share": "Share",
"Workspace": "Workspace",
"Recent searches": "Recent searches",
"currently editing": "currently editing",
"currently viewing": "currently viewing",
"previously edited": "previously edited",
@@ -1042,6 +1042,7 @@
"Any time": "Any time",
"Remove document filter": "Remove document filter",
"Any status": "Any status",
"Recent searches": "Recent searches",
"Remove search": "Remove search",
"Relevance": "Relevance",
"Newest": "Newest",