From af350c3a8b2c4ceae450c6ec5a3915bb157999e7 Mon Sep 17 00:00:00 2001 From: Shahed Nasser Date: Mon, 20 Jan 2025 17:02:29 +0200 Subject: [PATCH] docs: add search to workflows reference (#11054) * docs: add search to workflows reference * fix error --- .../components/Homepage/TopSection/index.tsx | 5 +- .../app/medusa-workflows-reference/page.mdx | 6 +- www/packages/docs-ui/package.json | 1 + .../components/Card/Layout/Default/index.tsx | 32 ++- .../docs-ui/src/components/Card/index.tsx | 1 + .../src/components/Icons/Book/index.tsx | 63 ----- .../docs-ui/src/components/Icons/index.tsx | 1 - .../src/components/Input/Search/index.tsx | 64 +++++ .../docs-ui/src/components/Kbd/index.tsx | 16 +- .../components/MainNav/DesktopMenu/index.tsx | 4 +- www/packages/docs-ui/src/components/index.ts | 1 + .../src/hooks/use-child-docs/index.tsx | 226 +++++++++++++++--- .../docs-ui/src/utils/get-local-search.ts | 32 +++ www/packages/docs-ui/src/utils/index.ts | 1 + www/yarn.lock | 8 + 15 files changed, 349 insertions(+), 112 deletions(-) delete mode 100644 www/packages/docs-ui/src/components/Icons/Book/index.tsx create mode 100644 www/packages/docs-ui/src/components/Input/Search/index.tsx create mode 100644 www/packages/docs-ui/src/utils/get-local-search.ts diff --git a/www/apps/book/components/Homepage/TopSection/index.tsx b/www/apps/book/components/Homepage/TopSection/index.tsx index c80bc9923d..bce66a118a 100644 --- a/www/apps/book/components/Homepage/TopSection/index.tsx +++ b/www/apps/book/components/Homepage/TopSection/index.tsx @@ -1,5 +1,6 @@ import clsx from "clsx" -import { BookIcon, Card, IconHeadline, WindowPaintbrushIcon } from "docs-ui" +import { Card, IconHeadline, WindowPaintbrushIcon } from "docs-ui" +import { Book } from "@medusajs/icons" import { basePathUrl } from "../../../utils/base-path-url" import HomepageCodeTabs from "../CodeTabs" @@ -35,7 +36,7 @@ const HomepageTopSection = () => { >
- } /> + } />

Learn how to build Medusa projects. Explore our guides.

