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:
@@ -3,22 +3,35 @@ import AdminContent from "../_mdx/admin.mdx"
|
||||
import Tags from "@/components/Tags"
|
||||
import PageTitleProvider from "@/providers/page-title"
|
||||
import { H1 } from "docs-ui"
|
||||
import { getBaseSpecs } from "../../lib"
|
||||
import BaseSpecsProvider from "../../providers/base-specs"
|
||||
import clsx from "clsx"
|
||||
|
||||
const AdminPage = async () => {
|
||||
const data = await getBaseSpecs("admin")
|
||||
|
||||
const ReferencePage = async () => {
|
||||
return (
|
||||
<AreaProvider area={"admin"}>
|
||||
<PageTitleProvider>
|
||||
<H1 className="!h2-docs scroll-m-56 lg:pl-4" id="introduction">
|
||||
Medusa V2 Admin API Reference
|
||||
</H1>
|
||||
<AdminContent />
|
||||
<Tags />
|
||||
</PageTitleProvider>
|
||||
</AreaProvider>
|
||||
<BaseSpecsProvider baseSpecs={data}>
|
||||
<AreaProvider area={"admin"}>
|
||||
<PageTitleProvider>
|
||||
<H1
|
||||
className={clsx(
|
||||
"!h2-docs lg:pl-4",
|
||||
"scroll-m-[184px] lg:scroll-m-[264px]"
|
||||
)}
|
||||
id="introduction"
|
||||
>
|
||||
Medusa V2 Admin API Reference
|
||||
</H1>
|
||||
<AdminContent />
|
||||
<Tags tags={data?.tags} />
|
||||
</PageTitleProvider>
|
||||
</AreaProvider>
|
||||
</BaseSpecsProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default ReferencePage
|
||||
export default AdminPage
|
||||
|
||||
export function generateMetadata() {
|
||||
return {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import { SchemaObject } from "../../../types/openapi"
|
||||
import path from "path"
|
||||
import { existsSync, promises as fs } from "fs"
|
||||
import { parseDocument } from "yaml"
|
||||
import dereference from "../../../utils/dereference"
|
||||
import { existsSync } from "fs"
|
||||
import getSchemaContent from "../../../utils/get-schema-content"
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const { searchParams } = new URL(request.url)
|
||||
@@ -59,14 +57,8 @@ export async function GET(request: Request) {
|
||||
)
|
||||
}
|
||||
|
||||
const schemaContent = await fs.readFile(schemaPath, "utf-8")
|
||||
const schema = parseDocument(schemaContent).toJS() as SchemaObject
|
||||
|
||||
// resolve references in schema
|
||||
const dereferencedDocument = await dereference({
|
||||
basePath: baseSchemasPath,
|
||||
schemas: [schema],
|
||||
})
|
||||
const { dereferencedDocument, originalSchema: schema } =
|
||||
await getSchemaContent(schemaPath, baseSchemasPath)
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
|
||||
@@ -3,22 +3,35 @@ import StoreContent from "../_mdx/store.mdx"
|
||||
import Tags from "@/components/Tags"
|
||||
import PageTitleProvider from "@/providers/page-title"
|
||||
import { H1 } from "docs-ui"
|
||||
import { getBaseSpecs } from "../../lib"
|
||||
import BaseSpecsProvider from "../../providers/base-specs"
|
||||
import clsx from "clsx"
|
||||
|
||||
const StorePage = async () => {
|
||||
const data = await getBaseSpecs("store")
|
||||
|
||||
const ReferencePage = async () => {
|
||||
return (
|
||||
<AreaProvider area={"store"}>
|
||||
<PageTitleProvider>
|
||||
<H1 className="!h2-docs scroll-m-56 lg:pl-4" id="introduction">
|
||||
Medusa V2 Store API Reference
|
||||
</H1>
|
||||
<StoreContent />
|
||||
<Tags />
|
||||
</PageTitleProvider>
|
||||
</AreaProvider>
|
||||
<BaseSpecsProvider baseSpecs={data}>
|
||||
<AreaProvider area={"store"}>
|
||||
<PageTitleProvider>
|
||||
<H1
|
||||
className={clsx(
|
||||
"!h2-docs lg:pl-4",
|
||||
"scroll-m-[184px] lg:scroll-m-[264px]"
|
||||
)}
|
||||
id="introduction"
|
||||
>
|
||||
Medusa V2 Store API Reference
|
||||
</H1>
|
||||
<StoreContent />
|
||||
<Tags tags={data?.tags} />
|
||||
</PageTitleProvider>
|
||||
</AreaProvider>
|
||||
</BaseSpecsProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default ReferencePage
|
||||
export default StorePage
|
||||
|
||||
export function generateMetadata() {
|
||||
return {
|
||||
|
||||
@@ -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} />
|
||||
))}
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
"use server"
|
||||
|
||||
import { Area, ExpandedDocument } from "../types/openapi"
|
||||
|
||||
const URL = `${process.env.NEXT_PUBLIC_BASE_URL}${process.env.NEXT_PUBLIC_BASE_PATH}`
|
||||
|
||||
export async function getBaseSpecs(area: Area) {
|
||||
try {
|
||||
const res = await fetch(`${URL}/api/base-specs?area=${area}`, {
|
||||
next: {
|
||||
revalidate: 3000,
|
||||
tags: [area],
|
||||
},
|
||||
}).then(async (res) => res.json())
|
||||
|
||||
return res as ExpandedDocument
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,34 @@
|
||||
"use client"
|
||||
|
||||
import { ExpandedDocument, SecuritySchemeObject } from "@/types/openapi"
|
||||
import { ReactNode, createContext, useContext, useState } from "react"
|
||||
import {
|
||||
ReactNode,
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
} from "react"
|
||||
import { SidebarItem, SidebarItemSections } from "types"
|
||||
import getSectionId from "../utils/get-section-id"
|
||||
import getTagChildSidebarItems from "../utils/get-tag-child-sidebar-items"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useSidebar } from "docs-ui"
|
||||
|
||||
type BaseSpecsContextType = {
|
||||
baseSpecs: ExpandedDocument | null
|
||||
setBaseSpecs: React.Dispatch<React.SetStateAction<ExpandedDocument | null>>
|
||||
baseSpecs: ExpandedDocument | undefined
|
||||
getSecuritySchema: (securityName: string) => SecuritySchemeObject | null
|
||||
}
|
||||
|
||||
const BaseSpecsContext = createContext<BaseSpecsContextType | null>(null)
|
||||
|
||||
type BaseSpecsProviderProps = {
|
||||
initialSpecs?: ExpandedDocument | null
|
||||
baseSpecs: ExpandedDocument | undefined
|
||||
children?: ReactNode
|
||||
}
|
||||
|
||||
const BaseSpecsProvider = ({
|
||||
children,
|
||||
initialSpecs = null,
|
||||
}: BaseSpecsProviderProps) => {
|
||||
const [baseSpecs, setBaseSpecs] = useState<ExpandedDocument | null>(
|
||||
initialSpecs
|
||||
)
|
||||
const BaseSpecsProvider = ({ children, baseSpecs }: BaseSpecsProviderProps) => {
|
||||
const router = useRouter()
|
||||
const { activePath, addItems, setActivePath, resetItems } = useSidebar()
|
||||
|
||||
const getSecuritySchema = (
|
||||
securityName: string
|
||||
@@ -43,11 +49,63 @@ const BaseSpecsProvider = ({
|
||||
return null
|
||||
}
|
||||
|
||||
const handleOpen = useCallback(
|
||||
(tagPathName: string) => {
|
||||
if (location.hash !== tagPathName) {
|
||||
router.push(`#${tagPathName}`, {
|
||||
scroll: false,
|
||||
})
|
||||
}
|
||||
if (activePath !== tagPathName) {
|
||||
setActivePath(tagPathName)
|
||||
}
|
||||
},
|
||||
[activePath, router, setActivePath]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!baseSpecs) {
|
||||
return
|
||||
}
|
||||
|
||||
const itemsToAdd: SidebarItem[] = [
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
]
|
||||
|
||||
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: () => {
|
||||
handleOpen(tagPathName)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
addItems(itemsToAdd, {
|
||||
section: SidebarItemSections.DEFAULT,
|
||||
})
|
||||
|
||||
return () => {
|
||||
resetItems()
|
||||
}
|
||||
}, [baseSpecs])
|
||||
|
||||
return (
|
||||
<BaseSpecsContext.Provider
|
||||
value={{
|
||||
baseSpecs,
|
||||
setBaseSpecs,
|
||||
getSecuritySchema,
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
AnalyticsProvider,
|
||||
PageLoadingProvider,
|
||||
ScrollControllerProvider,
|
||||
SiteConfigProvider,
|
||||
} from "docs-ui"
|
||||
import BaseSpecsProvider from "./base-specs"
|
||||
import SidebarProvider from "./sidebar"
|
||||
import SearchProvider from "./search"
|
||||
import { config } from "../config"
|
||||
@@ -21,15 +18,13 @@ const Providers = ({ children }: ProvidersProps) => {
|
||||
<AnalyticsProvider writeKey={process.env.NEXT_PUBLIC_SEGMENT_API_KEY}>
|
||||
<SiteConfigProvider config={config}>
|
||||
<PageLoadingProvider>
|
||||
<BaseSpecsProvider>
|
||||
<ScrollControllerProvider scrollableSelector="#main">
|
||||
<SidebarProvider>
|
||||
<MainNavProvider>
|
||||
<SearchProvider>{children}</SearchProvider>
|
||||
</MainNavProvider>
|
||||
</SidebarProvider>
|
||||
</ScrollControllerProvider>
|
||||
</BaseSpecsProvider>
|
||||
<ScrollControllerProvider scrollableSelector="#main">
|
||||
<SidebarProvider>
|
||||
<MainNavProvider>
|
||||
<SearchProvider>{children}</SearchProvider>
|
||||
</MainNavProvider>
|
||||
</SidebarProvider>
|
||||
</ScrollControllerProvider>
|
||||
</PageLoadingProvider>
|
||||
</SiteConfigProvider>
|
||||
</AnalyticsProvider>
|
||||
|
||||
@@ -3,12 +3,9 @@
|
||||
import {
|
||||
SidebarProvider as UiSidebarProvider,
|
||||
usePageLoading,
|
||||
usePrevious,
|
||||
useScrollController,
|
||||
} from "docs-ui"
|
||||
import { config } from "../config"
|
||||
import { useCallback } from "react"
|
||||
import { usePathname } from "next/navigation"
|
||||
|
||||
type SidebarProviderProps = {
|
||||
children?: React.ReactNode
|
||||
@@ -17,13 +14,6 @@ type SidebarProviderProps = {
|
||||
const SidebarProvider = ({ children }: SidebarProviderProps) => {
|
||||
const { isLoading, setIsLoading } = usePageLoading()
|
||||
const { scrollableElement } = useScrollController()
|
||||
const pathname = usePathname()
|
||||
const prevPathName = usePrevious(pathname)
|
||||
|
||||
const resetOnCondition = useCallback(
|
||||
() => prevPathName !== undefined && pathname !== prevPathName,
|
||||
[pathname, prevPathName]
|
||||
)
|
||||
|
||||
return (
|
||||
<UiSidebarProvider
|
||||
@@ -32,7 +22,6 @@ const SidebarProvider = ({ children }: SidebarProviderProps) => {
|
||||
shouldHandleHashChange={true}
|
||||
scrollableElement={scrollableElement}
|
||||
initialItems={config.sidebar}
|
||||
resetOnCondition={resetOnCondition}
|
||||
persistState={false}
|
||||
projectName="api"
|
||||
>
|
||||
|
||||
@@ -53,6 +53,9 @@ export default async function dereference({
|
||||
canParse: /.*/,
|
||||
},
|
||||
},
|
||||
dereference: {
|
||||
circular: "ignore",
|
||||
},
|
||||
})) as unknown as Document
|
||||
|
||||
return document
|
||||
|
||||
@@ -5,8 +5,9 @@ import type { Operation, Document, ParsedPathItemObject } from "@/types/openapi"
|
||||
import readSpecDocument from "./read-spec-document"
|
||||
import getSectionId from "./get-section-id"
|
||||
import dereference from "./dereference"
|
||||
import { unstable_cache } from "next/cache"
|
||||
|
||||
export default async function getPathsOfTag(
|
||||
async function getPathsOfTag_(
|
||||
tagName: string,
|
||||
area: string
|
||||
): Promise<Document> {
|
||||
@@ -47,3 +48,13 @@ export default async function getPathsOfTag(
|
||||
paths: documents,
|
||||
})
|
||||
}
|
||||
|
||||
const getPathsOfTag = unstable_cache(
|
||||
async (tagName: string, area: string) => getPathsOfTag_(tagName, area),
|
||||
["tag-paths"],
|
||||
{
|
||||
revalidate: 3600,
|
||||
}
|
||||
)
|
||||
|
||||
export default getPathsOfTag
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { promises as fs } from "fs"
|
||||
import { parseDocument } from "yaml"
|
||||
import { SchemaObject } from "../types/openapi"
|
||||
import dereference from "./dereference"
|
||||
import { unstable_cache } from "next/cache"
|
||||
|
||||
async function getSchemaContent_(schemaPath: string, baseSchemasPath: string) {
|
||||
const schemaContent = await fs.readFile(schemaPath, "utf-8")
|
||||
const schema = parseDocument(schemaContent).toJS() as SchemaObject
|
||||
|
||||
// resolve references in schema
|
||||
const dereferencedDocument = await dereference({
|
||||
basePath: baseSchemasPath,
|
||||
schemas: [schema],
|
||||
})
|
||||
|
||||
return {
|
||||
dereferencedDocument,
|
||||
originalSchema: schema,
|
||||
}
|
||||
}
|
||||
|
||||
const getSchemaContent = unstable_cache(
|
||||
async (schemaPath: string, baseSchemasPath: string) =>
|
||||
getSchemaContent_(schemaPath, baseSchemasPath),
|
||||
["tag-schema"]
|
||||
)
|
||||
|
||||
export default getSchemaContent
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client"
|
||||
|
||||
import React from "react"
|
||||
import {
|
||||
BrowserProvider,
|
||||
|
||||
@@ -134,14 +134,22 @@ export const Sidebar = ({
|
||||
{!sidebarItems.default.length && !staticSidebarItems && (
|
||||
<Loading className="px-0" />
|
||||
)}
|
||||
{sidebarItems.default.map((item, index) => (
|
||||
<SidebarItem
|
||||
item={item}
|
||||
key={index}
|
||||
expandItems={expandItems}
|
||||
hasNextItems={index !== sidebarItems.default.length - 1}
|
||||
/>
|
||||
))}
|
||||
{sidebarItems.default.map((item, index) => {
|
||||
const itemKey =
|
||||
item.type === "separator"
|
||||
? index
|
||||
: item.type === "link"
|
||||
? `${item.path}-${index}`
|
||||
: `${item.title}-${index}`
|
||||
return (
|
||||
<SidebarItem
|
||||
item={item}
|
||||
key={itemKey}
|
||||
expandItems={expandItems}
|
||||
hasNextItems={index !== sidebarItems.default.length - 1}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</CSSTransition>
|
||||
|
||||
@@ -40,7 +40,8 @@ export type SidebarContextType = {
|
||||
setActivePath: (path: string | null) => void
|
||||
isLinkActive: (item: SidebarItem, checkChildren?: boolean) => boolean
|
||||
isChildrenActive: (item: SidebarItemCategory) => boolean
|
||||
addItems: (item: SidebarItem[], options?: ActionOptionsType) => void
|
||||
addItems: (items: SidebarItem[], options?: ActionOptionsType) => void
|
||||
removeItems: (items: SidebarItem[]) => void
|
||||
findItemInSection: (
|
||||
section: SidebarItem[],
|
||||
item: Partial<SidebarItem>,
|
||||
@@ -87,6 +88,10 @@ export type ActionType =
|
||||
type: "replace"
|
||||
replacementItems: SidebarSectionItems
|
||||
}
|
||||
| {
|
||||
type: "remove"
|
||||
items: SidebarItem[]
|
||||
}
|
||||
|
||||
type LinksMap = Map<string, SidebarItemLinkWithParent>
|
||||
|
||||
@@ -152,10 +157,46 @@ const getLinksMap = (
|
||||
return map
|
||||
}
|
||||
|
||||
export const reducer = (state: SidebarSectionItems, actionData: ActionType) => {
|
||||
export const reducer = (
|
||||
state: SidebarSectionItems,
|
||||
actionData: ActionType
|
||||
): SidebarSectionItems => {
|
||||
if (actionData.type === "replace") {
|
||||
return actionData.replacementItems
|
||||
}
|
||||
if (actionData.type === "remove") {
|
||||
return {
|
||||
...state,
|
||||
default: state.default.filter((item) => {
|
||||
if (item.type === "separator") {
|
||||
return true
|
||||
}
|
||||
|
||||
const found = actionData.items.some((itemToRemove) => {
|
||||
if (
|
||||
itemToRemove.type === "separator" ||
|
||||
itemToRemove.type !== item.type
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
itemToRemove.type === "category" ||
|
||||
itemToRemove.type === "sub-category"
|
||||
) {
|
||||
return itemToRemove.title === item.title
|
||||
}
|
||||
|
||||
return (
|
||||
itemToRemove.path === (item as SidebarItemLink).path &&
|
||||
itemToRemove.title === (item as SidebarItemLink).title
|
||||
)
|
||||
})
|
||||
|
||||
return !found
|
||||
}),
|
||||
}
|
||||
}
|
||||
const { type, options } = actionData
|
||||
let { items } = actionData
|
||||
|
||||
@@ -306,6 +347,13 @@ export const SidebarProvider = ({
|
||||
})
|
||||
}
|
||||
|
||||
const removeItems = (itemsToRemove: SidebarItem[]) => {
|
||||
dispatch({
|
||||
type: "remove",
|
||||
items: itemsToRemove,
|
||||
})
|
||||
}
|
||||
|
||||
const isLinkActive = useCallback(
|
||||
(item: SidebarItem, checkChildren = false): boolean => {
|
||||
if (item.type !== "link") {
|
||||
@@ -516,6 +564,7 @@ export const SidebarProvider = ({
|
||||
useEffect(() => {
|
||||
if (resetOnCondition?.()) {
|
||||
resetItems()
|
||||
setActivePath(null)
|
||||
}
|
||||
}, [resetOnCondition, resetItems])
|
||||
|
||||
@@ -575,6 +624,7 @@ export const SidebarProvider = ({
|
||||
items,
|
||||
currentItems,
|
||||
addItems,
|
||||
removeItems,
|
||||
activePath,
|
||||
setActivePath,
|
||||
isLinkActive: isLinkActive,
|
||||
|
||||
Reference in New Issue
Block a user