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",