diff --git a/www/apps/resources/app/medusa-workflows-reference/page.mdx b/www/apps/resources/app/medusa-workflows-reference/page.mdx index 11c0140621..89d89edd45 100644 --- a/www/apps/resources/app/medusa-workflows-reference/page.mdx +++ b/www/apps/resources/app/medusa-workflows-reference/page.mdx @@ -20,4 +20,8 @@ Through this reference, you'll learn about the available workflows and steps in All workflows and steps in this reference are exported by the `@medusajs/medusa/core-flows` package. - + diff --git a/www/packages/docs-ui/package.json b/www/packages/docs-ui/package.json index 1550855c25..e8e4243d4b 100644 --- a/www/packages/docs-ui/package.json +++ b/www/packages/docs-ui/package.json @@ -67,6 +67,7 @@ "algoliasearch": "^5.2.1", "framer-motion": "^11.11.9", "mermaid": "^10.9.0", + "minisearch": "^7.1.1", "npm-to-yarn": "^2.1.0", "prism-react-renderer": "2.4.0", "react": "rc", diff --git a/www/packages/docs-ui/src/components/Card/Layout/Default/index.tsx b/www/packages/docs-ui/src/components/Card/Layout/Default/index.tsx index de31304980..21303adf19 100644 --- a/www/packages/docs-ui/src/components/Card/Layout/Default/index.tsx +++ b/www/packages/docs-ui/src/components/Card/Layout/Default/index.tsx @@ -17,9 +17,35 @@ export const CardDefaultLayout = ({ children, badge, rightIcon: RightIconComponent, + highlightText = [], }: CardProps) => { const isExternal = useIsExternalLink({ href }) + const getHighlightedText = (textToHighlight: string) => { + if (!highlightText.length) { + return textToHighlight + } + + const parts = textToHighlight.split( + new RegExp(`(${highlightText.join("|")})`, "gi") + ) + return parts.map((part, index) => { + const isHighlighted = highlightText.some((highlight) => { + return part.toLowerCase() === highlight.toLowerCase() + }) + return isHighlighted ? ( + + {part} + + ) : ( + part + ) + }) + } + return (
{title && (
- {title} + {getHighlightedText(title)}
)} {text && ( - {text} + + {getHighlightedText(text)} + )} {children}
diff --git a/www/packages/docs-ui/src/components/Card/index.tsx b/www/packages/docs-ui/src/components/Card/index.tsx index c710ea177a..102b339555 100644 --- a/www/packages/docs-ui/src/components/Card/index.tsx +++ b/www/packages/docs-ui/src/components/Card/index.tsx @@ -23,6 +23,7 @@ export type CardProps = { iconClassName?: string children?: React.ReactNode badge?: BadgeProps + highlightText?: string[] } export const Card = ({ type = "default", ...props }: CardProps) => { diff --git a/www/packages/docs-ui/src/components/Icons/Book/index.tsx b/www/packages/docs-ui/src/components/Icons/Book/index.tsx deleted file mode 100644 index 11ac7b0518..0000000000 --- a/www/packages/docs-ui/src/components/Icons/Book/index.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import React from "react" -import { IconProps } from "@medusajs/icons/dist/types" - -export const BookIcon = (props: IconProps) => { - return ( - - - - - - - - - - - - - - - - ) -} diff --git a/www/packages/docs-ui/src/components/Icons/index.tsx b/www/packages/docs-ui/src/components/Icons/index.tsx index 8df127ecc2..d33254ed11 100644 --- a/www/packages/docs-ui/src/components/Icons/index.tsx +++ b/www/packages/docs-ui/src/components/Icons/index.tsx @@ -1,5 +1,4 @@ export * from "./AiAssistant" -export * from "./Book" export * from "./CalendarRefresh" export * from "./ChefHat" export * from "./CircleDottedLine" diff --git a/www/packages/docs-ui/src/components/Input/Search/index.tsx b/www/packages/docs-ui/src/components/Input/Search/index.tsx new file mode 100644 index 0000000000..a9a69b45af --- /dev/null +++ b/www/packages/docs-ui/src/components/Input/Search/index.tsx @@ -0,0 +1,64 @@ +"use client" + +import { MagnifyingGlass, XMark } from "@medusajs/icons" +import clsx from "clsx" +import React from "react" +import { useKeyboardShortcut } from "../../../hooks" +import { Kbd } from "../../Kbd" + +type SearchInputProps = { + onChange: (value: string) => void +} & Omit, "onChange"> + +export const SearchInput = ({ + value, + onChange, + className, + placeholder = "Search...", + ...props +}: SearchInputProps) => { + useKeyboardShortcut({ + metakey: false, + shortcutKeys: ["escape"], + action: () => onChange(""), + checkEditing: false, + preventDefault: true, + }) + + return ( +
+
+ + onChange(e.target.value)} + {...props} + /> + {value && ( + + )} +
+ + esc + Clear Search + +
+ ) +} diff --git a/www/packages/docs-ui/src/components/Kbd/index.tsx b/www/packages/docs-ui/src/components/Kbd/index.tsx index 60acc5cece..155f79722d 100644 --- a/www/packages/docs-ui/src/components/Kbd/index.tsx +++ b/www/packages/docs-ui/src/components/Kbd/index.tsx @@ -1,9 +1,16 @@ import React from "react" import clsx from "clsx" -export type KbdProps = React.ComponentProps<"kbd"> +export type KbdProps = React.ComponentProps<"kbd"> & { + variant?: "default" | "small" +} -export const Kbd = ({ children, className, ...props }: KbdProps) => { +export const Kbd = ({ + children, + className, + variant = "default", + ...props +}: KbdProps) => { return ( { "py-0 px-docs_0.25", "bg-medusa-bg-field", "text-medusa-fg-subtle", - "text-compact-x-small-plus font-base shadow-none", + "font-base shadow-none", + variant === "small" + ? "text-compact-x-small" + : "text-compact-x-small-plus", className )} {...props} diff --git a/www/packages/docs-ui/src/components/MainNav/DesktopMenu/index.tsx b/www/packages/docs-ui/src/components/MainNav/DesktopMenu/index.tsx index b6e5cfb18a..7b70a47e90 100644 --- a/www/packages/docs-ui/src/components/MainNav/DesktopMenu/index.tsx +++ b/www/packages/docs-ui/src/components/MainNav/DesktopMenu/index.tsx @@ -2,13 +2,13 @@ import { BarsThree, + Book, QuestionMarkCircle, SidebarLeft, TimelineVertical, } from "@medusajs/icons" import React, { useMemo, useRef, useState } from "react" import { - BookIcon, Button, getOsShortcut, Menu, @@ -41,7 +41,7 @@ export const MainNavDesktopMenu = () => { }, { type: "link", - icon: , + icon: , title: "Medusa v1", link: "https://docs.medusajs.com/v1", }, diff --git a/www/packages/docs-ui/src/components/index.ts b/www/packages/docs-ui/src/components/index.ts index 10562fb049..32fbe2e413 100644 --- a/www/packages/docs-ui/src/components/index.ts +++ b/www/packages/docs-ui/src/components/index.ts @@ -31,6 +31,7 @@ export * from "./InlineIcon" export * from "./InlineThemeImage" export * from "./InlineCode" export * from "./Input/Text" +export * from "./Input/Search" export * from "./Kbd" export * from "./Label" export * from "./LearningPath" diff --git a/www/packages/docs-ui/src/hooks/use-child-docs/index.tsx b/www/packages/docs-ui/src/hooks/use-child-docs/index.tsx index ca693560ad..dda0b50ae9 100644 --- a/www/packages/docs-ui/src/hooks/use-child-docs/index.tsx +++ b/www/packages/docs-ui/src/hooks/use-child-docs/index.tsx @@ -1,21 +1,25 @@ "use client" -import React, { useCallback, useMemo } from "react" +import React, { useCallback, useEffect, useMemo, useState } from "react" import { Card, CardList, + getLocalSearch, H2, H3, H4, Hr, isSidebarItemLink, + LocalSearch, MarkdownContent, + SearchInput, + useIsBrowser, useSidebar, } from "../.." import { InteractiveSidebarItem, SidebarItem, SidebarItemLink } from "types" import slugify from "slugify" import { MDXComponents } from "../.." -import { ChevronDoubleRight } from "@medusajs/icons" +import { ChevronDoubleRight, ExclamationCircle } from "@medusajs/icons" type HeadingComponent = ( props: React.HTMLAttributes @@ -32,6 +36,11 @@ export type UseChildDocsProps = { childLevel?: number itemsPerRow?: number defaultItemsPerRow?: number + search?: { + enable: boolean + storageKey?: string + placeholder?: string + } } export const useChildDocs = ({ @@ -45,8 +54,18 @@ export const useChildDocs = ({ childLevel = 1, itemsPerRow, defaultItemsPerRow, + search: { + enable: enableSearch = false, + storageKey = "child-docs", + ...searchProps + } = { enable: false }, }: UseChildDocsProps) => { const { currentItems, activeItem } = useSidebar() + const { isBrowser } = useIsBrowser() + const [searchQuery, setSearchQuery] = useState("") + const [localSearch, setLocalSearch] = useState< + LocalSearch | undefined + >() const TitleHeaderComponent = useCallback( (level: number): HeadingComponent => { switch (level) { @@ -92,16 +111,11 @@ export const useChildDocs = ({ } } - const filterItems = (items: SidebarItem[]): SidebarItem[] => { - return items - .filter(filterCondition) + const filterItems = (items: SidebarItem[]): InteractiveSidebarItem[] => { + return (items.filter(filterCondition) as InteractiveSidebarItem[]) .map((item) => Object.assign({}, item)) .map((item) => { - if ( - item.type !== "separator" && - item.children && - filterType === "hide" - ) { + if (item.children && filterType === "hide") { item.children = filterItems(item.children) } @@ -109,25 +123,6 @@ export const useChildDocs = ({ }) } - const filteredItems = useMemo(() => { - const targetItems = - type === "sidebar" - ? currentItems - ? Object.assign({}, currentItems) - : undefined - : { - default: [...(activeItem?.children || [])], - } - if (filterType === "all" || !targetItems) { - return targetItems - } - - return { - ...targetItems, - default: filterItems(targetItems.default), - } - }, [currentItems, type, activeItem, filterItems]) - const filterNonInteractiveItems = ( items: SidebarItem[] | undefined ): InteractiveSidebarItem[] => { @@ -164,11 +159,115 @@ export const useChildDocs = ({ return childrenResult } - const getTopLevelElms = (items?: SidebarItem[]) => { + const filteredItems = useMemo(() => { + let targetItems = + type === "sidebar" + ? currentItems + ? Object.assign({}, currentItems) + : undefined + : { + default: [...(activeItem?.children || [])], + } + if (filterType !== "all" && targetItems) { + targetItems = { + ...targetItems, + default: filterItems(targetItems.default), + } + } + + return targetItems + ? { + ...targetItems, + default: filterNonInteractiveItems(targetItems?.default), + } + : undefined + }, [currentItems, type, activeItem, filterType]) + + const searchableItems = useMemo(() => { + const searchableItems: SidebarItemLink[] = [] + if (!enableSearch) { + return searchableItems + } + if (onlyTopLevel) { + filteredItems?.default?.forEach((item) => { + if (isSidebarItemLink(item)) { + searchableItems.push(item) + } else { + const firstChild = item.children?.find((child) => + isSidebarItemLink(child) + ) + if (firstChild) { + searchableItems.push(firstChild as SidebarItemLink) + } + } + }) + } else { + filteredItems?.default?.forEach((item) => { + const childItems: SidebarItemLink[] = + (getChildrenForLevel(item)?.filter((childItem) => { + return isSidebarItemLink(childItem) + }) as SidebarItemLink[]) || [] + searchableItems.push(...childItems) + }) + } + + return searchableItems + }, [filteredItems, onlyTopLevel, enableSearch]) + + useEffect(() => { + if (!enableSearch && localSearch) { + setLocalSearch(undefined) + return + } + if (!enableSearch || !searchableItems?.length || localSearch) { + return + } + + setLocalSearch( + getLocalSearch({ + docs: searchableItems, + searchableFields: ["title", "description"], + options: { + storeFields: ["title", "description", "path", "type"], + searchOptions: { + boost: { title: 2 }, + prefix: true, + fuzzy: 0.2, + }, + idField: "path", + }, + }) + ) + }, [searchableItems, enableSearch, localSearch]) + + const searchResult = useMemo(() => { + return localSearch?.search(searchQuery) || [] + }, [localSearch, searchQuery]) + + useEffect(() => { + if (!isBrowser || !enableSearch) { + return + } + + const storedQuery = localStorage.getItem(`${storageKey}-query`) + if (storedQuery) { + setSearchQuery(storedQuery) + } + }, [isBrowser, storageKey, enableSearch]) + + useEffect(() => { + if (!isBrowser || !enableSearch) { + return + } + + localStorage.setItem(`${storageKey}-query`, searchQuery) + }, [isBrowser, searchQuery, storageKey, enableSearch]) + + const getTopLevelElms = (items?: InteractiveSidebarItem[]) => { return ( { + items?.map((childItem) => { const href = isSidebarItemLink(childItem) ? childItem.path : childItem.children?.length @@ -193,11 +292,10 @@ export const useChildDocs = ({ } const getAllLevelsElms = ( - items?: SidebarItem[], + items?: InteractiveSidebarItem[], headerLevel = titleLevel ) => { - const filteredItems = filterNonInteractiveItems(items) - return filteredItems.map((item, key) => { + return items?.map((item, key) => { const itemChildren = getChildrenForLevel(item) const HeadingComponent = itemChildren?.length ? TitleHeaderComponent(headerLevel) @@ -243,7 +341,7 @@ export const useChildDocs = ({ defaultItemsPerRow={defaultItemsPerRow} /> )} - {key !== filteredItems.length - 1 && headerLevel === 2 &&
} + {key !== items.length - 1 && headerLevel === 2 &&
} )} {!HeadingComponent && isSidebarItemLink(item) && ( @@ -259,12 +357,64 @@ export const useChildDocs = ({ }) } - const getElms = (items?: SidebarItem[]) => { - return onlyTopLevel ? getTopLevelElms(items) : getAllLevelsElms(items) + const getSearchResultElms = () => { + const Heading = TitleHeaderComponent(titleLevel) + return ( + <> + Search Results + {searchResult.length > 0 && ( + ({ + title: item.title, + href: item.path, + text: item.description, + rightIcon: item.type === "ref" ? ChevronDoubleRight : undefined, + highlightText: item.terms, + }))} + itemsPerRow={itemsPerRow} + defaultItemsPerRow={defaultItemsPerRow} + className="my-docs_2" + /> + )} + {!searchResult.length && ( +
+ + + No results found matching your query. + + + Try searching with another term or clearing the search. + +
+ )} + + ) + } + + const getElms = () => { + return ( + <> + {enableSearch && ( + + )} + {searchQuery && getSearchResultElms()} + {!searchQuery && ( + <> + {onlyTopLevel + ? getTopLevelElms(filteredItems?.default) + : getAllLevelsElms(filteredItems?.default)} + + )} + + ) } return { items: filteredItems, - component: getElms(filteredItems?.default), + component: getElms(), } } diff --git a/www/packages/docs-ui/src/utils/get-local-search.ts b/www/packages/docs-ui/src/utils/get-local-search.ts new file mode 100644 index 0000000000..00c753f524 --- /dev/null +++ b/www/packages/docs-ui/src/utils/get-local-search.ts @@ -0,0 +1,32 @@ +import MiniSearch, { Options as MiniSearchOptions } from "minisearch" + +type BaseSearchRecord = Record + +type GetLocalSearchInput = { + docs: T[] + searchableFields: string[] + options?: Omit +} + +type SearchResult = (T & { + terms?: string[] +})[] + +export type LocalSearch = + MiniSearch & { + search: (query: string) => SearchResult + } + +export const getLocalSearch = ({ + docs, + searchableFields, + options, +}: GetLocalSearchInput): LocalSearch => { + const miniSearch = new MiniSearch({ + fields: searchableFields, + ...options, + }) + miniSearch.addAll(docs) + + return miniSearch as LocalSearch +} diff --git a/www/packages/docs-ui/src/utils/index.ts b/www/packages/docs-ui/src/utils/index.ts index ab26e6101e..54988860ad 100644 --- a/www/packages/docs-ui/src/utils/index.ts +++ b/www/packages/docs-ui/src/utils/index.ts @@ -4,6 +4,7 @@ export * from "./check-sidebar-item-visibility" export * from "./decode-str" export * from "./dom-utils" export * from "./get-link-with-base-path" +export * from "./get-local-search" export * from "./get-navbar-items" export * from "./os-browser-utils" export * from "./get-scrolled-top" diff --git a/www/yarn.lock b/www/yarn.lock index a64ba78fe4..83c6850c5b 100644 --- a/www/yarn.lock +++ b/www/yarn.lock @@ -8445,6 +8445,7 @@ __metadata: eslint: ^9.13.0 framer-motion: ^11.11.9 mermaid: ^10.9.0 + minisearch: ^7.1.1 next: 15.0.4 npm-to-yarn: ^2.1.0 prism-react-renderer: 2.4.0 @@ -13011,6 +13012,13 @@ __metadata: languageName: node linkType: hard +"minisearch@npm:^7.1.1": + version: 7.1.1 + resolution: "minisearch@npm:7.1.1" + checksum: a601963ae5fa3b2e884278c92f614187651f2734e248cb564236258cb307cbe6aab2f985962f77939a6255da123d625e38ff6d72fa9c4164ac3e49477fbad9f5 + languageName: node + linkType: hard + "minizlib@npm:^2.1.1, minizlib@npm:^2.1.2": version: 2.1.2 resolution: "minizlib@npm:2.1.2"