From a0417ce71dafc97bcb06e32df7a2cf38632bff95 Mon Sep 17 00:00:00 2001 From: dphuang2 Date: Mon, 13 Nov 2023 19:53:44 -0500 Subject: [PATCH] [ENG-1295] Implement a search bar for the API Portal (#293) * save save re-run yarn * upgrade fuse.js * TODO: integrate fuse.js for searching * fix outline color * mod +k for search * style search button TODO: add actual search functionality * styling * pass allMarkdown to Search.tsx * basic search + UI working * add operations to search index * add demos to search index * ensure styling looks good on light mode * search is available on mobile * fix key for children * fix forward ref bug --- generator/konfig-next-app/package.json | 3 + .../src/components/DemoHeader.tsx | 10 +- .../src/components/DemoPortal.tsx | 4 + .../src/components/DocumentationHeader.tsx | 10 +- .../src/components/HeaderButton.tsx | 3 +- .../src/components/HeaderTabs.tsx | 161 ++++++++-------- .../src/components/HeaderWrapper.tsx | 12 +- .../src/components/HighlightTextComponent.tsx | 88 +++++++++ .../src/components/LayoutHeader.tsx | 7 + .../src/components/NavbarLink.tsx | 12 +- .../src/components/ReferenceHeader.tsx | 10 +- .../konfig-next-app/src/components/Search.tsx | 43 +++++ .../src/components/SpotlightProvider.tsx | 177 ++++++++++++++++++ .../src/pages/[org]/[portal]/[demo].tsx | 2 + .../pages/[org]/[portal]/docs/[[...doc]].tsx | 3 + .../reference/[tag]/[operationId].tsx | 2 + .../konfig-next-app/src/pages/sandbox.tsx | 1 + .../src/utils/compute-document-props.ts | 175 +++++++++++++++++ .../src/utils/compute-fuse-index.ts | 46 +++++ .../src/utils/generate-demos-from-github.ts | 19 +- .../src/utils/generate-navbar-links.ts | 18 +- .../src/utils/generate-props-for-demo-page.ts | 2 +- .../utils/generate-props-for-markdown-page.ts | 51 +---- .../generate-props-for-reference-page.ts | 17 ++ .../src/utils/github-get-konfig-yaml.ts | 20 ++ .../src/utils/github-get-openapi-spec.ts | 36 ++++ .../utils/github-get-reference-resources.ts | 29 +-- .../src/utils/is-operation-hidden.ts | 26 +++ generator/konfig-next-app/yarn.lock | 29 +++ 29 files changed, 845 insertions(+), 171 deletions(-) create mode 100644 generator/konfig-next-app/src/components/HighlightTextComponent.tsx create mode 100644 generator/konfig-next-app/src/components/Search.tsx create mode 100644 generator/konfig-next-app/src/components/SpotlightProvider.tsx create mode 100644 generator/konfig-next-app/src/utils/compute-document-props.ts create mode 100644 generator/konfig-next-app/src/utils/compute-fuse-index.ts create mode 100644 generator/konfig-next-app/src/utils/github-get-konfig-yaml.ts create mode 100644 generator/konfig-next-app/src/utils/github-get-openapi-spec.ts create mode 100644 generator/konfig-next-app/src/utils/is-operation-hidden.ts diff --git a/generator/konfig-next-app/package.json b/generator/konfig-next-app/package.json index 7c5cdfd45..6e3a87128 100644 --- a/generator/konfig-next-app/package.json +++ b/generator/konfig-next-app/package.json @@ -28,6 +28,7 @@ "@mantine/next": "^6.0.13", "@mantine/notifications": "^6.0.13", "@mantine/prism": "^6.0.13", + "@mantine/spotlight": "^6.0.13", "@octokit/auth-app": "^6.0.0", "@octokit/rest": "^20.0.1", "@tabler/icons-react": "^2.21.0", @@ -48,6 +49,7 @@ "deepmerge": "^4.3.1", "eslint": "8.41.0", "eslint-config-next": "13.4.4", + "fuse.js": "^7.0.0", "github-slugger": "^2.0.0", "hast-util-to-text": "^3.1.2", "httpsnippet": "^3.0.1", @@ -77,6 +79,7 @@ "unist-util-position": "^4.0.4", "unist-util-stringify-position": "^3.0.3", "unist-util-visit": "^4.1.2", + "use-deep-compare": "^1.1.0", "uuid": "^9.0.0", "zod": "^3.21.4" }, diff --git a/generator/konfig-next-app/src/components/DemoHeader.tsx b/generator/konfig-next-app/src/components/DemoHeader.tsx index bdd494ada..05077c4ae 100644 --- a/generator/konfig-next-app/src/components/DemoHeader.tsx +++ b/generator/konfig-next-app/src/components/DemoHeader.tsx @@ -6,6 +6,7 @@ import { HeaderWrapper } from './HeaderWrapper' import { TABS } from './HeaderButton' import { HeaderTabs } from './HeaderTabs' import type { generateLogoLink } from '@/utils/generate-logo-link' +import { MarkdownPageProps } from '@/utils/generate-props-for-markdown-page' export const DemoHeader = observer( ({ @@ -19,10 +20,12 @@ export const DemoHeader = observer( owner, repo, logo, + allMarkdown, }: { opened: boolean setOpened: Dispatch> state: PortalState + allMarkdown: MarkdownPageProps['allMarkdown'] sandbox?: boolean hasDocumentation: boolean demos: string[] @@ -32,8 +35,12 @@ export const DemoHeader = observer( logo: ReturnType }) => { return ( - + }) => { @@ -315,6 +318,7 @@ export const DemoPortal = observer( repo={repo} omitOwnerAndRepo={omitOwnerAndRepo} hasDocumentation={hasDocumentation} + allMarkdown={allMarkdown} demos={state.demos.map((demo) => demo.id)} opened={opened} setOpened={setOpened} diff --git a/generator/konfig-next-app/src/components/DocumentationHeader.tsx b/generator/konfig-next-app/src/components/DocumentationHeader.tsx index 91f6d5949..7b5f4cfd8 100644 --- a/generator/konfig-next-app/src/components/DocumentationHeader.tsx +++ b/generator/konfig-next-app/src/components/DocumentationHeader.tsx @@ -5,6 +5,7 @@ import { navbarOffsetBreakpoint } from '@/utils/navbar-offset-breakpoint' import { HeaderTabs } from './HeaderTabs' import { TABS } from './HeaderButton' import type { generateLogoLink } from '@/utils/generate-logo-link' +import { MarkdownPageProps } from '@/utils/generate-props-for-markdown-page' export function DocumentationHeader({ opened, @@ -15,6 +16,7 @@ export function DocumentationHeader({ owner, repo, logo, + allMarkdown, }: { opened: boolean setOpened: Dispatch> @@ -24,10 +26,15 @@ export function DocumentationHeader({ owner: string repo: string logo: ReturnType + allMarkdown: MarkdownPageProps['allMarkdown'] }) { return ( - + ) diff --git a/generator/konfig-next-app/src/components/HeaderButton.tsx b/generator/konfig-next-app/src/components/HeaderButton.tsx index bffba6e08..6720301ee 100644 --- a/generator/konfig-next-app/src/components/HeaderButton.tsx +++ b/generator/konfig-next-app/src/components/HeaderButton.tsx @@ -10,9 +10,10 @@ export const TABS = { sdks: 'SDKs', } as const +export type TabType = typeof TABS export type Tab = (typeof TABS)[keyof typeof TABS] -const ICONS = { +export const ICONS = { [TABS.documentation]: IconBook, [TABS.reference]: IconCode, [TABS.demos]: IconTerminal, diff --git a/generator/konfig-next-app/src/components/HeaderTabs.tsx b/generator/konfig-next-app/src/components/HeaderTabs.tsx index 6a96376d8..1f5703ece 100644 --- a/generator/konfig-next-app/src/components/HeaderTabs.tsx +++ b/generator/konfig-next-app/src/components/HeaderTabs.tsx @@ -1,17 +1,12 @@ -import { - Box, - Flex, - Group, - MediaQuery, - Menu, - useMantineTheme, -} from '@mantine/core' +import { Box, Flex, Group, Menu, clsx, useMantineTheme } from '@mantine/core' import { HeaderTab } from './HeaderTab' import { useBasePath } from '@/utils/use-base-path' import { HeaderButton, TABS, Tab } from './HeaderButton' import { IconMenu } from '@tabler/icons-react' import Link from 'next/link' import { getClickableStyles } from '@/utils/get-clickable-styles' +import { Search } from './Search' +import { MarkdownPageProps } from '@/utils/generate-props-for-markdown-page' export function HeaderTabs({ currentTab, @@ -20,6 +15,7 @@ export function HeaderTabs({ hasDocumentation, omitOwnerAndRepo, owner, + allMarkdown, repo, hasLightAndDarkLogo, }: { @@ -31,6 +27,7 @@ export function HeaderTabs({ repo: string hasLightAndDarkLogo: boolean omitOwnerAndRepo?: boolean + allMarkdown: MarkdownPageProps['allMarkdown'] }) { const docsPath = useDocsPath({ omitOwnerAndRepo }) const referencePath = useReferencePath({ omitOwnerAndRepo }) @@ -51,84 +48,83 @@ export function HeaderTabs({ const theme = useMantineTheme() return ( - - - - - - - - - - - - {Object.values(TABS) - .filter((tab) => tab !== currentTab) - .map((tab) => { - if (tab === TABS.documentation && !hasDocumentation) { - return null - } - if (tab === TABS.demos && demos.length === 0) { - return null - } - return ( - - - - - - ) - })} - - - - - - + + + + + + + + + + {Object.values(TABS) + .filter((tab) => tab !== currentTab) + .map((tab) => { + if (tab === TABS.documentation && !hasDocumentation) { + return null + } + if (tab === TABS.demos && demos.length === 0) { + return null + } + return ( + + + + + + ) + })} + + + + ) } diff --git a/generator/konfig-next-app/src/components/HeaderWrapper.tsx b/generator/konfig-next-app/src/components/HeaderWrapper.tsx index 0a6aebb39..4541552c1 100644 --- a/generator/konfig-next-app/src/components/HeaderWrapper.tsx +++ b/generator/konfig-next-app/src/components/HeaderWrapper.tsx @@ -1,11 +1,17 @@ import { Header, useMantineTheme } from '@mantine/core' import { PropsWithChildren } from 'react' import { TITLE_OFFSET_PX } from './DemoTitle' +import { MarkdownPageProps } from '@/utils/generate-props-for-markdown-page' +import { SpotlightProvider } from './SpotlightProvider' export function HeaderWrapper({ children, hasLightAndDarkLogo, -}: PropsWithChildren<{ hasLightAndDarkLogo: boolean }>) { + allMarkdown, +}: PropsWithChildren<{ + hasLightAndDarkLogo: boolean + allMarkdown: MarkdownPageProps['allMarkdown'] +}>) { const theme = useMantineTheme() return (
- {children} + + {children} +
) } diff --git a/generator/konfig-next-app/src/components/HighlightTextComponent.tsx b/generator/konfig-next-app/src/components/HighlightTextComponent.tsx new file mode 100644 index 000000000..ef3e2531c --- /dev/null +++ b/generator/konfig-next-app/src/components/HighlightTextComponent.tsx @@ -0,0 +1,88 @@ +import React, { PropsWithChildren } from 'react' + +interface HighlightTextProps { + children: string + bold: string[] + className?: string +} + +const HighlightTextComponent: React.FC = ({ + children, + bold, + className, +}) => { + if (bold.length === 0 || (bold.length === 1 && bold[0] === '')) + return {children} + // Function to create a regex from the bold strings + const createRegex = (strings: string[]): RegExp => { + return new RegExp( + strings.map((s) => s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')).join('|'), + 'gi' + ) + } + + // Splitting the children string based on the bold strings + const splitWithBold = ( + text: string, + boldStrings: string[] + ): { matched: boolean; parts: React.ReactNode[] } => { + // empty strings or empty arrays cause issues + const cleaned = boldStrings.filter((s) => s !== '' && s.length > 1) + + if (!cleaned || cleaned.length === 0) + return { matched: false, parts: [text] } + + const regex = createRegex(cleaned) + const parts: React.ReactNode[] = [] + let match: RegExpExecArray | null + let lastIndex = 0 + + let matched = false + let key = 0 + while ((match = regex.exec(text)) !== null) { + matched = true + const matchIndex = match.index + if (lastIndex !== matchIndex) { + parts.push( + + {text.substring(lastIndex, matchIndex)} + + ) + key++ + } + parts.push( + + {match[0]} + + ) + key++ + lastIndex = matchIndex + match[0].length + } + + if (lastIndex < text.length) { + parts.push( + {text.substring(lastIndex)} + ) + key++ + } + + return { parts, matched } + } + + const { parts } = splitWithBold(children, bold) + + return
{parts}
+} + +function Demphasized({ children }: PropsWithChildren<{}>) { + return ( + + {children} + + ) +} + +export default HighlightTextComponent diff --git a/generator/konfig-next-app/src/components/LayoutHeader.tsx b/generator/konfig-next-app/src/components/LayoutHeader.tsx index 557414579..e7a94984f 100644 --- a/generator/konfig-next-app/src/components/LayoutHeader.tsx +++ b/generator/konfig-next-app/src/components/LayoutHeader.tsx @@ -17,6 +17,8 @@ import { useBaseUrl } from '@/utils/use-base-url' import Link from 'next/link' import type { generateLogoLink } from '@/utils/generate-logo-link' import { useHeaderColor } from '@/utils/use-header-color' +import { MarkdownPageProps } from '@/utils/generate-props-for-markdown-page' +import { Search } from './Search' const useLogoStyles = createStyles(() => ({ logo: { @@ -30,6 +32,7 @@ export const LayoutHeader = observer( opened, setOpened, breakpoint, + allMarkdown, logo, }: { title: string @@ -37,6 +40,7 @@ export const LayoutHeader = observer( setOpened: Dispatch> breakpoint: MantineNumberSize logo: ReturnType + allMarkdown: MarkdownPageProps['allMarkdown'] }) => { const theme = useMantineTheme() const baseUrl = useBaseUrl() @@ -101,6 +105,9 @@ export const LayoutHeader = observer( )} +
+ +
diff --git a/generator/konfig-next-app/src/components/NavbarLink.tsx b/generator/konfig-next-app/src/components/NavbarLink.tsx index d23ec6239..c6aa4e5d7 100644 --- a/generator/konfig-next-app/src/components/NavbarLink.tsx +++ b/generator/konfig-next-app/src/components/NavbarLink.tsx @@ -6,14 +6,16 @@ import { useMantineTheme, } from '@mantine/core' import Link, { LinkProps } from 'next/link' -import { RefObject } from 'react' +import { forwardRef } from 'react' -export function NavbarLink( - props: NavLinkProps & LinkProps & { ref?: RefObject } -) { +export const NavbarLink = forwardRef< + HTMLAnchorElement, + NavLinkProps & LinkProps +>((props, ref) => { const theme = useMantineTheme() return ( + ref={ref} {...props} component={Link} styles={{ @@ -23,7 +25,7 @@ export function NavbarLink( }} /> ) -} +}) export function navbarLinkColor({ theme, diff --git a/generator/konfig-next-app/src/components/ReferenceHeader.tsx b/generator/konfig-next-app/src/components/ReferenceHeader.tsx index 9f10208b5..3d1e8db15 100644 --- a/generator/konfig-next-app/src/components/ReferenceHeader.tsx +++ b/generator/konfig-next-app/src/components/ReferenceHeader.tsx @@ -4,6 +4,7 @@ import { HeaderWrapper } from './HeaderWrapper' import { TABS } from './HeaderButton' import { HeaderTabs } from './HeaderTabs' import type { generateLogoLink } from '@/utils/generate-logo-link' +import { MarkdownPageProps } from '@/utils/generate-props-for-markdown-page' export function ReferenceHeader({ opened, @@ -15,6 +16,7 @@ export function ReferenceHeader({ owner, repo, logo, + allMarkdown, }: { opened: boolean hasDocumentation: boolean @@ -23,12 +25,17 @@ export function ReferenceHeader({ title: string omitOwnerAndRepo?: boolean owner: string + allMarkdown: MarkdownPageProps['allMarkdown'] repo: string logo: ReturnType }) { return ( - + spotlight.open()} + className="dark:outline-brand-700 border-zinc-200 hover:border-zinc-300 bg-zinc-50 hover:bg-zinc-100 dark:bg-zinc-950 hover:dark:bg-zinc-900 flex group transition text-zinc-500 dark:text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-200 border gap-24 dark:border-zinc-800 hover:dark:border-zinc-700 p-2 md:p-4 rounded-lg my-auto items-center h-2/3 text-sm" + > + +
+ + Search +
+
+ {os === 'macos' ? '⌘' : 'Ctrl'} + K +
+ + ) +} + +export function Kbd({ + children, + className, +}: PropsWithChildren<{ className?: string }>) { + return ( + + {children} + + ) +} diff --git a/generator/konfig-next-app/src/components/SpotlightProvider.tsx b/generator/konfig-next-app/src/components/SpotlightProvider.tsx new file mode 100644 index 000000000..ddbae4fbe --- /dev/null +++ b/generator/konfig-next-app/src/components/SpotlightProvider.tsx @@ -0,0 +1,177 @@ +import { MarkdownPageProps } from '@/utils/generate-props-for-markdown-page' +import { useOs } from '@mantine/hooks' +import { + SpotlightProvider as MantineSpotlightProvider, + SpotlightAction, + SpotlightActionProps, +} from '@mantine/spotlight' +import Fuse, { IFuseOptions } from 'fuse.js' +import { useRouter } from 'next/router' +import { PropsWithChildren, useState } from 'react' +import { useDeepCompareMemo } from 'use-deep-compare' +import { ICONS, TABS } from './HeaderButton' +import { createStyles, rem, UnstyledButton, Group, Center } from '@mantine/core' +import { IconSearch } from '@tabler/icons-react' +import HighlightTextComponent from './HighlightTextComponent' +import { HttpMethodBadge } from './HttpMethodBadge' + +export function SpotlightProvider({ + allMarkdown, + children, +}: PropsWithChildren<{ + allMarkdown: MarkdownPageProps['allMarkdown'] +}>) { + const options: IFuseOptions = { + keys: ['content', 'title'], + + // Fuzzy finding options + useExtendedSearch: true, // use ' to do substring search + ignoreLocation: true, // distance & location mean nothing + threshold: 0.3, // 0.0 is perfect, 1.0 is match all so 0.3 is stricter than default value of 0.6 + fieldNormWeight: 2, // title has more weight than content + } + const fuse = useDeepCompareMemo(() => { + return new Fuse(allMarkdown, options) + }, [allMarkdown, options]) + const os = useOs() + const router = useRouter() + const actions: SpotlightAction[] = allMarkdown.map((doc) => { + const Icon = ICONS[doc.type] + return { + id: doc.id, + title: doc.title, + content: doc.content, + icon: , + path: doc.type === TABS.reference ? doc.path : '', + method: doc.type === TABS.reference ? doc.method : '', + onTrigger: () => { + if (doc.type === TABS.documentation) { + router.push(`/docs/${doc.id}`) + } else if (doc.type === TABS.reference) { + router.push(`/reference/${doc.tag}/${doc.id}`) + } else if (doc.type === TABS.demos) { + router.push(`/demo/${doc.id}`) + } + }, + } + }) + const [query, setQuery] = useState('') + return ( + { + if (query === '') return actions + const searchResult = fuse.search(query) + // filter/sort actions by search result + const filteredActions = actions + .filter((action) => + searchResult.some((result) => result.item.id === action.id) + ) + .sort((a, b) => { + const aIndex = searchResult.findIndex( + (result) => result.item.id === a.id + ) + const bIndex = searchResult.findIndex( + (result) => result.item.id === b.id + ) + return aIndex - bIndex + }) + // add description if there is a case insensitive substring match between query term and content + const terms = query.split(' ') + filteredActions.forEach((action) => { + const content: string = action.content.toLowerCase() + + for (const term of terms) { + const index = content.indexOf(term.toLowerCase()) + if (index === -1) continue + action.description = content.substring( + Math.max(0, index - 100), + Math.min(content.length, index + term.length + 100) + ) + return + } + }) + + return filteredActions + }} + onQueryChange={setQuery} + limit={15} + searchIcon={} + searchPlaceholder="Search..." + nothingFoundMessage="Nothing found..." + actionComponent={CustomAction} + classNames={ + { + // content: 'dark:bg-zinc-950 rounded-lg', + // searchInput: 'dark:bg-zinc-900', + } + } + > + {children} + + ) +} + +const useStyles = createStyles((theme) => ({ + action: { + position: 'relative', + display: 'block', + width: '100%', + padding: `${rem(10)} ${rem(12)}`, + borderRadius: theme.radius.sm, + ...theme.fn.hover({ + backgroundColor: + theme.colorScheme === 'dark' + ? theme.colors.dark[6] + : theme.colors.gray[0], + }), + + '&[data-hovered]': { + backgroundColor: + theme.colorScheme === 'dark' + ? theme.colors.dark[4] + : theme.colors.gray[1], + }, + }, +})) + +function CustomAction({ + action, + styles, + classNames, + hovered, + onTrigger, + query, + highlightQuery, + ...others +}: SpotlightActionProps) { + const { classes } = useStyles() + const queryTerms = query.split(' ') + + return ( + event.preventDefault()} + onClick={onTrigger} + {...others} + > + + {action.icon &&
{action.icon}
} +
+ + {action.title} + + {action.description && ( + + {action.description} + + )} +
+ {action.method && } +
+
+ ) +} diff --git a/generator/konfig-next-app/src/pages/[org]/[portal]/[demo].tsx b/generator/konfig-next-app/src/pages/[org]/[portal]/[demo].tsx index 49963ee71..e95557134 100644 --- a/generator/konfig-next-app/src/pages/[org]/[portal]/[demo].tsx +++ b/generator/konfig-next-app/src/pages/[org]/[portal]/[demo].tsx @@ -57,6 +57,7 @@ const DemoPage = observer( omitOwnerAndRepo, googleAnalyticsId, hasDocumentation, + allMarkdown, owner, repo, faviconLink, @@ -110,6 +111,7 @@ const DemoPage = observer( omitOwnerAndRepo={omitOwnerAndRepo} hasDocumentation={hasDocumentation} state={state} + allMarkdown={allMarkdown} owner={owner} repo={repo} logo={logo} diff --git a/generator/konfig-next-app/src/pages/[org]/[portal]/docs/[[...doc]].tsx b/generator/konfig-next-app/src/pages/[org]/[portal]/docs/[[...doc]].tsx index ced25b813..958a4fb10 100644 --- a/generator/konfig-next-app/src/pages/[org]/[portal]/docs/[[...doc]].tsx +++ b/generator/konfig-next-app/src/pages/[org]/[portal]/docs/[[...doc]].tsx @@ -4,6 +4,7 @@ import { DocEditThisPage } from '@/components/DocEditThisPage' import { DocNavLink } from '@/components/DocNavLink' import { DocumentationHeader } from '@/components/DocumentationHeader' import { NAVBAR_WIDTH } from '@/components/ReferenceNavbar' +import Fuse from 'fuse.js' import { useMantineTheme, useMantineColorScheme, @@ -92,6 +93,7 @@ const DocumentationPage = observer( owner, defaultBranch, idToLabel, + allMarkdown, docPath, breadcrumb, repo, @@ -225,6 +227,7 @@ const DocumentationPage = observer( omitOwnerAndRepo={omitOwnerAndRepo} opened={opened} setOpened={setOpened} + allMarkdown={allMarkdown} title={title} demos={demos} logo={logo} diff --git a/generator/konfig-next-app/src/pages/[org]/[portal]/reference/[tag]/[operationId].tsx b/generator/konfig-next-app/src/pages/[org]/[portal]/reference/[tag]/[operationId].tsx index a5c1b969e..c24c03338 100644 --- a/generator/konfig-next-app/src/pages/[org]/[portal]/reference/[tag]/[operationId].tsx +++ b/generator/konfig-next-app/src/pages/[org]/[portal]/reference/[tag]/[operationId].tsx @@ -87,6 +87,7 @@ const Operation = ({ hideNonSdkSnippets, googleAnalyticsId, responses, + allMarkdown, logo, }: InferGetStaticPropsType) => { const { colors } = useMantineTheme() @@ -171,6 +172,7 @@ const Operation = ({ owner={owner} repo={repo} hasDocumentation={hasDocumentation} + allMarkdown={allMarkdown} opened={opened} setOpened={setOpened} title={title} diff --git a/generator/konfig-next-app/src/pages/sandbox.tsx b/generator/konfig-next-app/src/pages/sandbox.tsx index a2405a0e2..b343bd45a 100644 --- a/generator/konfig-next-app/src/pages/sandbox.tsx +++ b/generator/konfig-next-app/src/pages/sandbox.tsx @@ -171,6 +171,7 @@ const DemoPortalWrapper = observer(() => { owner="sandbox" repo="sandbox" omitOwnerAndRepo={true} + allMarkdown={[]} hasDocumentation={false} logo={null} refreshSandbox={async () => { diff --git a/generator/konfig-next-app/src/utils/compute-document-props.ts b/generator/konfig-next-app/src/utils/compute-document-props.ts new file mode 100644 index 000000000..cf5ffa1b6 --- /dev/null +++ b/generator/konfig-next-app/src/utils/compute-document-props.ts @@ -0,0 +1,175 @@ +import { DocumentationConfig, getOperations } from 'konfig-lib' +import { collectAllDocuments } from './collect-all-documents' +import { githubGetFileContent } from './github-get-file-content' +import { Octokit } from '@octokit/rest' +import { findFirstHeadingText } from './find-first-heading-text' +import { githubGetKonfigYaml } from './github-get-konfig-yaml' +import { githubGetOpenApiSpec } from './github-get-openapi-spec' +import { isOperationHidden } from './is-operation-hidden' +import { TABS, TabType } from '@/components/HeaderButton' +import { getDemos } from './generate-demos-from-github' + +type SearchRecordBase = { + id: string + title: string + content: string +} +export type SearchRecord = ( + | { + type: TabType['documentation'] + } + | { + type: TabType['demos'] + } + | { + method: string + path: string + tag: string + type: TabType['reference'] + } +) & + SearchRecordBase + +export async function computeDocumentProps({ + documentationConfig, + owner, + repo, + octokit, +}: { + documentationConfig: DocumentationConfig + octokit: Octokit + owner: string + repo: string +}) { + // get all docs with collectAllDocumentation and generate a map of id to label from first heading text + const docs = collectAllDocuments({ docConfig: documentationConfig }) + const idToContent: Record = {} + const idToLabel: Record = {} + for (const { id, path } of docs) { + const content = await githubGetFileContent({ + octokit, + owner, + repo, + path, + }) + idToContent[id] = content + const docTitle = findFirstHeadingText({ markdown: content }) + idToLabel[id] = docTitle + } + + const allMarkdown: SearchRecord[] = Object.entries(idToContent).map( + ([id, content]) => { + if (content === undefined) + throw Error(`Couldn't find content for id: ${id}`) + const title = idToLabel[id] + if (title === undefined) throw Error("Couldn't find title for id: " + id) + + // strip all non-alphanumeric characters from content + // this is used for search + content = stripNonAlphanumericCharacters(content) + + return { + id, + title, + content, + type: TABS.documentation, + } + } + ) + + const konfigYaml = await githubGetKonfigYaml({ owner, repo, octokit }) + const spec = await githubGetOpenApiSpec({ + owner, + repo, + octokit, + konfigYaml, + }) + if (spec.specDereferenced === null) throw Error('specDereferenced is null') + const operations = getOperations({ spec: spec.specDereferenced }) + + for (const { operation, path, method } of operations) { + if (operation.operationId === undefined) + throw Error('operationId is undefined') + if (operation.tags === undefined || operation.tags.length === 0) + throw Error('tags is undefined or empty') + const hidden = isOperationHidden({ + path, + method, + konfigYaml: konfigYaml.content, + tag: operation.tags[0], + }) + if (hidden) continue + allMarkdown.push({ + id: operation.operationId, + title: operation.summary ?? `${path}`, + content: operation.description ?? '', + method, + path, + type: TABS.reference, + tag: operation.tags[0], + }) + } + + const demos = await getDemos({ + konfigYaml: konfigYaml.content, + octokit, + repo, + owner, + }) + + if (demos !== null) { + for (const demo of demos) { + allMarkdown.push({ + id: demo.id, + title: demo.name, + content: stripNonAlphanumericCharacters(demo.markdown), + type: TABS.demos, + }) + } + } + + const idToBreadcrumbs: Record = {} + for (const { id } of docs) { + // compute breadcrumb for every document in the documentation config + // the breadcrumb for a document consists of [section, group, document] + // group is optional if the document is not nested in a group + const breadcrumb: string[] = [] + for (const section of documentationConfig.sidebar.sections) { + for (const link of section.links) { + if (link.type === 'group') { + for (const innerLink of link.links) { + if (innerLink.id === id) { + breadcrumb.push(section.label) + breadcrumb.push(link.label) + const docLabel = idToLabel[id] + if (docLabel === undefined) + throw Error(`Couldn't find document label for id: ${id}`) + breadcrumb.push(docLabel) + break + } + } + } else if (link.id === id) { + breadcrumb.push(section.label) + const docLabel = idToLabel[id] + if (docLabel === undefined) + throw Error(`Couldn't find document label for id: ${id}`) + breadcrumb.push(docLabel) + break + } + } + } + idToBreadcrumbs[id] = breadcrumb + } + + return { + idToBreadcrumbs, + allMarkdown, + idToLabel, + idToContent, + docs, + } +} + +function stripNonAlphanumericCharacters(str: string) { + return str.replace(/[^a-zA-Z0-9 ]/g, ' ') +} diff --git a/generator/konfig-next-app/src/utils/compute-fuse-index.ts b/generator/konfig-next-app/src/utils/compute-fuse-index.ts new file mode 100644 index 000000000..38ab63f7b --- /dev/null +++ b/generator/konfig-next-app/src/utils/compute-fuse-index.ts @@ -0,0 +1,46 @@ +import { OpenAPIV3_XDocument } from 'konfig-lib' +import Fuse, { FuseIndex } from 'fuse.js' + +/** + * Computes a search index using flexsearch + */ +export function computeFuseIndex({ + markdown, + openapi, +}: { + markdown: { id: string; content: string }[] + openapi: OpenAPIV3_XDocument +}): FuseIndex<{ + id: string + content: string +}> { + // const fuse = new Fuse(markdown, { + // keys: ['id', 'content'], + // includeScore: true, + // includeMatches: true, + // useExtendedSearch: true, + // }) + return Fuse.createIndex(['id', 'content'], markdown) + // const results = fuse.search('\'"Welcome to the SnapTrade"') + // console.log( + // results + // .sort((a, b) => { + // if (a.score === undefined || b.score === undefined) { + // return 0 + // } + // return a.score - b.score + // }) + // .map((r) => { + // return JSON.stringify( + // { + // substrings: r.matches?.[0].indices.map((index) => { + // return r.item.content.substring(index[0], index[1] + 1) + // }), + // score: r.score, + // }, + // null, + // 2 + // ) + // }) + // ) +} diff --git a/generator/konfig-next-app/src/utils/generate-demos-from-github.ts b/generator/konfig-next-app/src/utils/generate-demos-from-github.ts index 6add67489..2c4ba5739 100644 --- a/generator/konfig-next-app/src/utils/generate-demos-from-github.ts +++ b/generator/konfig-next-app/src/utils/generate-demos-from-github.ts @@ -8,6 +8,8 @@ import type { KonfigYamlType, SocialObject } from 'konfig-lib' import { Octokit } from '@octokit/rest' import { generateFaviconLink } from './generate-favicon-link' import { generateLogoLink } from './generate-logo-link' +import { MarkdownPageProps } from './generate-props-for-markdown-page' +import { computeDocumentProps } from './compute-document-props' /** * Custom mappings to preserve existing links for SnapTrade @@ -32,6 +34,7 @@ export type FetchResult = { portal: Portal demos: Demo[] socials?: SocialObject + allMarkdown: MarkdownPageProps['allMarkdown'] mainBranch: string hasDocumentation: boolean faviconLink: string | null @@ -86,6 +89,7 @@ export async function generateDemosDataFromGithub({ portalTitle: string | null primaryColor: string | null hasDocumentation: boolean + allMarkdown: MarkdownPageProps['allMarkdown'] faviconLink: string | null logo: ReturnType } @@ -114,6 +118,7 @@ export async function generateDemosDataFromGithub({ portal, googleAnalyticsId: fetchResult.googleAnalyticsId, demo, + allMarkdown: fetchResult.allMarkdown, portalTitle: fetchResult.portalTitle ?? null, faviconLink: fetchResult.faviconLink, primaryColor: fetchResult.primaryColor ?? null, @@ -184,10 +189,22 @@ async function _fetch({ portals: [portal], } + const allMarkdown = konfigYaml.content.portal?.documentation + ? ( + await computeDocumentProps({ + documentationConfig: konfigYaml.content.portal?.documentation, + owner, + repo, + octokit, + }) + ).allMarkdown + : [] + return { organization, portal, demos, + allMarkdown, hasDocumentation: konfigYaml.content.portal.documentation !== undefined, googleAnalyticsId: konfigYaml.content.portal?.googleAnalyticsId ?? null, portalTitle: konfigYaml.content.portal.title, @@ -201,7 +218,7 @@ async function _fetch({ } } -async function getDemos({ +export async function getDemos({ konfigYaml, repo, owner, diff --git a/generator/konfig-next-app/src/utils/generate-navbar-links.ts b/generator/konfig-next-app/src/utils/generate-navbar-links.ts index 66ea7a729..0d22dd3f2 100644 --- a/generator/konfig-next-app/src/utils/generate-navbar-links.ts +++ b/generator/konfig-next-app/src/utils/generate-navbar-links.ts @@ -1,5 +1,6 @@ import { NavbarDataItem } from '@/components/LinksGroup' import type { Spec, HttpMethods, KonfigYamlType } from 'konfig-lib' +import { isOperationHidden } from './is-operation-hidden' /** * Generates the navbar links as NavbarDataItem[]. Each group is determined by the tag of an operation. @@ -63,14 +64,15 @@ export function generateNavbarLinks({ : `/${owner}/${repo}/${suffix}` // if path and method match up with operation in hideOperations then continue - const hideOperations = konfigYaml.portal?.hideOperations - if (hideOperations !== undefined) { - if (path in hideOperations) { - const methods = hideOperations[path] - if (methods === undefined) return - if (method in methods) return - } - } + if ( + isOperationHidden({ + path, + method, + konfigYaml, + tag: operation.tags[0], + }) + ) + return navbarLink.links.push({ label: operation.summary ?? path, diff --git a/generator/konfig-next-app/src/utils/generate-props-for-demo-page.ts b/generator/konfig-next-app/src/utils/generate-props-for-demo-page.ts index 87dfd1f78..52bcf977b 100644 --- a/generator/konfig-next-app/src/utils/generate-props-for-demo-page.ts +++ b/generator/konfig-next-app/src/utils/generate-props-for-demo-page.ts @@ -1,4 +1,4 @@ -import { GetStaticProps, GetStaticPropsResult } from 'next' +import { GetStaticPropsResult } from 'next' import { GenerationSuccess, generateDemosDataFromGithub, diff --git a/generator/konfig-next-app/src/utils/generate-props-for-markdown-page.ts b/generator/konfig-next-app/src/utils/generate-props-for-markdown-page.ts index e36ccada5..0ecf65ed9 100644 --- a/generator/konfig-next-app/src/utils/generate-props-for-markdown-page.ts +++ b/generator/konfig-next-app/src/utils/generate-props-for-markdown-page.ts @@ -20,6 +20,7 @@ import { transformImageLinks } from './transform-image-links' import { transformInternalLinks } from './transform-internal-links' import { generateFaviconLink } from './generate-favicon-link' import { generateLogoLink } from './generate-logo-link' +import { computeDocumentProps } from './compute-document-props' export type MarkdownPageProps = { konfigYaml: KonfigYamlType @@ -35,6 +36,7 @@ export type MarkdownPageProps = { googleAnalyticsId: string | null markdown: string defaultBranch: string + allMarkdown: Awaited>['allMarkdown'] /** * Mapping of document id from konfig.yaml and the first heading text of the document. @@ -80,7 +82,6 @@ export async function generatePropsForMarkdownPage({ // TODO: handle multiple konfig.yaml const konfigYaml = konfigYamls?.[0] - if (konfigYaml === undefined) throw Error("Couldn't find konfig.yaml") const faviconLink = generateFaviconLink({ @@ -177,52 +178,13 @@ export async function generatePropsForMarkdownPage({ const docTitle = findFirstHeadingText({ markdown }) - // get all docs with collectAllDocumentation and generate a map of id to label from first heading text - const docs = collectAllDocuments({ docConfig: documentationConfig }) - const idToLabel: Record = {} - for (const { id, path } of docs) { - const content = await githubGetFileContent({ - octokit, + const { allMarkdown, idToLabel, idToBreadcrumbs } = + await computeDocumentProps({ + documentationConfig, owner, repo, - path, + octokit, }) - const docTitle = findFirstHeadingText({ markdown: content }) - idToLabel[id] = docTitle - } - - const idToBreadcrumbs: Record = {} - for (const { id } of docs) { - // compute breadcrumb for every document in the documentation config - // the breadcrumb for a document consists of [section, group, document] - // group is optional if the document is not nested in a group - const breadcrumb: string[] = [] - for (const section of documentationConfig.sidebar.sections) { - for (const link of section.links) { - if (link.type === 'group') { - for (const innerLink of link.links) { - if (innerLink.id === id) { - breadcrumb.push(section.label) - breadcrumb.push(link.label) - const docLabel = idToLabel[id] - if (docLabel === undefined) - throw Error(`Couldn't find document label for id: ${id}`) - breadcrumb.push(docLabel) - break - } - } - } else if (link.id === id) { - breadcrumb.push(section.label) - const docLabel = idToLabel[id] - if (docLabel === undefined) - throw Error(`Couldn't find document label for id: ${id}`) - breadcrumb.push(docLabel) - break - } - } - } - idToBreadcrumbs[id] = breadcrumb - } const breadcrumb = idToBreadcrumbs[documentId] if (breadcrumb === undefined) @@ -244,6 +206,7 @@ export async function generatePropsForMarkdownPage({ owner, repo, breadcrumb, + allMarkdown, operations, defaultBranch, idToLabel, diff --git a/generator/konfig-next-app/src/utils/generate-props-for-reference-page.ts b/generator/konfig-next-app/src/utils/generate-props-for-reference-page.ts index 5b6e17d76..573088166 100644 --- a/generator/konfig-next-app/src/utils/generate-props-for-reference-page.ts +++ b/generator/konfig-next-app/src/utils/generate-props-for-reference-page.ts @@ -18,6 +18,9 @@ import { NavbarDataItem } from '@/components/LinksGroup' import { generateDemosDataFromGithub } from './generate-demos-from-github' import { sortParametersByRequired } from './sort-parameters-by-required' import { generateLogoLink } from './generate-logo-link' +import { MarkdownPageProps } from './generate-props-for-markdown-page' +import { computeDocumentProps } from './compute-document-props' +import { createOctokitInstance } from './octokit' export type ReferencePageProps = Omit & { spec: Spec['spec'] @@ -42,6 +45,7 @@ export type ReferencePageProps = Omit & { requestBodyProperties: Record | null requestBodyRequired: string[] | null googleAnalyticsId: string | null + allMarkdown: MarkdownPageProps['allMarkdown'] responses: Record securityRequirements: Record | null securitySchemes: Record | null @@ -62,6 +66,7 @@ export async function generatePropsForReferencePage({ operationId: string omitOwnerAndRepo?: boolean }): Promise> { + const octokit = await createOctokitInstance({ owner, repo }) const { spec, ...props } = await githubGetReferenceResources({ owner, repo, @@ -259,6 +264,17 @@ export async function generatePropsForReferencePage({ notFound: true, } + const allMarkdown = props.konfigYaml.portal?.documentation + ? ( + await computeDocumentProps({ + documentationConfig: props.konfigYaml.portal.documentation, + owner, + repo, + octokit, + }) + ).allMarkdown + : [] + return { props: { ...props, @@ -267,6 +283,7 @@ export async function generatePropsForReferencePage({ hideNonSdkSnippets: props.konfigYaml.portal.hideNonSdkSnippets ?? false, httpMethod: operation.method, path: operation.path, + allMarkdown, operationId, operation, spec: spec.spec, diff --git a/generator/konfig-next-app/src/utils/github-get-konfig-yaml.ts b/generator/konfig-next-app/src/utils/github-get-konfig-yaml.ts new file mode 100644 index 000000000..488e4b977 --- /dev/null +++ b/generator/konfig-next-app/src/utils/github-get-konfig-yaml.ts @@ -0,0 +1,20 @@ +import { Octokit } from '@octokit/rest' +import { githubGetKonfigYamls } from './github-get-konfig-yamls' + +export async function githubGetKonfigYaml({ + owner, + repo, + octokit, +}: { + owner: string + repo: string + octokit: Octokit +}) { + const konfigYamls = await githubGetKonfigYamls({ owner, repo, octokit }) + + // TODO: handle multiple konfig.yaml + const konfigYaml = konfigYamls?.[0] + + if (konfigYaml === undefined) throw Error("Couldn't find konfig.yaml") + return konfigYaml +} diff --git a/generator/konfig-next-app/src/utils/github-get-openapi-spec.ts b/generator/konfig-next-app/src/utils/github-get-openapi-spec.ts new file mode 100644 index 000000000..0c63d5801 --- /dev/null +++ b/generator/konfig-next-app/src/utils/github-get-openapi-spec.ts @@ -0,0 +1,36 @@ +import { Octokit } from '@octokit/rest' +import { githubGetFileContent } from './github-get-file-content' +import * as path from 'path' +import { orderOpenApiSpecification, parseSpec } from 'konfig-lib' +import { githubGetKonfigYaml } from './github-get-konfig-yaml' + +export async function githubGetOpenApiSpec({ + owner, + repo, + octokit, + konfigYaml, +}: { + owner: string + repo: string + octokit: Octokit + konfigYaml: Awaited> +}) { + const specPath = konfigYaml.content.specPath + const openapi = await githubGetFileContent({ + owner, + repo, + octokit, + path: path.join(path.dirname(konfigYaml.info.path), specPath), + }) + + const spec = await parseSpec(openapi) + + if (konfigYaml.content.order !== undefined) { + orderOpenApiSpecification({ + spec: spec.spec, + order: konfigYaml.content.order, + }) + } + + return spec +} diff --git a/generator/konfig-next-app/src/utils/github-get-reference-resources.ts b/generator/konfig-next-app/src/utils/github-get-reference-resources.ts index 2fc0c2957..57e4d3612 100644 --- a/generator/konfig-next-app/src/utils/github-get-reference-resources.ts +++ b/generator/konfig-next-app/src/utils/github-get-reference-resources.ts @@ -1,14 +1,11 @@ -import path from 'path' import { generateNavbarLinks } from './generate-navbar-links' -import { githubGetFileContent } from './github-get-file-content' -import { githubGetKonfigYamls } from './github-get-konfig-yamls' import { createOctokitInstance } from './octokit' -import { parseSpec } from 'konfig-lib/dist/parseSpec' -import { orderOpenApiSpecification } from 'konfig-lib/dist/util/order-openapi-specification' import { UnwrapPromise } from 'next/dist/lib/coalesced-function' import { githubGetRepository } from './github-get-repository' import { generateFaviconLink } from './generate-favicon-link' import { generateLogoLink } from './generate-logo-link' +import { githubGetOpenApiSpec } from './github-get-openapi-spec' +import { githubGetKonfigYaml } from './github-get-konfig-yaml' export type GithubResources = UnwrapPromise< ReturnType @@ -27,14 +24,9 @@ export async function githubGetReferenceResources({ // time the next two lines const start = Date.now() - const konfigYamls = await githubGetKonfigYamls({ owner, repo, octokit }) + const konfigYaml = await githubGetKonfigYaml({ owner, repo, octokit }) console.log(`githubGetKonfigYamls took ${Date.now() - start}ms`) - // TODO: handle multiple konfig.yaml - const konfigYaml = konfigYamls?.[0] - - if (konfigYaml === undefined) throw Error("Couldn't find konfig.yaml") - // get default branch of repo const { data: repoData } = await githubGetRepository({ owner, @@ -57,26 +49,15 @@ export async function githubGetReferenceResources({ repo, }) - const specPath = konfigYaml.content.specPath - // time the next three lines const start2 = Date.now() - const openapi = await githubGetFileContent({ + const spec = await githubGetOpenApiSpec({ owner, repo, octokit, - path: path.join(path.dirname(konfigYaml.info.path), specPath), + konfigYaml, }) - const spec = await parseSpec(openapi) - - if (konfigYaml.content.order !== undefined) { - orderOpenApiSpecification({ - spec: spec.spec, - order: konfigYaml.content.order, - }) - } - const navbarData = generateNavbarLinks({ spec: spec.spec, owner, diff --git a/generator/konfig-next-app/src/utils/is-operation-hidden.ts b/generator/konfig-next-app/src/utils/is-operation-hidden.ts new file mode 100644 index 000000000..8b109a2c9 --- /dev/null +++ b/generator/konfig-next-app/src/utils/is-operation-hidden.ts @@ -0,0 +1,26 @@ +import { KonfigYamlType } from 'konfig-lib' + +export function isOperationHidden({ + path, + method, + tag, + konfigYaml, +}: { + path: string + method: string + tag: string + konfigYaml: KonfigYamlType +}) { + const hideOperations = konfigYaml.portal?.hideOperations + if (hideOperations !== undefined) { + if (path in hideOperations) { + const methods = hideOperations[path] + if (methods === undefined) return false + if (method in methods) return true + } + } + if (konfigYaml.filterTags !== undefined) { + if (konfigYaml.filterTags.includes(tag)) return true + } + return false +} diff --git a/generator/konfig-next-app/yarn.lock b/generator/konfig-next-app/yarn.lock index 057edf2f8..90f97b8f0 100644 --- a/generator/konfig-next-app/yarn.lock +++ b/generator/konfig-next-app/yarn.lock @@ -499,6 +499,13 @@ "@mantine/utils" "6.0.13" prism-react-renderer "^1.2.1" +"@mantine/spotlight@^6.0.13": + version "6.0.21" + resolved "https://registry.yarnpkg.com/@mantine/spotlight/-/spotlight-6.0.21.tgz#98f507bd3429fee1f2b57ad5ef9f88d1d8d8ff32" + integrity sha512-xJqF2Vpn8s6I4mSF+iCi7IzqL8iaqbvq0RcYlF1usLZYW2HrArX31s1r11DmzqM1PIuBQUhquW8jUXx/MZy3oA== + dependencies: + "@mantine/utils" "6.0.21" + "@mantine/ssr@6.0.13": version "6.0.13" resolved "https://registry.yarnpkg.com/@mantine/ssr/-/ssr-6.0.13.tgz#5ccfdc8b7c26e16c326b7f91200f6a38bb58965e" @@ -520,6 +527,11 @@ resolved "https://registry.yarnpkg.com/@mantine/utils/-/utils-6.0.13.tgz#a7adc128a2e7c07031c7221c1533800d0c80279a" integrity sha512-iqIU9wurqAeccVbWjM0yr1JGne5VP+ob55M03QAXOEN4+ck93VDTjCkZJR2RFhDcs5q0twQFoOmU/gULR8aKIA== +"@mantine/utils@6.0.21": + version "6.0.21" + resolved "https://registry.yarnpkg.com/@mantine/utils/-/utils-6.0.21.tgz#6185506e91cba3e308aaa8ea9ababc8e767995d6" + integrity sha512-33RVDRop5jiWFao3HKd3Yp7A9mEq4HAJxJPTuYm1NkdqX6aTKOQK7wT8v8itVodBp+sb4cJK6ZVdD1UurK/txQ== + "@next/env@13.4.6": version "13.4.6" resolved "https://registry.yarnpkg.com/@next/env/-/env-13.4.6.tgz#3f2041c7758660d7255707ae4cb9166519113dea" @@ -2320,6 +2332,11 @@ deprecation@^2.0.0, deprecation@^2.3.1: resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919" integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ== +dequal@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/dequal/-/dequal-1.0.0.tgz#41c6065e70de738541c82cdbedea5292277a017e" + integrity sha512-/Nd1EQbQbI9UbSHrMiKZjFLrXSnU328iQdZKPQf78XQI6C+gutkFUeoHpG5J08Ioa6HeRbRNFpSIclh1xyG0mw== + dequal@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" @@ -3088,6 +3105,11 @@ functions-have-names@^1.2.2, functions-have-names@^1.2.3: resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== +fuse.js@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-7.0.0.tgz#6573c9fcd4c8268e403b4fc7d7131ffcf99a9eb2" + integrity sha512-14F4hBIxqKvD4Zz/XjDc3y94mNZN6pRv3U13Udo0lNLCWRBUsrMv2xwcF/y/Z5sV6+FQW+/ow68cHpm4sunt8Q== + generic-pool@3.9.0: version "3.9.0" resolved "https://registry.yarnpkg.com/generic-pool/-/generic-pool-3.9.0.tgz#36f4a678e963f4fdb8707eab050823abc4e8f5e4" @@ -6517,6 +6539,13 @@ use-composed-ref@^1.3.0: resolved "https://registry.yarnpkg.com/use-composed-ref/-/use-composed-ref-1.3.0.tgz#3d8104db34b7b264030a9d916c5e94fbe280dbda" integrity sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ== +use-deep-compare@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/use-deep-compare/-/use-deep-compare-1.1.0.tgz#85580dde751f68400bf6ef7e043c7f986595cef8" + integrity sha512-6yY3zmKNCJ1jjIivfZMZMReZjr8e6iC6Uqtp701jvWJ6ejC/usXD+JjmslZDPJQgX8P4B1Oi5XSLHkOLeYSJsA== + dependencies: + dequal "1.0.0" + use-isomorphic-layout-effect@^1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz#497cefb13d863d687b08477d9e5a164ad8c1a6fb"