From d2c3f08c609440f071f38181fe3721dd611fb4cf Mon Sep 17 00:00:00 2001 From: Andrew Jiang Date: Tue, 4 Jun 2024 15:04:11 -0400 Subject: [PATCH] fix: bug bashing PR #955 (#957) --- packages/commons/fdr-utils/src/types.ts | 3 +- .../yes-version-no-tabs.test.ts.snap | 12 +- .../yes-version-yes-tabs.test.ts.snap | 4 +- .../__test__/output/humanloop/node.json | 4 +- .../output/humanloop/versionNodes.json | 4 +- .../output/yes-version-no-tabs/node.json | 6 +- .../yes-version-no-tabs/versionNodes.json | 6 +- .../output/yes-version-yes-tabs/node.json | 6 +- .../yes-version-yes-tabs/versionNodes.json | 6 +- .../converters/NavigationConfigConverter.ts | 6 +- .../PlaygroundEndpointSelectorContent.tsx | 27 ++++- packages/ui/app/src/components/FernLink.tsx | 111 +++++++++-------- packages/ui/app/src/docs/VersionDropdown.tsx | 10 +- packages/ui/components/src/FernDropdown.tsx | 114 ++++++++++-------- .../docs-bundle/src/utils/getDocsPageProps.ts | 3 + .../src/utils/getDocsPageProps.ts | 1 + 16 files changed, 189 insertions(+), 134 deletions(-) diff --git a/packages/commons/fdr-utils/src/types.ts b/packages/commons/fdr-utils/src/types.ts index 16ad5c156c..9e3a743895 100644 --- a/packages/commons/fdr-utils/src/types.ts +++ b/packages/commons/fdr-utils/src/types.ts @@ -7,7 +7,8 @@ export interface ColorsConfig { } export interface SidebarVersionInfo { - id: string; + id: FernNavigation.VersionId; + title: string; slug: FernNavigation.Slug; index: number; availability: FernNavigation.Availability | undefined; diff --git a/packages/fdr-sdk/src/navigation/__test__/__snapshots__/yes-version-no-tabs.test.ts.snap b/packages/fdr-sdk/src/navigation/__test__/__snapshots__/yes-version-no-tabs.test.ts.snap index e64a75e376..5a642c2f9f 100644 --- a/packages/fdr-sdk/src/navigation/__test__/__snapshots__/yes-version-no-tabs.test.ts.snap +++ b/packages/fdr-sdk/src/navigation/__test__/__snapshots__/yes-version-no-tabs.test.ts.snap @@ -40,7 +40,7 @@ exports[`getNavigationRoot > yes-version-no-tabs > gets navigation root for /doc } `; -exports[`getNavigationRoot > yes-version-no-tabs > gets navigation root for /docs/api/page-5 2`] = `"version-1"`; +exports[`getNavigationRoot > yes-version-no-tabs > gets navigation root for /docs/api/page-5 2`] = `"Version 1"`; exports[`getNavigationRoot > yes-version-no-tabs > gets navigation root for /docs/api/page-5 3`] = `undefined`; @@ -63,7 +63,7 @@ exports[`getNavigationRoot > yes-version-no-tabs > gets navigation root for /doc } `; -exports[`getNavigationRoot > yes-version-no-tabs > gets navigation root for /docs/api/version-1 2`] = `"version-1"`; +exports[`getNavigationRoot > yes-version-no-tabs > gets navigation root for /docs/api/version-1 2`] = `"Version 1"`; exports[`getNavigationRoot > yes-version-no-tabs > gets navigation root for /docs/api/version-1 3`] = `undefined`; @@ -79,7 +79,7 @@ exports[`getNavigationRoot > yes-version-no-tabs > gets navigation root for /doc } `; -exports[`getNavigationRoot > yes-version-no-tabs > gets navigation root for /docs/api/version-1 5`] = `"version-1"`; +exports[`getNavigationRoot > yes-version-no-tabs > gets navigation root for /docs/api/version-1 5`] = `"Version 1"`; exports[`getNavigationRoot > yes-version-no-tabs > gets navigation root for /docs/api/version-1 6`] = `undefined`; @@ -95,7 +95,7 @@ exports[`getNavigationRoot > yes-version-no-tabs > gets navigation root for /doc } `; -exports[`getNavigationRoot > yes-version-no-tabs > gets navigation root for /docs/api/version-1/page-5 2`] = `"version-1"`; +exports[`getNavigationRoot > yes-version-no-tabs > gets navigation root for /docs/api/version-1/page-5 2`] = `"Version 1"`; exports[`getNavigationRoot > yes-version-no-tabs > gets navigation root for /docs/api/version-1/page-5 3`] = `undefined`; @@ -118,7 +118,7 @@ exports[`getNavigationRoot > yes-version-no-tabs > gets navigation root for /doc } `; -exports[`getNavigationRoot > yes-version-no-tabs > gets navigation root for /docs/api/version-1/version-1 2`] = `"version-1"`; +exports[`getNavigationRoot > yes-version-no-tabs > gets navigation root for /docs/api/version-1/version-1 2`] = `"Version 1"`; exports[`getNavigationRoot > yes-version-no-tabs > gets navigation root for /docs/api/version-1/version-1 3`] = `undefined`; @@ -141,6 +141,6 @@ exports[`getNavigationRoot > yes-version-no-tabs > gets navigation root for /doc } `; -exports[`getNavigationRoot > yes-version-no-tabs > gets navigation root for /docs/api/version-2/version-1 2`] = `"version-2"`; +exports[`getNavigationRoot > yes-version-no-tabs > gets navigation root for /docs/api/version-2/version-1 2`] = `"Version 2"`; exports[`getNavigationRoot > yes-version-no-tabs > gets navigation root for /docs/api/version-2/version-1 3`] = `undefined`; diff --git a/packages/fdr-sdk/src/navigation/__test__/__snapshots__/yes-version-yes-tabs.test.ts.snap b/packages/fdr-sdk/src/navigation/__test__/__snapshots__/yes-version-yes-tabs.test.ts.snap index 0790ab6c21..5de3ddbe75 100644 --- a/packages/fdr-sdk/src/navigation/__test__/__snapshots__/yes-version-yes-tabs.test.ts.snap +++ b/packages/fdr-sdk/src/navigation/__test__/__snapshots__/yes-version-yes-tabs.test.ts.snap @@ -51,7 +51,7 @@ exports[`getNavigationRoot > yes-version-yes-tabs > gets navigation root for /do } `; -exports[`getNavigationRoot > yes-version-yes-tabs > gets navigation root for /docs/api/tab-1 2`] = `"version-1"`; +exports[`getNavigationRoot > yes-version-yes-tabs > gets navigation root for /docs/api/tab-1 2`] = `"Version 1"`; exports[`getNavigationRoot > yes-version-yes-tabs > gets navigation root for /docs/api/tab-1 3`] = `"docs/api/tab-1"`; @@ -81,7 +81,7 @@ exports[`getNavigationRoot > yes-version-yes-tabs > gets navigation root for /do } `; -exports[`getNavigationRoot > yes-version-yes-tabs > gets navigation root for /docs/api/version-1/section-2 2`] = `"version-1"`; +exports[`getNavigationRoot > yes-version-yes-tabs > gets navigation root for /docs/api/version-1/section-2 2`] = `"Version 1"`; exports[`getNavigationRoot > yes-version-yes-tabs > gets navigation root for /docs/api/version-1/section-2 3`] = `"docs/api/version-1/tab-1"`; diff --git a/packages/fdr-sdk/src/navigation/__test__/output/humanloop/node.json b/packages/fdr-sdk/src/navigation/__test__/output/humanloop/node.json index 7d0aa6aae9..fb210fd771 100644 --- a/packages/fdr-sdk/src/navigation/__test__/output/humanloop/node.json +++ b/packages/fdr-sdk/src/navigation/__test__/output/humanloop/node.json @@ -10,7 +10,7 @@ "type": "version", "title": "v4.0", "default": true, - "versionId": "v4", + "versionId": "v4.0", "slug": "", "child": { "id": "root..*.uv", @@ -2071,7 +2071,7 @@ "type": "version", "title": "v4.0", "default": false, - "versionId": "v4", + "versionId": "v4.0", "slug": "v4", "hidden": true, "child": { diff --git a/packages/fdr-sdk/src/navigation/__test__/output/humanloop/versionNodes.json b/packages/fdr-sdk/src/navigation/__test__/output/humanloop/versionNodes.json index e91f29a34d..10900b58ff 100644 --- a/packages/fdr-sdk/src/navigation/__test__/output/humanloop/versionNodes.json +++ b/packages/fdr-sdk/src/navigation/__test__/output/humanloop/versionNodes.json @@ -4,7 +4,7 @@ "type": "version", "title": "v4.0", "default": true, - "versionId": "v4", + "versionId": "v4.0", "slug": "", "child": { "id": "root..*.uv", @@ -2065,7 +2065,7 @@ "type": "version", "title": "v4.0", "default": false, - "versionId": "v4", + "versionId": "v4.0", "slug": "v4", "hidden": true, "child": { diff --git a/packages/fdr-sdk/src/navigation/__test__/output/yes-version-no-tabs/node.json b/packages/fdr-sdk/src/navigation/__test__/output/yes-version-no-tabs/node.json index a0cbdcd83c..1576a9e996 100644 --- a/packages/fdr-sdk/src/navigation/__test__/output/yes-version-no-tabs/node.json +++ b/packages/fdr-sdk/src/navigation/__test__/output/yes-version-no-tabs/node.json @@ -10,7 +10,7 @@ "type": "version", "title": "Version 1", "default": true, - "versionId": "version-1", + "versionId": "Version 1", "slug": "docs/api", "child": { "id": "root..*.uv", @@ -97,7 +97,7 @@ "type": "version", "title": "Version 1", "default": false, - "versionId": "version-1", + "versionId": "Version 1", "slug": "docs/api/version-1", "hidden": true, "child": { @@ -185,7 +185,7 @@ "type": "version", "title": "Version 2", "default": false, - "versionId": "version-2", + "versionId": "Version 2", "slug": "docs/api/version-2", "hidden": false, "child": { diff --git a/packages/fdr-sdk/src/navigation/__test__/output/yes-version-no-tabs/versionNodes.json b/packages/fdr-sdk/src/navigation/__test__/output/yes-version-no-tabs/versionNodes.json index 361dafdd9d..816c2a71d4 100644 --- a/packages/fdr-sdk/src/navigation/__test__/output/yes-version-no-tabs/versionNodes.json +++ b/packages/fdr-sdk/src/navigation/__test__/output/yes-version-no-tabs/versionNodes.json @@ -4,7 +4,7 @@ "type": "version", "title": "Version 1", "default": true, - "versionId": "version-1", + "versionId": "Version 1", "slug": "docs/api", "child": { "id": "root..*.uv", @@ -91,7 +91,7 @@ "type": "version", "title": "Version 1", "default": false, - "versionId": "version-1", + "versionId": "Version 1", "slug": "docs/api/version-1", "hidden": true, "child": { @@ -179,7 +179,7 @@ "type": "version", "title": "Version 2", "default": false, - "versionId": "version-2", + "versionId": "Version 2", "slug": "docs/api/version-2", "hidden": false, "child": { diff --git a/packages/fdr-sdk/src/navigation/__test__/output/yes-version-yes-tabs/node.json b/packages/fdr-sdk/src/navigation/__test__/output/yes-version-yes-tabs/node.json index d8d7d7dec7..4aca2d1c2a 100644 --- a/packages/fdr-sdk/src/navigation/__test__/output/yes-version-yes-tabs/node.json +++ b/packages/fdr-sdk/src/navigation/__test__/output/yes-version-yes-tabs/node.json @@ -10,7 +10,7 @@ "type": "version", "title": "Version 1", "default": true, - "versionId": "version-1", + "versionId": "Version 1", "slug": "docs/api", "child": { "id": "root..*.uv", @@ -175,7 +175,7 @@ "type": "version", "title": "Version 1", "default": false, - "versionId": "version-1", + "versionId": "Version 1", "slug": "docs/api/version-1", "hidden": true, "child": { @@ -341,7 +341,7 @@ "type": "version", "title": "Version 2", "default": false, - "versionId": "version-2", + "versionId": "Version 2", "slug": "docs/api/version-2", "hidden": false, "child": { diff --git a/packages/fdr-sdk/src/navigation/__test__/output/yes-version-yes-tabs/versionNodes.json b/packages/fdr-sdk/src/navigation/__test__/output/yes-version-yes-tabs/versionNodes.json index 527be6cc5c..9312730c4d 100644 --- a/packages/fdr-sdk/src/navigation/__test__/output/yes-version-yes-tabs/versionNodes.json +++ b/packages/fdr-sdk/src/navigation/__test__/output/yes-version-yes-tabs/versionNodes.json @@ -4,7 +4,7 @@ "type": "version", "title": "Version 1", "default": true, - "versionId": "version-1", + "versionId": "Version 1", "slug": "docs/api", "child": { "id": "root..*.uv", @@ -169,7 +169,7 @@ "type": "version", "title": "Version 1", "default": false, - "versionId": "version-1", + "versionId": "Version 1", "slug": "docs/api/version-1", "hidden": true, "child": { @@ -335,7 +335,7 @@ "type": "version", "title": "Version 2", "default": false, - "versionId": "version-2", + "versionId": "Version 2", "slug": "docs/api/version-2", "hidden": false, "child": { diff --git a/packages/fdr-sdk/src/navigation/converters/NavigationConfigConverter.ts b/packages/fdr-sdk/src/navigation/converters/NavigationConfigConverter.ts index d8eb4fec39..1fb5ef049d 100644 --- a/packages/fdr-sdk/src/navigation/converters/NavigationConfigConverter.ts +++ b/packages/fdr-sdk/src/navigation/converters/NavigationConfigConverter.ts @@ -74,7 +74,8 @@ export class NavigationConfigConverter { type: "version", title: version.version, default: true, - versionId: FernNavigation.VersionId(version.urlSlug), + // the versionId must match `indexSegmentsByVersionId` + versionId: FernNavigation.VersionId(version.version), slug: baseSlug, icon: undefined, hidden: undefined, @@ -95,7 +96,8 @@ export class NavigationConfigConverter { type: "version", title: version.version, default: false, - versionId: FernNavigation.VersionId(version.urlSlug), + // the versionId must match `indexSegmentsByVersionId` + versionId: FernNavigation.VersionId(version.version), slug, icon: undefined, hidden: idx === 0, // hidden from the version selector diff --git a/packages/ui/app/src/api-playground/PlaygroundEndpointSelectorContent.tsx b/packages/ui/app/src/api-playground/PlaygroundEndpointSelectorContent.tsx index 2575f38478..207de0a93b 100644 --- a/packages/ui/app/src/api-playground/PlaygroundEndpointSelectorContent.tsx +++ b/packages/ui/app/src/api-playground/PlaygroundEndpointSelectorContent.tsx @@ -41,17 +41,38 @@ export function flattenApiSection(root: FernNavigation.SidebarRootNode): ApiGrou return; } - const breadcrumbs = parents.filter(FernNavigation.isSection).map((parent) => parent); + const breadcrumbs = [...parents, node].filter(FernNavigation.isSection).map((parent) => parent.title); result.push({ api: node.apiDefinitionId, id: node.id, - breadcrumbs: breadcrumbs.map((breadcrumb) => breadcrumb.title), + breadcrumbs, items, }); } return; }); - return result; + + if (result.length === 0) { + return []; + } + + /** + * we want to get the lowest level of breadcrumbs shared by all groups + * for example: + * - [a, b, c] + * - [a, b, d] + * + * the shared breadcrumbs would be [a, b], and the resulting breadcrumbs for each group would be [c] and [d] + */ + const allBreadcrumbs = result.map((group) => group.breadcrumbs); + const sharedBreadcrumbs = allBreadcrumbs.reduce((acc, breadcrumbs) => { + return acc.filter((breadcrumb, idx) => breadcrumb === breadcrumbs[idx]); + }, allBreadcrumbs[0]); + + return result.map((group) => ({ + ...group, + breadcrumbs: group.breadcrumbs.slice(sharedBreadcrumbs.length), + })); } function matchesEndpoint(query: string, group: ApiGroup, endpoint: FernNavigation.NavigationNodeApiLeaf): boolean { diff --git a/packages/ui/app/src/components/FernLink.tsx b/packages/ui/app/src/components/FernLink.tsx index 1cefb4225e..b7b7a03abe 100644 --- a/packages/ui/app/src/components/FernLink.tsx +++ b/packages/ui/app/src/components/FernLink.tsx @@ -1,6 +1,6 @@ import { ExternalLinkIcon } from "@radix-ui/react-icons"; import Link from "next/link"; -import { ReactElement, useEffect, useState, type ComponentProps } from "react"; +import { ReactElement, forwardRef, useEffect, useState, type ComponentProps } from "react"; import { format, parse, resolve, type UrlObject } from "url"; import { useDocsContext } from "../contexts/docs-context/useDocsContext"; import { useNavigationContext } from "../contexts/navigation-context"; @@ -9,68 +9,79 @@ interface FernLinkProps extends ComponentProps { showExternalLinkIcon?: boolean; } -export function FernLink({ showExternalLinkIcon = false, ...props }: FernLinkProps): ReactElement { - const url = toUrlObject(props.href); - const isExternalUrl = checkIsExternalUrl(url); +export const FernLink = forwardRef( + ({ showExternalLinkIcon = false, ...props }, ref): ReactElement => { + const url = toUrlObject(props.href); + const isExternalUrl = checkIsExternalUrl(url); - // if the url is relative, we will need to invoke useRouter to resolve the relative url - // since useRouter injects the router context, it will cause a re-render any time the route changes. - // to avoid unnecessary re-renders, we will isolate the useRouter call to a separate component. - if (!isExternalUrl && checkIsRelativeUrl(url)) { - return ; - } + // if the url is relative, we will need to invoke useRouter to resolve the relative url + // since useRouter injects the router context, it will cause a re-render any time the route changes. + // to avoid unnecessary re-renders, we will isolate the useRouter call to a separate component. + if (!isExternalUrl && checkIsRelativeUrl(url)) { + return ; + } - if (isExternalUrl) { - return ; - } + if (isExternalUrl) { + return ; + } - return ; -} + return ; + }, +); + +FernLink.displayName = "FernLink"; -function FernRelativeLink(props: ComponentProps) { +const FernRelativeLink = forwardRef>((props, ref) => { const { selectedSlug } = useNavigationContext(); const href = resolveRelativeUrl(`/${selectedSlug}`, formatUrlString(props.href)); - return ; -} + return ; +}); + +FernRelativeLink.displayName = "FernRelativeLink"; interface FernExternalLinkProps extends Omit, "href"> { showExternalLinkIcon: boolean; url: UrlObject; } -function FernExternalLink({ showExternalLinkIcon, url, ...props }: FernExternalLinkProps) { - const { domain } = useDocsContext(); - const [host, setHost] = useState(domain); - useEffect(() => { - if (typeof window !== "undefined") { - setHost(window.location.host); - } - }, []); - - // if the link is to a different domain, always open in a new tab - // TODO: if the link is to the same domain, we should check if the page is a fern page, and if so, use the Link component to leverage client-side navigation - const isSameSite = host === url.host; - return ( - // eslint-disable-next-line react/jsx-no-target-blank - ( + ({ showExternalLinkIcon, url, ...props }, ref) => { + const { domain } = useDocsContext(); + const [host, setHost] = useState(domain); + useEffect(() => { + if (typeof window !== "undefined") { + setHost(window.location.host); } - href={formatUrlString(url)} - > - {props.children} - {!isSameSite && showExternalLinkIcon && } - - ); -} + }, []); + + // if the link is to a different domain, always open in a new tab + // TODO: if the link is to the same domain, we should check if the page is a fern page, and if so, use the Link component to leverage client-side navigation + const isSameSite = host === url.host; + return ( + // eslint-disable-next-line react/jsx-no-target-blank + + {props.children} + {!isSameSite && showExternalLinkIcon && } + + ); + }, +); + +FernExternalLink.displayName = "FernExternalLink"; export function toUrlObject(url: string | UrlObject): UrlObject { if (url == null) { diff --git a/packages/ui/app/src/docs/VersionDropdown.tsx b/packages/ui/app/src/docs/VersionDropdown.tsx index 2075455546..d9803a7233 100644 --- a/packages/ui/app/src/docs/VersionDropdown.tsx +++ b/packages/ui/app/src/docs/VersionDropdown.tsx @@ -14,6 +14,8 @@ export const VersionDropdown: React.FC = () => { const { versions, currentVersionId } = useDocsContext(); const { unversionedSlug } = useNavigationContext(); + const currentVersion = versions.find(({ id }) => id === currentVersionId); + if (versions.length <= 1) { return null; } @@ -21,11 +23,11 @@ export const VersionDropdown: React.FC = () => {
({ + options={versions.map(({ id, title, availability, slug }) => ({ type: "value", - label: versionName, + label: title, helperText: availability != null ? getVersionAvailabilityLabel(availability) : undefined, - value: versionName, + value: id, disabled: availability == null, href: urljoin("/", slug, unversionedSlug), }))} @@ -33,7 +35,7 @@ export const VersionDropdown: React.FC = () => { } disableAutomaticTooltip /> diff --git a/packages/ui/components/src/FernDropdown.tsx b/packages/ui/components/src/FernDropdown.tsx index 2717d4cfbd..2dc2f7dd2b 100644 --- a/packages/ui/components/src/FernDropdown.tsx +++ b/packages/ui/components/src/FernDropdown.tsx @@ -6,6 +6,7 @@ import { ReactElement, ReactNode, cloneElement, + forwardRef, useCallback, useEffect, useRef, @@ -47,58 +48,71 @@ export declare namespace FernDropdown { } } -export function FernDropdown({ - options, - onValueChange, - value, - children, - onOpen, - usePortal = true, - side, - align, - defaultOpen = false, - dropdownMenuElement, -}: PropsWithChildren): ReactElement { - const [isOpen, setOpen] = useState(defaultOpen); - const handleOpenChange = useCallback( - (toOpen: boolean) => { - setOpen(toOpen); - if (toOpen && onOpen != null) { - onOpen(); - } +export const FernDropdown = forwardRef>( + ( + { + options, + onValueChange, + value, + children, + onOpen, + usePortal = true, + side, + align, + defaultOpen = false, + dropdownMenuElement, }, - [onOpen], - ); - const renderDropdownContent = () => ( - - - - - {options.map((option, idx) => - option.type === "value" ? ( - - ) : ( - - ), - )} - - - - - ); + ref, + ): ReactElement => { + const [isOpen, setOpen] = useState(defaultOpen); + const handleOpenChange = useCallback( + (toOpen: boolean) => { + setOpen(toOpen); + if (toOpen && onOpen != null) { + onOpen(); + } + }, + [onOpen], + ); + const renderDropdownContent = () => ( + + + + + {options.map((option, idx) => + option.type === "value" ? ( + + ) : ( + + ), + )} + + + + + ); - return ( - - {children} - {usePortal ? {renderDropdownContent()} : renderDropdownContent()} - - ); -} + return ( + + + {children} + + {usePortal ? ( + {renderDropdownContent()} + ) : ( + renderDropdownContent() + )} + + ); + }, +); + +FernDropdown.displayName = "FernDropdown"; function FernDropdownItemValue({ option, diff --git a/packages/ui/docs-bundle/src/utils/getDocsPageProps.ts b/packages/ui/docs-bundle/src/utils/getDocsPageProps.ts index 7407f1b622..1c36896af8 100644 --- a/packages/ui/docs-bundle/src/utils/getDocsPageProps.ts +++ b/packages/ui/docs-bundle/src/utils/getDocsPageProps.ts @@ -60,6 +60,8 @@ export async function getDocsPageProps( type: "redirect", redirect: await getUnauthenticatedRedirect(xFernHost), }; + } else if ((docs.error as any).content.statusCode === 404) { + return { type: "notFound", notFound: true }; } // eslint-disable-next-line no-console @@ -276,6 +278,7 @@ async function convertDocsToDocsPageProps({ versions: node.versions .filter((version) => !version.hidden) .map((version, index) => ({ + title: version.title, id: version.versionId, slug: version.slug, index, diff --git a/packages/ui/local-preview-bundle/src/utils/getDocsPageProps.ts b/packages/ui/local-preview-bundle/src/utils/getDocsPageProps.ts index de8e37df09..80b415678d 100644 --- a/packages/ui/local-preview-bundle/src/utils/getDocsPageProps.ts +++ b/packages/ui/local-preview-bundle/src/utils/getDocsPageProps.ts @@ -119,6 +119,7 @@ export async function getDocsPageProps( versions: node.versions .filter((version) => !version.hidden) .map((version, index) => ({ + title: version.title, id: version.versionId, slug: version.slug, index,