From 4a40712dccf30dfd9ec95709fbbb5a1540e8f891 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Sun, 22 Mar 2026 18:49:43 -0400 Subject: [PATCH] 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 --- app/actions/index.ts | 3 + app/actions/sections.ts | 5 +- app/components/CommandBar/CommandBarItem.tsx | 19 +++++ .../CommandBar/SharedCommandBar.tsx | 20 ++--- .../CommandBar/SharedSearchActions.tsx | 74 +++++++++++++++---- app/types.ts | 1 + shared/i18n/locales/en_US/translation.json | 3 +- 7 files changed, 101 insertions(+), 24 deletions(-) diff --git a/app/actions/index.ts b/app/actions/index.ts index 26c38e364c..dd557e573b 100644 --- a/app/actions/index.ts +++ b/app/actions/index.ts @@ -210,6 +210,7 @@ export function actionToKBar( const name = resolve(action.name, context); const icon = resolve(action.icon, context); const section = resolve(action.section, context); + const subtitle = resolve(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) => ({ diff --git a/app/actions/sections.ts b/app/actions/sections.ts index ef55a448c7..fa5190d736 100644 --- a/app/actions/sections.ts +++ b/app/actions/sections.ts @@ -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; diff --git a/app/components/CommandBar/CommandBarItem.tsx b/app/components/CommandBar/CommandBarItem.tsx index 1301d117ac..cb18da84ac 100644 --- a/app/components/CommandBar/CommandBarItem.tsx +++ b/app/components/CommandBar/CommandBarItem.tsx @@ -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>/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>/gi, "$1"); +} + function CommandBarItem( { action, active, currentRootActionId }: Props, ref: React.RefObject @@ -56,6 +65,16 @@ function CommandBarItem( ))} {action.name} {action.children?.length ? "…" : ""} + {action.subtitle && ( + +    + + + )} {action.shortcut?.length ? ( diff --git a/app/components/CommandBar/SharedCommandBar.tsx b/app/components/CommandBar/SharedCommandBar.tsx index 125733f650..531a252bb3 100644 --- a/app/components/CommandBar/SharedCommandBar.tsx +++ b/app/components/CommandBar/SharedCommandBar.tsx @@ -16,15 +16,17 @@ function SharedCommandBar() { const { t } = useTranslation(); return ( - - - - - - - - - + <> + + + + + + + + + + ); } diff --git a/app/components/CommandBar/SharedSearchActions.tsx b/app/components/CommandBar/SharedSearchActions.tsx index 3669234e54..c29ed109c2 100644 --- a/app/components/CommandBar/SharedSearchActions.tsx +++ b/app/components/CommandBar/SharedSearchActions.tsx @@ -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>(new Map()); const [results, setResults] = React.useState([]); + const recentDocsRef = React.useRef([]); + const [recentDocs, setRecentDocs] = React.useState([]); 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 ? ( + + ) : ( + + ), + [] + ); + 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 ? ( - - ) : ( - - ), + 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; } diff --git a/app/types.ts b/app/types.ts index de7c78b87f..27d476e8e6 100644 --- a/app/types.ts +++ b/app/types.ts @@ -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. */ diff --git a/shared/i18n/locales/en_US/translation.json b/shared/i18n/locales/en_US/translation.json index 3450069eb8..23acaea211 100644 --- a/shared/i18n/locales/en_US/translation.json +++ b/shared/i18n/locales/en_US/translation.json @@ -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",