From 1d52dfacded4011fc5a6776b526bdc7528f7514a Mon Sep 17 00:00:00 2001 From: Charis <26616127+charislam@users.noreply.github.com> Date: Thu, 4 Apr 2024 13:35:31 -0400 Subject: [PATCH] feat: federated docs for linter (#22472) * feat: federated docs for linter * style: clean up * fix: toc and link rewrite * fix: remove console logs --- .../docs/components/GuidesTableOfContents.tsx | 101 ++++++++-- .../NavigationMenu.constants.ts | 9 +- apps/docs/components/TableOfContents.tsx | 83 -------- apps/docs/hooks/useHash.ts | 4 +- apps/docs/hooks/useManualRerender.ts | 16 ++ apps/docs/layouts/DefaultLayout.tsx | 87 +------- apps/docs/layouts/HomeLayout.tsx | 22 +- apps/docs/layouts/guides/index.tsx | 78 ++----- .../docs/layouts/tutorials/TutorialLayout.tsx | 87 -------- apps/docs/package.json | 2 +- apps/docs/pages/guides/cli/config.tsx | 33 +-- .../pages/guides/database/database-linter.tsx | 190 ++++++++++++++++++ package-lock.json | 8 +- packages/ui/index.tsx | 3 + packages/ui/src/components/Tabs/Tabs.tsx | 13 +- 15 files changed, 348 insertions(+), 388 deletions(-) delete mode 100644 apps/docs/components/TableOfContents.tsx create mode 100644 apps/docs/hooks/useManualRerender.ts delete mode 100644 apps/docs/layouts/tutorials/TutorialLayout.tsx create mode 100644 apps/docs/pages/guides/database/database-linter.tsx diff --git a/apps/docs/components/GuidesTableOfContents.tsx b/apps/docs/components/GuidesTableOfContents.tsx index eaeb45dcfc5..b33c3eb3a0f 100644 --- a/apps/docs/components/GuidesTableOfContents.tsx +++ b/apps/docs/components/GuidesTableOfContents.tsx @@ -1,5 +1,14 @@ -import { FC } from 'react' +import { usePathname } from 'next/navigation' +import { useEffect, useState } from 'react' + +import { cn, TAB_CHANGE_EVENT_NAME } from 'ui' +import { ExpandableVideo } from 'ui-patterns' + +import { highlightSelectedTocItem } from '~/components/CustomHTMLElements/CustomHTMLElements.utils' +import useHash from '~/hooks/useHash' +import { useRerenderOnEvt } from '~/hooks/useManualRerender' import { removeAnchor } from './CustomHTMLElements/CustomHTMLElements.utils' +import { Feedback } from './Feedback' const formatSlug = (slug: string) => { // [Joshen] We will still provide support for headers declared like this: @@ -28,30 +37,78 @@ const formatTOCHeader = (content: string) => { return res.join('') } -interface TOCHeader { - id: number - level: number - text: string - link: string -} +const GuidesTableOfContents = ({ + className, + overrideToc, + video, +}: { + className?: string + overrideToc?: Array<{ text: string; link: string; level: number }> + video?: string +}) => { + const [tocList, setTocList] = useState([]) + const pathname = usePathname() + const [hash] = useHash() -interface Props { - list: TOCHeader[] -} + const displayedList = overrideToc ?? tocList + + useEffect(() => { + if (overrideToc) return + + const headings = Array.from( + document.querySelector('#sb-docs-guide-main-article')?.querySelectorAll('h2, h3') ?? [] + ) + const newHeadings = headings + .filter((heading) => heading.id) + .map((heading) => { + const text = heading.textContent.replace('#', '') + const link = heading.querySelector('a').getAttribute('href') + const level = heading.tagName === 'H2' ? 2 : 3 + return { text, link, level } + }) + setTocList(newHeadings) + }, [pathname]) // needed to recalculate the toc when path changes + + useEffect(() => { + if (hash && displayedList.length > 0) { + highlightSelectedTocItem(hash) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [hash, JSON.stringify(displayedList)]) + + /** + * Displayed headings may change if the tab changes, so the table of contents + * needs to rerender. + */ + useRerenderOnEvt(TAB_CHANGE_EVENT_NAME) + + if (!displayedList.length) return + + const tocVideoPreview = `http://img.youtube.com/vi/${video}/0.jpg` -const GuidesTableOfContents: FC = ({ list }) => { return ( - +
+ {video && ( +
+ +
+ )} + + + On this page + +
+
) } diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index de4d8290537..305c6d5a092 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts @@ -794,7 +794,14 @@ export const database: NavMenuConstant = { name: 'Query optimization', url: '/guides/database/query-optimization', }, - { name: 'Debugging and monitoring', url: '/guides/database/inspect' }, + { + name: 'Database linter', + url: '/guides/database/database-linter', + }, + { + name: 'Debugging and monitoring', + url: '/guides/database/inspect', + }, { name: 'Debugging performance issues', url: '/guides/database/debugging-performance', diff --git a/apps/docs/components/TableOfContents.tsx b/apps/docs/components/TableOfContents.tsx deleted file mode 100644 index a9af7b2157d..00000000000 --- a/apps/docs/components/TableOfContents.tsx +++ /dev/null @@ -1,83 +0,0 @@ -// [Terry] -// Delete this after we've implemented GuidesTableofContents and moved all guides -// and rename GuidesTableofContents to TableOfContents - -import { FC } from 'react' -import { getAnchor, removeAnchor } from './CustomHTMLElements/CustomHTMLElements.utils' - -interface TOCHeader { - id: number - lvl: number - seen: number - content: string - slug: string -} - -interface Props { - toc: any - video?: string -} - -const formatSlug = (slug: string) => { - // [Joshen] We will still provide support for headers declared like this: - // ## REST API {#rest-api-overview} - // At least for now, this was a docusaurus thing. - if (slug.includes('#')) return slug.split('-#')[1] - return slug -} - -const formatTOCHeader = (content: string) => { - let begin = false - const res = [] - for (const x of content) { - if (x === '`') { - if (!begin) { - begin = true - res.push(``) - } else { - begin = false - res.push(``) - } - } else { - res.push(x) - } - } - return res.join('') -} - -const TableOfContents: FC = ({ toc, video }) => { - // [Joshen] markdown-toc doesn't seem to read maxdepth from the options passed in - // Our first level headers will be H2s (H1 is ignored), and we only show up to H3 - - return ( - <> - {video && ( -
- -
- )} -
- - ) -} - -export default TableOfContents diff --git a/apps/docs/hooks/useHash.ts b/apps/docs/hooks/useHash.ts index 92c0cec4fb8..3c6f9567dab 100644 --- a/apps/docs/hooks/useHash.ts +++ b/apps/docs/hooks/useHash.ts @@ -14,7 +14,7 @@ const useHash = () => { return () => { window.removeEventListener('hashchange', hashChangeHandler) } - }, []) + }, [hashChangeHandler]) const updateHash = useCallback( (newHash) => { @@ -23,7 +23,7 @@ const useHash = () => { [hash] ) - return [hash, updateHash] + return [hash, updateHash] as const } export default useHash diff --git a/apps/docs/hooks/useManualRerender.ts b/apps/docs/hooks/useManualRerender.ts new file mode 100644 index 00000000000..cc1a6266ded --- /dev/null +++ b/apps/docs/hooks/useManualRerender.ts @@ -0,0 +1,16 @@ +import { useEffect, useReducer } from 'react' + +/** + * An escape hatch to manually trigger component rerenders in response to + * external events, such as fired custom events. + */ +const useRerenderOnEvt = (event: string, listeningElem?: Document | Window | HTMLElement) => { + const [, rerender] = useReducer((state) => !state, true) + + useEffect(() => { + ;(listeningElem ?? window).addEventListener(event, rerender) + return () => (listeningElem ?? window).removeEventListener(event, rerender) + }, [event, listeningElem, rerender]) +} + +export { useRerenderOnEvt } diff --git a/apps/docs/layouts/DefaultLayout.tsx b/apps/docs/layouts/DefaultLayout.tsx index 6bfc53a2564..f6b3f8bf230 100644 --- a/apps/docs/layouts/DefaultLayout.tsx +++ b/apps/docs/layouts/DefaultLayout.tsx @@ -1,91 +1,6 @@ -import { MDXProvider } from '@mdx-js/react' -import { NextSeo } from 'next-seo' -import Head from 'next/head' -import { useRouter } from 'next/router' -import { FC, PropsWithChildren } from 'react' -import components from '~/components' -import TableOfContents from '~/components/TableOfContents' -import { MainSkeleton } from './MainSkeleton' - -interface Props { - meta: { - title: string - description?: string - hide_table_of_contents?: boolean - video?: string - tocVideo?: string - canonical?: string - } - children: any - toc?: any - menuItems: any -} - -const Layout: FC = (props: Props) => { - const { asPath } = useRouter() - - const hasTableOfContents = - props.toc !== undefined && - props.toc.json.filter((item) => item.lvl !== 1 && item.lvl <= 3).length > 0 - - return ( - <> - - {asPath === '/' ? 'Supabase Docs' : `${props.meta?.title} | Supabase Docs`} - - - - - - - -
-
-

{props.meta.title}

- {props.children} -
-
- {hasTableOfContents && !props.meta?.hide_table_of_contents && ( - - )} -
- - ) -} +import { type FC, type PropsWithChildren } from 'react' export const LayoutMainContent: FC> = ({ className, children, }) =>
{children}
- -export default Layout diff --git a/apps/docs/layouts/HomeLayout.tsx b/apps/docs/layouts/HomeLayout.tsx index 9f5e7d46320..7f2ff3de39a 100644 --- a/apps/docs/layouts/HomeLayout.tsx +++ b/apps/docs/layouts/HomeLayout.tsx @@ -6,30 +6,22 @@ import { FC } from 'react' import components from '~/components' import HomePageCover from '~/components/HomePageCover' import { MenuId } from '~/components/Navigation/NavigationMenu/NavigationMenu' -import TableOfContents from '~/components/TableOfContents' import { LayoutMainContent } from './DefaultLayout' import { MainSkeleton } from './MainSkeleton' interface Props { meta: { title: string - description?: string - hide_table_of_contents?: boolean - video?: string canonical?: string + description?: string + video?: string } children: any - toc?: any - menuItems: any } const HomeLayout: FC = (props: Props) => { const { asPath } = useRouter() - const hasTableOfContents = - props.toc !== undefined && - props.toc.json.filter((item) => item.lvl !== 1 && item.lvl <= 3).length > 0 - return ( <> @@ -72,16 +64,6 @@ const HomeLayout: FC = (props: Props) => { {props.children} - {hasTableOfContents && !props.meta?.hide_table_of_contents && ( - - )} diff --git a/apps/docs/layouts/guides/index.tsx b/apps/docs/layouts/guides/index.tsx index 3200de40047..a137a67c90a 100644 --- a/apps/docs/layouts/guides/index.tsx +++ b/apps/docs/layouts/guides/index.tsx @@ -1,22 +1,18 @@ import { MDXProvider } from '@mdx-js/react' +import 'katex/dist/katex.min.css' +import { ExternalLink } from 'lucide-react' import { NextSeo } from 'next-seo' -import { usePathname } from 'next/navigation' import { useRouter } from 'next/router' -import { FC, useEffect, useRef, useState } from 'react' +import { type FC } from 'react' -import { IconExternalLink, cn } from 'ui' -import { ExpandableVideo } from 'ui-patterns/ExpandableVideo' +import { cn } from 'ui' import components from '~/components' -import { highlightSelectedTocItem } from '~/components/CustomHTMLElements/CustomHTMLElements.utils' import { FooterHelpCalloutType } from '~/components/FooterHelpCallout' import GuidesTableOfContents from '~/components/GuidesTableOfContents' import { type MenuId } from '~/components/Navigation/NavigationMenu/NavigationMenu' -import useHash from '~/hooks/useHash' -import { Feedback } from '~/components/Feedback' import { LayoutMainContent } from '~/layouts/DefaultLayout' import { MainSkeleton } from '~/layouts/MainSkeleton' -import 'katex/dist/katex.min.css' interface Props { meta: { @@ -39,12 +35,6 @@ interface Props { } const Layout: FC = (props) => { - const pathname = usePathname() - const [hash] = useHash() - - const articleRef = useRef() - const [tocList, setTocList] = useState([]) - const { asPath } = useRouter() const router = useRouter() @@ -52,31 +42,6 @@ const Layout: FC = (props) => { const EDIT_BUTTON_EXCLUDE_LIST = ['/404'] - useEffect(() => { - if (hash && tocList.length > 0) { - highlightSelectedTocItem(hash as string) - } - }, [hash, JSON.stringify(tocList)]) - - useEffect(() => { - const articleEl = articleRef.current as HTMLElement - - if (!articleRef.current) return - const headings = Array.from(articleEl.querySelectorAll('h2, h3')) - const newHeadings = headings - .filter((heading) => heading.id) - .map((heading) => { - const text = heading.textContent.replace('#', '') - const link = heading.querySelector('a').getAttribute('href') - const level = heading.tagName === 'H2' ? 2 : 3 - return { text, link, level } - }) - setTocList(newHeadings) - }, [pathname]) // needed to recalculate the toc when path changes - - const hasTableOfContents = tocList.length > 0 - const tocVideoPreview = `http://img.youtube.com/vi/${props.meta?.tocVideo}/0.jpg` - // page type, ie, Auth, Database, Storage etc const ogPageType = asPath.split('/')[2] // open graph image url constructor @@ -141,10 +106,9 @@ const Layout: FC = (props) => {

{props.meta.breadcrumb}

)}

{props.meta.title}

{props.meta?.subtitle && ( @@ -166,35 +130,23 @@ const Layout: FC = (props) => { `} className="text-sm transition flex items-center gap-1 text-scale-1000 hover:text-scale-1200 w-fit" > - Edit this page on GitHub + Edit this page on GitHub
)}
- {!props.hideToc && hasTableOfContents && !props.meta?.hide_table_of_contents && ( - + /> )} diff --git a/apps/docs/layouts/tutorials/TutorialLayout.tsx b/apps/docs/layouts/tutorials/TutorialLayout.tsx deleted file mode 100644 index 4c369aeba16..00000000000 --- a/apps/docs/layouts/tutorials/TutorialLayout.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { MDXProvider } from '@mdx-js/react' -import Head from 'next/head' -import { FC, useEffect, useState } from 'react' -import components from '~/components' -import SideBar from '~/components/Navigation/SideBar' -import TableOfContents from '~/components/TableOfContents' - -interface Props { - meta: { - title: string - description?: string - hide_table_of_contents?: boolean - video?: string - tocVideo?: string - } - children: any - toc?: any - menuItems: any - currentPage: string -} - -const Layout: FC = (props: Props) => { - const [active, setActive] = useState(false) - - useEffect(() => { - setTimeout(function () { - setActive(true) - }, 150) - }, []) - - // const contentString = renderToString(props.children) - - // const content = serialize(contentString || '') - - // console.log('contentString', contentString) - - // const _toc = toc('#hello world', { maxdepth: 1, firsth1: false }) - - const hasTableOfContents = - props.toc !== undefined && - props.toc.json.filter((item) => item.lvl !== 1 && item.lvl <= 3).length > 0 - - return ( - <> - - {props.meta?.title} | Supabase - - - - - - - - - -
-
-

Tutorials

-
-

{props.meta.title}

-
- - -
-
- {hasTableOfContents && !props.meta?.hide_table_of_contents && ( - - )} -
- - ) -} - -export default Layout diff --git a/apps/docs/package.json b/apps/docs/package.json index 8151f248049..428e3dc80ba 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -25,7 +25,7 @@ "@next/bundle-analyzer": "^14.1.4", "@next/mdx": "^14.1.4", "@octokit/auth-app": "^6.0.3", - "@octokit/core": "^5.0.2", + "@octokit/core": "^5.1.0", "@octokit/graphql": "^7.0.2", "@octokit/plugin-paginate-graphql": "^4.0.0", "@radix-ui/react-accordion": "^1.1.2", diff --git a/apps/docs/pages/guides/cli/config.tsx b/apps/docs/pages/guides/cli/config.tsx index 193f25fca66..da10ec303ad 100644 --- a/apps/docs/pages/guides/cli/config.tsx +++ b/apps/docs/pages/guides/cli/config.tsx @@ -1,12 +1,14 @@ -import specFile from '~/spec/cli_v1_config.yaml' assert { type: 'yml' } -import { Parameter } from '~/lib/refGenerator/refTypes' -import ReactMarkdown from 'react-markdown' -import GuidesTableOfContents from '~/components/GuidesTableOfContents' -import { Heading } from '~/components/CustomHTMLElements' import Head from 'next/head' -import { CodeBlock } from 'ui' -import { MainSkeleton } from '~/layouts/MainSkeleton' +import ReactMarkdown from 'react-markdown' + +import { CodeBlock, cn } from 'ui' + +import { Heading } from '~/components/CustomHTMLElements' +import GuidesTableOfContents from '~/components/GuidesTableOfContents' import { MenuId } from '~/components/Navigation/NavigationMenu/NavigationMenu' +import { MainSkeleton } from '~/layouts/MainSkeleton' +import { Parameter } from '~/lib/refGenerator/refTypes' +import specFile from '~/spec/cli_v1_config.yaml' assert { type: 'yml' } // Parameters are grouped on the page by tag const tocList = [] @@ -41,14 +43,15 @@ export default function Config() {
{content}
-
-
- - On this page - - -
-
+ diff --git a/apps/docs/pages/guides/database/database-linter.tsx b/apps/docs/pages/guides/database/database-linter.tsx new file mode 100644 index 00000000000..9f488dc3686 --- /dev/null +++ b/apps/docs/pages/guides/database/database-linter.tsx @@ -0,0 +1,190 @@ +import { CodeHikeConfig, remarkCodeHike } from '@code-hike/mdx' +import { Octokit } from '@octokit/core' +import { capitalize } from 'lodash' +import { GetStaticProps, InferGetStaticPropsType } from 'next' +import { MDXRemote } from 'next-mdx-remote' +import { serialize } from 'next-mdx-remote/serialize' +import remarkGfm from 'remark-gfm' +import rehypeSlug from 'rehype-slug' + +import codeHikeTheme from 'config/code-hike.theme.json' assert { type: 'json' } +import { Tabs } from 'ui' + +import components from '~/components' +import { Heading } from '~/components/CustomHTMLElements' +import { MenuId } from '~/components/Navigation/NavigationMenu/NavigationMenu' +import Layout from '~/layouts/DefaultGuideLayout' +import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform' +import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition' +import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle' +import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs' + +// We fetch these docs at build time from an external repo +export const org = 'supabase' +export const repo = 'splinter' +export const branch = 'main' +export const docsDir = 'docs' + +const meta = { + title: 'Database Linter', + subtitle: 'Check your database for performance and security issues', +} + +const editLink = 'https://github.com/supabase/splinter/tree/main/docs' + +const markdownIntro = ` +You can use the Project Linter to check your database for issues such as missing indexes and improperly set-up RLS policies. + +## Using the linter + +In the dashboard, navigate to [Database Linter](/dashboard/project/_/database/linter) under Database. The linter runs automatically. You can also manually rerun it after you're resolved issues, or ignore individual lints. +`.trim() + +const getBasename = (path: string) => path.split('/').at(-1).replace(/\.md$/, '') + +export default function ProjectLinterDocs({ + intro, + lints, +}: InferGetStaticPropsType) { + return ( + + + Available lints + + {lints.map((lint) => ( + +
+ +
+
+ ))} +
+
+ ) +} + +/** + * The GitHub repo uses relative links, which don't lead to the right locations + * in docs. + * + * @param url The original link, as written in the Markdown file + * @returns The rewritten link + */ +const urlTransform: (lints: Array<{ path: string }>) => UrlTransformFunction = (lints) => (url) => { + try { + const placeholderHostname = 'placeholder' + const { hostname, pathname, hash } = new URL(url, `http://${placeholderHostname}`) + + // Don't modify a url with a FQDN or a url that's only a hash + if (hostname !== placeholderHostname || pathname === '/') { + return url + } + + const relativePath = getBasename(pathname) + const section = lints.find(({ path }) => path === relativePath) + + if (section) { + const url = new URL(window.location.href) + url.searchParams.set('lint', relativePath) + return url.toString() + } + + // If we don't have this page in our docs, link to GitHub repo + return `https://github.com/${org}/${repo}/blob/${branch}${pathname}${hash}` + } catch (err) { + console.error('Error transforming markdown URL', err) + return url + } +} + +const transformMarkdown = async ( + rawContent: string, + { replacementLinks = [] }: { replacementLinks?: Array<{ path: string }> } = {} +) => { + const codeHikeOptions: CodeHikeConfig = { + theme: codeHikeTheme, + lineNumbers: true, + showCopyButton: true, + skipLanguages: [], + autoImport: false, + } + + const content = await serialize(rawContent, { + scope: { + chCodeConfig: codeHikeOptions, + }, + mdxOptions: { + remarkPlugins: [ + remarkGfm, + remarkMkDocsAdmonition, + remarkPyMdownTabs, + [removeTitle, meta.title], + [remarkCodeHike, codeHikeOptions], + ], + rehypePlugins: [[linkTransform, urlTransform(replacementLinks)], rehypeSlug], + }, + }) + + return content +} + +/** + * Fetch markdown from external repo and transform links + */ +export const getStaticProps = (async () => { + const octokit = new Octokit() + + const response = await octokit.request('GET /repos/{owner}/{repo}/contents/{path}', { + owner: org, + repo: repo, + path: docsDir, + ref: branch, + headers: { + 'X-GitHub-Api-Version': '2022-11-28', + }, + }) + + if (response.status >= 400) { + throw Error(`Could not get contents of repo ${org}/${repo}`) + } + + if (!Array.isArray(response.data)) { + throw Error( + `Reading a directory, not a file. Should not reach this, solely to appease Typescript.` + ) + } + + const [intro, ...lints] = await Promise.all([ + await transformMarkdown(markdownIntro), + ...response.data + .filter(({ path }) => /docs\/\d+.+\.md$/.test(path)) + .map(async ({ path }, _, data) => { + const fileResponse = await fetch( + `https://raw.githubusercontent.com/${org}/${repo}/${branch}/${path}` + ) + + if (fileResponse.status >= 400) { + throw Error(`Could not get contents of file ${org}/${repo}/${path}`) + } + + const rawContent = await fileResponse.text() + const content = await transformMarkdown(rawContent, { replacementLinks: data }) + + return { + path: getBasename(path), + content, + } + }), + ]) + + return { + props: { + intro, + lints, + }, + } +}) satisfies GetStaticProps diff --git a/package-lock.json b/package-lock.json index 43e2001d8b7..e3c94fbb0b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -230,7 +230,7 @@ "@next/bundle-analyzer": "^14.1.4", "@next/mdx": "^14.1.4", "@octokit/auth-app": "^6.0.3", - "@octokit/core": "^5.0.2", + "@octokit/core": "^5.1.0", "@octokit/graphql": "^7.0.2", "@octokit/plugin-paginate-graphql": "^4.0.0", "@radix-ui/react-accordion": "^1.1.2", @@ -8378,9 +8378,9 @@ } }, "node_modules/@octokit/core": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.0.2.tgz", - "integrity": "sha512-cZUy1gUvd4vttMic7C0lwPed8IYXWYp8kHIMatyhY8t8n3Cpw2ILczkV5pGMPqef7v0bLo0pOHrEHarsau2Ydg==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.1.0.tgz", + "integrity": "sha512-BDa2VAMLSh3otEiaMJ/3Y36GU4qf6GI+VivQ/P41NC6GHcdxpKlqV0ikSZ5gdQsmS3ojXeRx5vasgNTinF0Q4g==", "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.0.0", diff --git a/packages/ui/index.tsx b/packages/ui/index.tsx index cb4c4f0b0f8..81a86053c1b 100644 --- a/packages/ui/index.tsx +++ b/packages/ui/index.tsx @@ -375,3 +375,6 @@ export * from './src/components/Icon/icons/IconZap' // Export hooks export * from './src/lib/Hooks' + +// Shared constants/utils +export { TAB_CHANGE_EVENT_NAME } from './src/lib/events' diff --git a/packages/ui/src/components/Tabs/Tabs.tsx b/packages/ui/src/components/Tabs/Tabs.tsx index e64e74e8659..4f8bb233531 100644 --- a/packages/ui/src/components/Tabs/Tabs.tsx +++ b/packages/ui/src/components/Tabs/Tabs.tsx @@ -81,7 +81,11 @@ const Tabs: React.FC> & TabsSubComponents = ({ */ function handleChange(e: CustomEvent) { - if (e.detail.queryGroup === queryGroup && tabIds.includes(e.detail.id)) { + if ( + e.detail.queryGroup && + e.detail.queryGroup === queryGroup && + tabIds.includes(e.detail.id) + ) { setActiveTab(e.detail.id) setGroupActiveId?.(e.detail.id) } @@ -113,11 +117,12 @@ const Tabs: React.FC> & TabsSubComponents = ({ const url = new URL(document.location.href) url.searchParams.set(queryGroup, id) window.history.replaceState(undefined, '', url) - currentTarget.dispatchEvent( - new CustomEvent(TAB_CHANGE_EVENT_NAME, { bubbles: true, detail: { queryGroup, id } }) - ) } + currentTarget.dispatchEvent( + new CustomEvent(TAB_CHANGE_EVENT_NAME, { bubbles: true, detail: { queryGroup, id } }) + ) + onClick?.(id) if (id !== active) { onChange?.(id)