docs: fixes and refactoring for API reference (#9708)

* docs: fixes and refactoring for API reference

* add route caching

* remove caching

* use next cache
This commit is contained in:
Shahed Nasser
2024-10-22 18:20:06 +03:00
committed by GitHub
parent 6b989353ac
commit b2122c4073
20 changed files with 377 additions and 277 deletions
@@ -3,12 +3,13 @@
import { useScrollController, useSidebar, H2 as UiH2 } from "docs-ui"
import { useEffect, useMemo, useRef, useState } from "react"
import getSectionId from "../../../utils/get-section-id"
import { SidebarItem } from "types"
type H2Props = React.HTMLAttributes<HTMLHeadingElement>
const H2 = ({ children, ...props }: H2Props) => {
const headingRef = useRef<HTMLHeadingElement>(null)
const { activePath, addItems } = useSidebar()
const { activePath, addItems, removeItems } = useSidebar()
const { scrollableElement, scrollToElement } = useScrollController()
const [scrolledFirstTime, setScrolledFirstTime] = useState(false)
@@ -28,14 +29,19 @@ const H2 = ({ children, ...props }: H2Props) => {
}, [scrollableElement, headingRef, id])
useEffect(() => {
addItems([
const item: SidebarItem[] = [
{
type: "link",
path: `${id}`,
title: children as string,
loaded: true,
},
])
]
addItems(item)
return () => {
removeItems(item)
}
}, [id])
return (
@@ -7,7 +7,7 @@ import type { TagOperationParametersProps } from "../.."
import type { TagsOperationParametersNestedProps } from "../../Nested"
import checkRequired from "@/utils/check-required"
import { Loading, type DetailsProps } from "docs-ui"
import { useMemo } from "react"
import { Fragment, useMemo } from "react"
const TagOperationParameters = dynamic<TagOperationParametersProps>(
async () => import("../.."),
@@ -101,20 +101,19 @@ const TagOperationParametersObject = ({
const content = (
<>
{sortedProperties.map((property, index) => (
<>
<Fragment key={index}>
{index !== 0 && <hr className="bg-medusa-border-base my-0" />}
<TagOperationParameters
schemaObject={{
...properties[property],
parameterName: property,
}}
key={index}
isRequired={
properties[property].isRequired ||
checkRequired(schema, property)
}
/>
</>
</Fragment>
))}
</>
)
@@ -7,7 +7,12 @@ import getSectionId from "@/utils/get-section-id"
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import dynamic from "next/dynamic"
import { useInView } from "react-intersection-observer"
import { isElmWindow, useScrollController, useSidebar } from "docs-ui"
import {
isElmWindow,
useIsBrowser,
useScrollController,
useSidebar,
} from "docs-ui"
import type { TagOperationCodeSectionProps } from "./CodeSection"
import TagsOperationDescriptionSection from "./DescriptionSection"
import DividedLayout from "@/layouts/Divided"
@@ -44,9 +49,14 @@ const TagOperation = ({
const nodeRef = useRef<Element | null>(null)
const { loading, removeLoading } = useLoading()
const { scrollableElement, scrollToTop } = useScrollController()
const { isBrowser } = useIsBrowser()
const root = useMemo(() => {
if (!isBrowser) {
return
}
return isElmWindow(scrollableElement) ? document.body : scrollableElement
}, [scrollableElement])
}, [isBrowser, scrollableElement])
const { ref } = useInView({
threshold: 0.3,
rootMargin: `112px 0px 112px 0px`,
@@ -83,6 +93,10 @@ const TagOperation = ({
)
const scrollIntoView = useCallback(() => {
if (!isBrowser) {
return
}
if (nodeRef.current && !checkElementInViewport(nodeRef.current, 0)) {
const elm = nodeRef.current as HTMLElement
scrollToTop(
@@ -91,7 +105,7 @@ const TagOperation = ({
)
}
setShow(true)
}, [scrollToTop, nodeRef])
}, [scrollToTop, nodeRef, isBrowser])
useEffect(() => {
if (nodeRef && nodeRef.current) {
@@ -1,21 +1,16 @@
"use client"
import getSectionId from "@/utils/get-section-id"
import type { OpenAPIV3 } from "openapi-types"
import useSWR from "swr"
import type { Operation, PathsObject } from "@/types/openapi"
import { useSidebar, swrFetcher } from "docs-ui"
import { Fragment, useEffect, useMemo } from "react"
import { useSidebar } from "docs-ui"
import { Fragment, useEffect } from "react"
import dynamic from "next/dynamic"
import type { TagOperationProps } from "../Operation"
import { useArea } from "@/providers/area"
import clsx from "clsx"
import { useBaseSpecs } from "@/providers/base-specs"
import getTagChildSidebarItems from "@/utils/get-tag-child-sidebar-items"
import { useLoading } from "@/providers/loading"
import DividedLoading from "@/components/DividedLoading"
import { SidebarItemSections, SidebarItem, SidebarItemCategory } from "types"
import basePathUrl from "../../../utils/base-path-url"
const TagOperation = dynamic<TagOperationProps>(
async () => import("../Operation")
@@ -23,35 +18,12 @@ const TagOperation = dynamic<TagOperationProps>(
export type TagPathsProps = {
tag: OpenAPIV3.TagObject
paths: PathsObject
} & React.HTMLAttributes<HTMLDivElement>
const TagPaths = ({ tag, className }: TagPathsProps) => {
const tagSlugName = useMemo(() => getSectionId([tag.name]), [tag])
const { area } = useArea()
const TagPaths = ({ tag, className, paths }: TagPathsProps) => {
const { items, addItems, findItemInSection } = useSidebar()
const { baseSpecs } = useBaseSpecs()
const { loading } = useLoading()
// if paths are already loaded since through
// the expanded field, they're loaded directly
// otherwise, they're loaded using the API route
let paths: PathsObject =
baseSpecs?.expandedTags &&
Object.hasOwn(baseSpecs.expandedTags, tagSlugName)
? baseSpecs.expandedTags[tagSlugName]
: {}
const { data } = useSWR<{
paths: PathsObject
}>(
!Object.keys(paths).length
? basePathUrl(`/api/tag?tagName=${tagSlugName}&area=${area}`)
: null,
swrFetcher,
{
errorRetryInterval: 2000,
}
)
paths = data?.paths || paths
useEffect(() => {
if (paths) {
@@ -1,10 +1,13 @@
import { useEffect, useMemo, useRef, useState } from "react"
"use client"
import { useEffect, useMemo, useRef } from "react"
import { SchemaObject } from "../../../../types/openapi"
import TagOperationParameters from "../../Operation/Parameters"
import {
Badge,
CodeBlock,
isElmWindow,
useIsBrowser,
useScrollController,
useSidebar,
} from "docs-ui"
@@ -16,7 +19,6 @@ import useSchemaExample from "../../../../hooks/use-schema-example"
import { InView } from "react-intersection-observer"
import checkElementInViewport from "../../../../utils/check-element-in-viewport"
import { singular } from "pluralize"
import useResizeObserver from "@react-hook/resize-observer"
import clsx from "clsx"
export type TagSectionSchemaProps = {
@@ -26,7 +28,6 @@ export type TagSectionSchemaProps = {
const TagSectionSchema = ({ schema, tagName }: TagSectionSchemaProps) => {
const paramsRef = useRef<HTMLDivElement>(null)
const [maxCodeHeight, setMaxCodeHeight] = useState(0)
const { addItems, setActivePath, activePath } = useSidebar()
const tagSlugName = useMemo(() => getSectionId([tagName]), [tagName])
const formattedName = useMemo(
@@ -43,14 +44,16 @@ const TagSectionSchema = ({ schema, tagName }: TagSectionSchemaProps) => {
skipNonRequired: false,
},
})
useResizeObserver(paramsRef, () => {
setMaxCodeHeight(paramsRef.current?.clientHeight || 0)
})
const { isBrowser } = useIsBrowser()
const { scrollableElement, scrollToElement } = useScrollController()
const root = useMemo(() => {
if (!isBrowser) {
return
}
return isElmWindow(scrollableElement) ? document.body : scrollableElement
}, [scrollableElement])
}, [isBrowser, scrollableElement])
useEffect(() => {
addItems(
@@ -77,18 +80,26 @@ const TagSectionSchema = ({ schema, tagName }: TagSectionSchemaProps) => {
}, [formattedName])
useEffect(() => {
if (!isBrowser) {
return
}
if (schemaSlug === (activePath || location.hash.replace("#", ""))) {
const elm = document.getElementById(schemaSlug) as HTMLElement
if (!checkElementInViewport(elm, 0)) {
scrollToElement(elm)
}
}
}, [activePath, schemaSlug])
}, [activePath, schemaSlug, isBrowser])
const handleViewChange = (
inView: boolean,
entry: IntersectionObserverEntry
) => {
if (!isBrowser) {
return
}
const section = entry.target
if (
@@ -106,7 +117,7 @@ const TagSectionSchema = ({ schema, tagName }: TagSectionSchemaProps) => {
<InView
as="div"
id={schemaSlug}
initialInView={false}
initialInView={true}
onChange={handleViewChange}
root={root}
threshold={0.1}
@@ -128,10 +139,9 @@ const TagSectionSchema = ({ schema, tagName }: TagSectionSchemaProps) => {
source={examples[0].content}
lang="json"
title={`The ${formattedName} Object`}
className={clsx(maxCodeHeight && "overflow-auto")}
className={clsx("overflow-auto")}
style={{
// remove padding + extra space
maxHeight: maxCodeHeight ? maxCodeHeight - 212 : "unset",
maxHeight: "100vh",
}}
/>
)}
@@ -6,13 +6,13 @@ import { useEffect, useMemo, useState } from "react"
import {
isElmWindow,
swrFetcher,
useIsBrowser,
useScrollController,
useSidebar,
} from "docs-ui"
import dynamic from "next/dynamic"
import type { SectionProps } from "../../Section"
import type { MDXContentClientProps } from "../../MDXContent/Client"
import TagPaths from "../Paths"
import DividedLayout from "@/layouts/Divided"
import LoadingProvider from "@/providers/loading"
import SectionContainer from "../../Section/Container"
@@ -22,11 +22,12 @@ import clsx from "clsx"
import { Feedback, Loading, Link } from "docs-ui"
import { usePathname, useRouter } from "next/navigation"
import formatReportLink from "@/utils/format-report-link"
import { SchemaObject, TagObject } from "@/types/openapi"
import useSWR from "swr"
import { PathsObject, SchemaObject, TagObject } from "@/types/openapi"
import { TagSectionSchemaProps } from "./Schema"
import basePathUrl from "../../../utils/base-path-url"
import checkElementInViewport from "../../../utils/check-element-in-viewport"
import TagPaths from "../Paths"
import useSWR from "swr"
import basePathUrl from "../../../utils/base-path-url"
export type TagSectionProps = {
tag: TagObject
@@ -47,39 +48,30 @@ const MDXContentClient = dynamic<MDXContentClientProps>(
}
) as React.FC<MDXContentClientProps>
const TagSection = ({ tag }: TagSectionProps) => {
const TagSectionComponent = ({ tag }: TagSectionProps) => {
const { activePath, setActivePath } = useSidebar()
const router = useRouter()
const [loadPaths, setLoadPaths] = useState(false)
const [loadData, setLoadData] = useState(false)
const slugTagName = useMemo(() => getSectionId([tag.name]), [tag])
const { area } = useArea()
const pathname = usePathname()
const { scrollableElement, scrollToTop } = useScrollController()
const { data } = useSWR<{
schema: SchemaObject
}>(
tag["x-associatedSchema"]
? basePathUrl(
`/api/schema?name=${tag["x-associatedSchema"].$ref}&area=${area}`
)
: null,
swrFetcher,
{
errorRetryInterval: 2000,
}
)
const associatedSchema = data?.schema
const { isBrowser } = useIsBrowser()
const root = useMemo(() => {
if (!isBrowser) {
return
}
return isElmWindow(scrollableElement) ? document.body : scrollableElement
}, [scrollableElement])
const { ref } = useInView({
}, [scrollableElement, isBrowser])
const { ref, inView } = useInView({
threshold: 0.8,
rootMargin: `112px 0px 112px 0px`,
root,
onChange: (inView) => {
if (inView && !loadPaths) {
setLoadPaths(true)
if (inView && !loadData) {
setLoadData(true)
}
if (inView) {
// ensure that the hash link doesn't change if it links to an inner path
@@ -97,8 +89,36 @@ const TagSection = ({ tag }: TagSectionProps) => {
}
},
})
const { data: schemaData } = useSWR<{
schema: SchemaObject
}>(
loadData && tag["x-associatedSchema"]
? basePathUrl(
`/api/schema?name=${tag["x-associatedSchema"].$ref}&area=${area}`
)
: null,
swrFetcher,
{
errorRetryInterval: 2000,
}
)
const { data: pathsData } = useSWR<{
paths: PathsObject
}>(
loadData
? basePathUrl(`/api/tag?tagName=${slugTagName}&area=${area}`)
: null,
swrFetcher,
{
errorRetryInterval: 2000,
}
)
useEffect(() => {
if (!isBrowser) {
return
}
if (activePath && activePath.includes(slugTagName)) {
const tagName = activePath.split("_")
if (tagName.length === 1 && tagName[0] === slugTagName) {
@@ -110,18 +130,18 @@ const TagSection = ({ tag }: TagSectionProps) => {
)
}
} else if (tagName.length > 1 && tagName[0] === slugTagName) {
setLoadPaths(true)
setLoadData(true)
}
}
}, [slugTagName, activePath])
}, [slugTagName, activePath, isBrowser])
return (
<div
className={clsx("min-h-screen", !loadPaths && "relative")}
className={clsx("min-h-screen", !loadData && "relative")}
id={slugTagName}
ref={ref}
>
<DividedLayout
ref={ref}
mainContent={
<SectionContainer noDivider={true}>
<h2>{tag.name}</h2>
@@ -159,17 +179,17 @@ const TagSection = ({ tag }: TagSectionProps) => {
}
codeContent={<></>}
/>
{associatedSchema && (
<TagSectionSchema schema={associatedSchema} tagName={tag.name} />
{schemaData && (
<TagSectionSchema schema={schemaData.schema} tagName={tag.name} />
)}
{loadPaths && (
{loadData && pathsData && (
<LoadingProvider initialLoading={true}>
<TagPaths tag={tag} />
<TagPaths tag={tag} paths={pathsData.paths} />
</LoadingProvider>
)}
{!loadPaths && <SectionDivider className="lg:!-left-1" />}
{!loadData && <SectionDivider className="lg:!-left-1" />}
</div>
)
}
export default TagSection
export default TagSectionComponent
@@ -1,120 +1,20 @@
"use client"
import type { OpenAPIV3 } from "openapi-types"
import { useEffect, useState } from "react"
import useSWR from "swr"
import { useBaseSpecs } from "@/providers/base-specs"
import { OpenAPIV3 } from "openapi-types"
import { TagSectionProps } from "./Section"
import dynamic from "next/dynamic"
import type { TagSectionProps } from "./Section"
import { useArea } from "@/providers/area"
import { swrFetcher, useSidebar } from "docs-ui"
import getSectionId from "@/utils/get-section-id"
import { ExpandedDocument } from "@/types/openapi"
import getTagChildSidebarItems from "@/utils/get-tag-child-sidebar-items"
import { SidebarItem, SidebarItemSections } from "types"
import basePathUrl from "../../utils/base-path-url"
import { useRouter } from "next/navigation"
const TagSection = dynamic<TagSectionProps>(
async () => import("./Section")
) as React.FC<TagSectionProps>
export type TagsProps = React.HTMLAttributes<HTMLDivElement>
function getCurrentTag() {
return typeof location !== "undefined"
? location.hash.replace("#", "").split("_")[0]
: ""
type TagsProps = {
tags?: OpenAPIV3.TagObject[]
}
const Tags = () => {
const [tags, setTags] = useState<OpenAPIV3.TagObject[]>([])
const [loadData, setLoadData] = useState<boolean>(false)
const [expand, setExpand] = useState<string>("")
const { baseSpecs, setBaseSpecs } = useBaseSpecs()
const { activePath, addItems, setActivePath } = useSidebar()
const { area, prevArea } = useArea()
const router = useRouter()
const { data } = useSWR<ExpandedDocument>(
loadData && !baseSpecs
? basePathUrl(`/api/base-specs?area=${area}&expand=${expand}`)
: null,
swrFetcher,
{
errorRetryInterval: 2000,
}
)
useEffect(() => {
setExpand(getCurrentTag())
}, [])
useEffect(() => {
setLoadData(true)
}, [expand])
useEffect(() => {
if (data) {
setBaseSpecs(data)
}
if (data?.tags) {
setTags(data.tags)
}
}, [data, setBaseSpecs])
useEffect(() => {
if (baseSpecs) {
if (prevArea !== area) {
setBaseSpecs(null)
setLoadData(true)
return
}
const itemsToAdd: SidebarItem[] = [
{
type: "separator",
},
]
if (baseSpecs.tags) {
baseSpecs.tags.forEach((tag) => {
const tagPathName = getSectionId([tag.name.toLowerCase()])
const childItems =
baseSpecs.expandedTags &&
Object.hasOwn(baseSpecs.expandedTags, tagPathName)
? getTagChildSidebarItems(baseSpecs.expandedTags[tagPathName])
: []
itemsToAdd.push({
type: "category",
title: tag.name,
children: childItems,
loaded: childItems.length > 0,
showLoadingIfEmpty: true,
onOpen: () => {
if (location.hash !== tagPathName) {
router.push(`#${tagPathName}`, {
scroll: false,
})
}
if (activePath !== tagPathName) {
setActivePath(tagPathName)
}
},
})
})
}
addItems(itemsToAdd, {
section: SidebarItemSections.DEFAULT,
})
}
}, [baseSpecs, prevArea, area])
const Tags = ({ tags }: TagsProps) => {
return (
<>
{tags.map((tag, index) => (
<TagSection tag={tag} key={index} />
{tags?.map((tag) => (
<TagSection tag={tag} key={tag.name} />
))}
</>
)