docs: create docs workspace (#5174)

* docs: migrate ui docs to docs universe

* created yarn workspace

* added eslint and tsconfig configurations

* fix eslint configurations

* fixed eslint configurations

* shared tailwind configurations

* added shared ui package

* added more shared components

* migrating more components

* made details components shared

* move InlineCode component

* moved InputText

* moved Loading component

* Moved Modal component

* moved Select components

* Moved Tooltip component

* moved Search components

* moved ColorMode provider

* Moved Notification components and providers

* used icons package

* use UI colors in api-reference

* moved Navbar component

* used Navbar and Search in UI docs

* added Feedback to UI docs

* general enhancements

* fix color mode

* added copy colors file from ui-preset

* added features and enhancements to UI docs

* move Sidebar component and provider

* general fixes and preparations for deployment

* update docusaurus version

* adjusted versions

* fix output directory

* remove rootDirectory property

* fix yarn.lock

* moved code component

* added vale for all docs MD and MDX

* fix tests

* fix vale error

* fix deployment errors

* change ignore commands

* add output directory

* fix docs test

* general fixes

* content fixes

* fix announcement script

* added changeset

* fix vale checks

* added nofilter option

* fix vale error
This commit is contained in:
Shahed Nasser
2023-09-21 20:57:15 +03:00
committed by GitHub
parent 19c5d5ba36
commit fa7c94b4cc
3209 changed files with 32188 additions and 31018 deletions

View File

@@ -0,0 +1,24 @@
"use server"
import type { OpenAPIV3 } from "openapi-types"
import Section from "../Section"
import MDXContentServer from "../MDXContent/Server"
export type DescriptionProps = {
specs: OpenAPIV3.Document
}
const Description = ({ specs }: DescriptionProps) => {
return (
<Section>
<MDXContentServer
content={specs.info.description}
scope={{
specs,
}}
/>
</Section>
)
}
export default Description

View File

@@ -0,0 +1,83 @@
"use client"
import { useState } from "react"
import { Label, TextArea, useAnalytics, useModal, ModalFooter } from "docs-ui"
const DetailedFeedback = () => {
const [improvementFeedback, setImprovementFeedback] = useState("")
const [positiveFeedback, setPositiveFeedback] = useState("")
const [additionalFeedback, setAdditionalFeedback] = useState("")
const { loaded, track } = useAnalytics()
const { closeModal } = useModal()
return (
<>
<div className="flex flex-col gap-1 overflow-auto py-1.5 px-2 lg:min-h-[400px]">
<div className="flex flex-col gap-1">
<Label>What should be improved in this API reference?</Label>
<TextArea
rows={4}
value={improvementFeedback}
onChange={(e) => setImprovementFeedback(e.target.value)}
/>
</div>
<div className="flex flex-col gap-1">
<Label>Is there a feature you like in this API reference?</Label>
<TextArea
rows={4}
value={positiveFeedback}
onChange={(e) => setPositiveFeedback(e.target.value)}
/>
</div>
<div className="flex flex-col gap-1">
<Label>Do you have any additional notes or feedback?</Label>
<TextArea
rows={4}
value={additionalFeedback}
onChange={(e) => setAdditionalFeedback(e.target.value)}
/>
</div>
</div>
<ModalFooter
actions={[
{
children: "Save",
onClick: (e) => {
if (
!loaded ||
(!improvementFeedback &&
!positiveFeedback &&
!additionalFeedback)
) {
return
}
const buttonElm = e.target as HTMLButtonElement
buttonElm.classList.add("cursor-not-allowed")
buttonElm.textContent = "Please wait"
track(
"api-ref-general-feedback",
{
feedbackData: {
improvementFeedback,
positiveFeedback,
additionalFeedback,
},
},
function () {
buttonElm.textContent = "Thank you!"
setTimeout(() => {
closeModal()
}, 1000)
}
)
},
variant: "primary",
},
]}
className="mt-1"
/>
</>
)
}
export default DetailedFeedback

View File

@@ -0,0 +1,55 @@
import DividedLayout from "@/layouts/Divided"
import { Loading } from "docs-ui"
type DividedLoadingProps = {
className?: string
}
const DividedLoading = ({ className }: DividedLoadingProps) => {
return (
<DividedLayout
mainContent={
<>
<Loading count={1} className="mb-2 !w-1/3" />
<Loading count={1} />
<div className="flex gap-1">
<Loading count={1} className="!w-1/3" />
<Loading count={1} className="!w-2/3" />
</div>
<div className="flex gap-1">
<Loading count={1} className="!w-1/3" />
<Loading count={1} className="!w-2/3" />
</div>
<Loading count={1} className="mt-2 !w-1/3" />
<Loading count={1} />
<div className="mt-2 flex gap-1">
<Loading count={1} className="!w-1/3" />
<Loading count={1} className="!w-2/3" />
</div>
<div className="flex gap-1">
<Loading count={1} className="!w-1/3" />
<Loading count={1} className="!w-2/3" />
</div>
<div className="flex gap-1">
<Loading count={1} className="!w-1/3" />
<Loading count={1} className="!w-2/3" />
</div>
<Loading count={5} barClassName="mt-1" />
</>
}
codeContent={
<>
<Loading count={1} />
<Loading count={1} className="my-2" />
<Loading count={1} barClassName="h-[200px] !rounded-sm" />
<Loading count={1} className="my-2" />
<Loading count={1} barClassName="h-3 !rounded-sm" />
<Loading count={1} barClassName="h-[230px] !rounded-sm" />
</>
}
className={className}
/>
)
}
export default DividedLoading

View File

@@ -0,0 +1,68 @@
"use client"
import { InView } from "react-intersection-observer"
import { useSidebar } from "docs-ui"
import checkElementInViewport from "../../../utils/check-element-in-viewport"
import { useEffect } from "react"
import getSectionId from "../../../utils/get-section-id"
type H2Props = {
addToSidebar?: boolean
} & React.HTMLAttributes<HTMLHeadingElement>
const H2 = ({ addToSidebar = true, children, ...props }: H2Props) => {
const { activePath, setActivePath, addItems } = useSidebar()
const handleViewChange = (
inView: boolean,
entry: IntersectionObserverEntry
) => {
if (!addToSidebar) {
return
}
const heading = entry.target
if (
(inView ||
checkElementInViewport(heading.parentElement || heading, 40)) &&
window.scrollY !== 0 &&
activePath !== heading.id
) {
// can't use next router as it doesn't support
// changing url without scrolling
history.pushState({}, "", `#${heading.id}`)
setActivePath(heading.id)
}
}
const id = getSectionId([children as string])
useEffect(() => {
if (id === (activePath || location.hash.replace("#", ""))) {
const elm = document.getElementById(id)
elm?.scrollIntoView()
}
addItems([
{
path: `${id}`,
title: children as string,
loaded: true,
},
])
}, [])
return (
<InView
as="h2"
threshold={0.4}
skip={!addToSidebar}
initialInView={false}
{...props}
onChange={handleViewChange}
id={id}
>
{children}
</InView>
)
}
export default H2

View File

@@ -0,0 +1,66 @@
import type { MDXContentClientProps } from "@/components/MDXContent/Client"
import type { MDXContentServerProps } from "@/components/MDXContent/Server"
import type { SecuritySchemeObject } from "@/types/openapi"
import getSecuritySchemaTypeName from "@/utils/get-security-schema-type-name"
import clsx from "clsx"
import { Loading } from "docs-ui"
import dynamic from "next/dynamic"
const MDXContentClient = dynamic<MDXContentClientProps>(
async () => import("../../../MDXContent/Client"),
{
loading: () => <Loading />,
}
) as React.FC<MDXContentClientProps>
const MDXContentServer = dynamic<MDXContentServerProps>(
async () => import("../../../MDXContent/Server"),
{
loading: () => <Loading />,
}
) as React.FC<MDXContentServerProps>
export type SecurityDescriptionProps = {
securitySchema: SecuritySchemeObject
isServer?: boolean
}
const SecurityDescription = ({
securitySchema,
isServer = true,
}: SecurityDescriptionProps) => {
return (
<>
<h2>{securitySchema["x-displayName"] as string}</h2>
{isServer && <MDXContentServer content={securitySchema.description} />}
{!isServer && <MDXContentClient content={securitySchema.description} />}
<p>
<strong>Security Scheme Type:</strong>{" "}
{getSecuritySchemaTypeName(securitySchema)}
</p>
{(securitySchema.type === "http" || securitySchema.type === "apiKey") && (
<p
className={clsx(
"bg-docs-bg-surface dark:bg-docs-bg-surface-dark",
"p-1"
)}
>
<strong>
{securitySchema.type === "http"
? "HTTP Authorization Scheme"
: "Cookie parameter name"}
:
</strong>{" "}
<code>
{securitySchema.type === "http"
? securitySchema.scheme
: securitySchema.name}
</code>
</p>
)}
<hr />
</>
)
}
export default SecurityDescription

View File

@@ -0,0 +1,34 @@
import dynamic from "next/dynamic"
import type { OpenAPIV3 } from "openapi-types"
import type { SecurityDescriptionProps } from "./Description"
import { Fragment } from "react"
const SecurityDescription = dynamic<SecurityDescriptionProps>(
async () => import("./Description")
) as React.FC<SecurityDescriptionProps>
type SecurityProps = {
specs?: OpenAPIV3.Document
}
const Security = ({ specs }: SecurityProps) => {
return (
<div>
{specs && (
<>
{Object.values(specs.components?.securitySchemes || {}).map(
(securitySchema, index) => (
<Fragment key={index}>
{!("$ref" in securitySchema) && (
<SecurityDescription securitySchema={securitySchema} />
)}
</Fragment>
)
)}
</>
)}
</div>
)
}
export default Security

View File

@@ -0,0 +1,22 @@
import type { MDXComponents } from "mdx/types"
import Security from "./Security"
import type { OpenAPIV3 } from "openapi-types"
import H2 from "./H2"
import { CodeMdx, Kbd, NextLink } from "docs-ui"
export type ScopeType = {
specs?: OpenAPIV3.Document
addToSidebar?: boolean
}
const getCustomComponents = (scope?: ScopeType): MDXComponents => {
return {
Security: () => <Security specs={scope?.specs} />,
code: CodeMdx,
a: NextLink,
h2: (props) => <H2 addToSidebar={scope?.addToSidebar} {...props} />,
kbd: Kbd,
}
}
export default getCustomComponents

View File

@@ -0,0 +1,48 @@
"use client"
import { useEffect, useState } from "react"
import getCustomComponents from "../../MDXComponents"
import type { ScopeType } from "../../MDXComponents"
import { MDXRemote } from "next-mdx-remote"
import type { MDXRemoteProps, MDXRemoteSerializeResult } from "next-mdx-remote"
import { serialize } from "next-mdx-remote/serialize"
export type MDXContentClientProps = {
content: any
className?: string
} & Partial<MDXRemoteProps>
const MDXContentClient = ({
content,
className,
...props
}: MDXContentClientProps) => {
const [parsedContent, setParsedContent] = useState<MDXRemoteSerializeResult>()
useEffect(() => {
void serialize(content, {
mdxOptions: {
// A workaround for an error in next-mdx-remote
// more details in this issue:
// https://github.com/hashicorp/next-mdx-remote/issues/350
development: process.env.NEXT_PUBLIC_ENV === "development",
},
scope: props.scope,
}).then((output) => {
setParsedContent(output)
})
}, [content, props.scope])
return (
<div className={className}>
{parsedContent !== undefined && (
<MDXRemote
{...parsedContent}
components={getCustomComponents((props.scope as ScopeType) || {})}
/>
)}
</div>
)
}
export default MDXContentClient

View File

@@ -0,0 +1,30 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
"use server"
import { MDXRemote } from "next-mdx-remote/rsc"
import getCustomComponents from "../../MDXComponents"
import type { ScopeType } from "../../MDXComponents"
import type { MDXRemoteProps } from "next-mdx-remote"
export type MDXContentServerProps = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
content: any
} & Partial<MDXRemoteProps>
const MDXContentServer = ({ content, ...props }: MDXContentServerProps) => {
return (
<>
{/* @ts-ignore promise error */}
<MDXRemote
source={content}
components={getCustomComponents((props.scope as ScopeType) || {})}
options={{
scope: props.scope,
}}
{...props}
/>
</>
)
}
export default MDXContentServer

View File

@@ -0,0 +1,19 @@
import { Badge, capitalize } from "docs-ui"
export type MethodLabelProps = {
method: string
className?: string
}
const MethodLabel = ({ method, className }: MethodLabelProps) => {
return (
<Badge
variant={method === "get" ? "green" : method === "post" ? "blue" : "red"}
className={className}
>
{method === "delete" ? "Del" : capitalize(method)}
</Badge>
)
}
export default MethodLabel

View File

@@ -0,0 +1,28 @@
"use client"
import { Button, useModal, usePageLoading } from "docs-ui"
import DetailedFeedback from "../../DetailedFeedback"
const FeedbackModal = () => {
const { setModalProps } = useModal()
const { isLoading } = usePageLoading()
const openModal = () => {
if (isLoading) {
return
}
setModalProps({
title: "Send your Feedback",
children: <DetailedFeedback />,
contentClassName: "lg:!min-h-auto !p-0",
})
}
return (
<Button onClick={openModal} variant="secondary">
Feedback
</Button>
)
}
export default FeedbackModal

View File

@@ -0,0 +1,50 @@
"use client"
import { Navbar as UiNavbar, usePageLoading } from "docs-ui"
import getLinkWithBasePath from "../../utils/get-link-with-base-path"
import { useSidebar } from "docs-ui"
import FeedbackModal from "./FeedbackModal"
const Navbar = () => {
const { setMobileSidebarOpen, mobileSidebarOpen } = useSidebar()
const { isLoading } = usePageLoading()
return (
<UiNavbar
logo={{
light: "/images/logo-icon.png",
dark: "/images/logo-icon-dark.png",
}}
items={[
{
href: `/`,
label: "Docs",
},
{
href: `/user-guide`,
label: "User Guide",
},
{
href: `${getLinkWithBasePath("/store")}`,
label: "Store API",
},
{
href: `${getLinkWithBasePath("/admin")}`,
label: "Admin API",
},
{
href: `/ui`,
label: "UI",
},
]}
mobileMenuButton={{
setMobileSidebarOpen,
mobileSidebarOpen,
}}
additionalActions={<FeedbackModal />}
isLoading={isLoading}
/>
)
}
export default Navbar

View File

@@ -0,0 +1,21 @@
import clsx from "clsx"
import SectionDivider from "../Divider"
import { forwardRef } from "react"
type SectionContainerProps = {
children: React.ReactNode
noTopPadding?: boolean
}
const SectionContainer = forwardRef<HTMLDivElement, SectionContainerProps>(
function SectionContainer({ children, noTopPadding = false }, ref) {
return (
<div className={clsx("relative pb-7", !noTopPadding && "pt-7")} ref={ref}>
{children}
<SectionDivider className="-left-1.5 lg:!-left-4" />
</div>
)
}
)
export default SectionContainer

View File

@@ -0,0 +1,18 @@
import clsx from "clsx"
type SectionDividerProps = {
className?: string
}
const SectionDivider = ({ className }: SectionDividerProps) => {
return (
<hr
className={clsx(
"absolute bottom-0 -left-1.5 z-0 m-0 w-screen lg:left-0",
className
)}
/>
)
}
export default SectionDivider

View File

@@ -0,0 +1,30 @@
"use client"
import clsx from "clsx"
import { useEffect, useRef } from "react"
export type SectionProps = {
addToSidebar?: boolean
} & React.AllHTMLAttributes<HTMLDivElement>
const Section = ({ children, className }: SectionProps) => {
const sectionRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if ("scrollRestoration" in history) {
// disable scroll on refresh
history.scrollRestoration = "manual"
}
}, [])
return (
<div
ref={sectionRef}
className={clsx("[&_ul]:list-disc [&_ul]:px-1", "[&_h2]:pt-7", className)}
>
{children}
</div>
)
}
export default Section

View File

@@ -0,0 +1,23 @@
type SpaceProps = {
top?: number
bottom?: number
left?: number
right?: number
}
const Space = ({ top = 0, bottom = 0, left = 0, right = 0 }: SpaceProps) => {
return (
<div
className="w-full"
style={{
height: `1px`,
marginTop: `${top ? top - 1 : top}px`,
marginBottom: `${bottom ? bottom - 1 : bottom}px`,
marginLeft: `${left}px`,
marginRight: `${right}px`,
}}
></div>
)
}
export default Space

View File

@@ -0,0 +1,31 @@
import type { Code } from "@/types/openapi"
import { CodeTabs } from "docs-ui"
import slugify from "slugify"
export type TagOperationCodeSectionRequestSamplesProps = {
codeSamples: Code[]
}
const TagOperationCodeSectionRequestSamples = ({
codeSamples,
}: TagOperationCodeSectionRequestSamplesProps) => {
return (
<div>
<h3>Request samples</h3>
<CodeTabs
tabs={codeSamples.map((codeSample) => ({
label: codeSample.label,
value: slugify(codeSample.label),
code: {
...codeSample,
collapsed: true,
className: "!mb-0",
},
}))}
className="mt-2 !mb-0"
/>
</div>
)
}
export default TagOperationCodeSectionRequestSamples

View File

@@ -0,0 +1,122 @@
import { CodeBlock } from "docs-ui"
import type { ExampleObject, ResponseObject } from "@/types/openapi"
import type { JSONSchema7 } from "json-schema"
import stringify from "json-stringify-pretty-compact"
import { sample } from "openapi-sampler"
import { useCallback, useEffect, useState } from "react"
export type TagsOperationCodeSectionResponsesSampleProps = {
response: ResponseObject
} & React.AllHTMLAttributes<HTMLDivElement>
const TagsOperationCodeSectionResponsesSample = ({
response,
className,
}: TagsOperationCodeSectionResponsesSampleProps) => {
const [examples, setExamples] = useState<ExampleObject[]>([])
const [selectedExample, setSelectedExample] = useState<
ExampleObject | undefined
>()
const initExamples = useCallback(() => {
if (!response.content) {
return []
}
const contentSchema = Object.values(response.content)[0]
const tempExamples = []
if (contentSchema.examples) {
Object.entries(contentSchema.examples).forEach(([value, example]) => {
if ("$ref" in example) {
return []
}
tempExamples.push({
title: example.summary || "",
value,
content: stringify(example.value, {
maxLength: 50,
}),
})
})
} else if (contentSchema.example) {
tempExamples.push({
title: "",
value: "",
content: stringify(contentSchema.example, {
maxLength: 50,
}),
})
} else {
const contentSample = stringify(
sample(
{
...contentSchema.schema,
} as JSONSchema7,
{
skipNonRequired: true,
}
),
{
maxLength: 50,
}
)
tempExamples.push({
title: "",
value: "",
content: contentSample,
})
}
return tempExamples
}, [response.content])
useEffect(() => {
const tempExamples = initExamples()
setExamples(tempExamples)
setSelectedExample(tempExamples[0])
}, [initExamples])
return (
<>
<div className={className}>
{response.content && (
<span>Content type: {Object.keys(response.content)[0]}</span>
)}
<>
{examples.length > 1 && (
<select
onChange={(event) =>
setSelectedExample(
examples.find((ex) => ex.value === event.target.value)
)
}
className="border-medusa-border-base dark:border-medusa-border-base-dark my-1 w-full rounded-sm border p-0.5"
>
{examples.map((example, index) => (
<option value={example.value} key={index}>
{example.title}
</option>
))}
</select>
)}
{selectedExample && (
<CodeBlock
source={selectedExample.content}
lang={getLanguageFromMedia(Object.keys(response.content)[0])}
collapsed={true}
className="mt-2 mb-0"
/>
)}
{!selectedExample && <>Empty Response</>}
</>
</div>
</>
)
}
export default TagsOperationCodeSectionResponsesSample
const getLanguageFromMedia = (media: string) => {
return media.substring(media.indexOf("/"))
}

View File

@@ -0,0 +1,38 @@
import type { Operation } from "@/types/openapi"
import dynamic from "next/dynamic"
import type { TagsOperationCodeSectionResponsesSampleProps } from "./Sample"
import { Badge } from "docs-ui"
const TagsOperationCodeSectionResponsesSample =
dynamic<TagsOperationCodeSectionResponsesSampleProps>(
async () => import("./Sample")
) as React.FC<TagsOperationCodeSectionResponsesSampleProps>
type TagsOperationCodeSectionResponsesProps = {
operation: Operation
}
const TagsOperationCodeSectionResponses = ({
operation,
}: TagsOperationCodeSectionResponsesProps) => {
const responseCodes = Object.keys(operation.responses)
const responseCode = responseCodes.find((rc) => rc === "200" || rc === "201")
const response = responseCode ? operation.responses[responseCode] : null
if (!response) {
return <></>
}
return (
<div>
<div className="mb-0.5 flex items-center gap-0.5">
<h3 className="mb-0">Response </h3>
<Badge variant="green">{responseCode}</Badge>
</div>
<TagsOperationCodeSectionResponsesSample response={response} />
</div>
)
}
export default TagsOperationCodeSectionResponses

View File

@@ -0,0 +1,58 @@
"use client"
import MethodLabel from "@/components/MethodLabel"
import type { Operation } from "@/types/openapi"
import TagsOperationCodeSectionResponses from "./Responses"
import type { TagOperationCodeSectionRequestSamplesProps } from "./RequestSamples"
import dynamic from "next/dynamic"
import clsx from "clsx"
import { CopyButton } from "docs-ui"
import { SquareTwoStack } from "@medusajs/icons"
const TagOperationCodeSectionRequestSamples =
dynamic<TagOperationCodeSectionRequestSamplesProps>(
async () => import("./RequestSamples")
) as React.FC<TagOperationCodeSectionRequestSamplesProps>
export type TagOperationCodeSectionProps = {
operation: Operation
method: string
endpointPath: string
} & React.HTMLAttributes<HTMLDivElement>
const TagOperationCodeSection = ({
operation,
method,
endpointPath,
className,
}: TagOperationCodeSectionProps) => {
return (
<div className={clsx("mt-2 flex flex-col gap-2", className)}>
<div
className={clsx(
"bg-medusa-bg-subtle border-medusa-border-base px-0.75 rounded border py-0.5",
"text-code-body flex w-full justify-between gap-1",
"dark:bg-medusa-bg-subtle-dark dark:border-medusa-border-base-dark"
)}
>
<div className={clsx("flex w-[calc(100%-36px)] gap-1")}>
<MethodLabel method={method} className="h-fit" />
<code className="text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark break-words break-all">
{endpointPath}
</code>
</div>
<CopyButton text={endpointPath} tooltipClassName="font-base">
<SquareTwoStack className="text-medusa-fg-muted dark:text-medusa-fg-muted-dark" />
</CopyButton>
</div>
{operation["x-codeSamples"] && (
<TagOperationCodeSectionRequestSamples
codeSamples={operation["x-codeSamples"]}
/>
)}
<TagsOperationCodeSectionResponses operation={operation} />
</div>
)
}
export default TagOperationCodeSection

View File

@@ -0,0 +1,71 @@
import type { Parameter, SchemaObject } from "@/types/openapi"
import TagOperationParameters from "../../Parameters"
export type TagsOperationDescriptionSectionParametersProps = {
parameters: Parameter[]
}
const TagsOperationDescriptionSectionParameters = ({
parameters,
}: TagsOperationDescriptionSectionParametersProps) => {
const pathParameters: SchemaObject = {
type: "object",
required: [],
properties: {},
}
const queryParameters: SchemaObject = {
type: "object",
required: [],
properties: {},
}
parameters.forEach((parameter) => {
const parameterObject = {
...parameter.schema,
parameterName: parameter.name,
description: parameter.description,
example: parameter.example,
examples: parameter.examples,
}
if (parameter.in === "path") {
if (parameter.required) {
pathParameters.required?.push(parameter.name)
}
pathParameters.properties[parameter.name] = parameterObject
} else if (parameter.in === "query") {
if (parameter.required) {
queryParameters.required?.push(parameter.name)
}
queryParameters.properties[parameter.name] = parameterObject
}
})
return (
<>
{Object.values(pathParameters.properties).length > 0 && (
<>
<h3 className="border-medusa-border-base dark:border-medusa-border-base-dark border-b py-1.5">
Path Parameters
</h3>
<TagOperationParameters
schemaObject={pathParameters}
topLevel={true}
/>
</>
)}
{Object.values(queryParameters.properties).length > 0 && (
<>
<h3 className="border-medusa-border-base dark:border-medusa-border-base-dark border-b py-1.5">
Query Parameters
</h3>
<TagOperationParameters
schemaObject={queryParameters}
topLevel={true}
/>
</>
)}
</>
)
}
export default TagsOperationDescriptionSectionParameters

View File

@@ -0,0 +1,31 @@
import type { RequestObject } from "@/types/openapi"
import TagOperationParameters from "../../Parameters"
import { DetailsSummary } from "docs-ui"
export type TagsOperationDescriptionSectionRequestProps = {
requestBody: RequestObject
}
const TagsOperationDescriptionSectionRequest = ({
requestBody,
}: TagsOperationDescriptionSectionRequestProps) => {
return (
<>
<DetailsSummary
title="Request Body"
subtitle={Object.keys(requestBody.content)[0]}
expandable={false}
className="border-t-0"
titleClassName="text-h3"
/>
<TagOperationParameters
schemaObject={
requestBody.content[Object.keys(requestBody.content)[0]].schema
}
topLevel={true}
/>
</>
)
}
export default TagsOperationDescriptionSectionRequest

View File

@@ -0,0 +1,100 @@
import type { ResponsesObject } from "@/types/openapi"
import clsx from "clsx"
import TagOperationParameters from "../../Parameters"
import { Fragment } from "react"
import { Badge, Details, DetailsSummary } from "docs-ui"
export type TagsOperationDescriptionSectionResponsesProps = {
responses: ResponsesObject
}
const TagsOperationDescriptionSectionResponses = ({
responses,
}: TagsOperationDescriptionSectionResponsesProps) => {
return (
<>
<h3 className="my-1.5">Responses</h3>
<div
className={clsx("[&>details:not(:first-of-type)>summary]:border-t-0")}
>
{Object.entries(responses).map(([code, response], index) => {
return (
<Fragment key={index}>
{response.content && (
<>
{(code === "200" || code === "201") && (
<>
<DetailsSummary
title={`${code} ${response.description}`}
subtitle={Object.keys(response.content)[0]}
badge={<Badge variant="green">Success</Badge>}
expandable={false}
className={clsx(
index !== 0 && "border-t-0",
index === 0 && "border-b-0"
)}
/>
<TagOperationParameters
schemaObject={
response.content[Object.keys(response.content)[0]]
.schema
}
topLevel={true}
/>
</>
)}
{code !== "200" && code !== "201" && (
<Details
summaryElm={
<DetailsSummary
title={`${code} ${response.description}`}
subtitle={Object.keys(response.content)[0]}
badge={<Badge variant="red">Error</Badge>}
open={index === 0}
/>
}
openInitial={index === 0}
className={clsx(index > 1 && "border-t-0")}
>
<TagOperationParameters
schemaObject={
response.content[Object.keys(response.content)[0]]
.schema
}
topLevel={true}
/>
</Details>
)}
</>
)}
{!response.content && (
<DetailsSummary
title={`${code} ${response.description}`}
subtitle={"Empty response"}
badge={
<Badge
variant={
code === "200" || code === "201" ? "green" : "red"
}
>
{code === "200" || code === "201" ? "Success" : "Error"}
</Badge>
}
expandable={false}
className={clsx(
index !== 0 && "border-t-0",
index === 0 &&
Object.entries(responses).length > 1 &&
"border-b-0"
)}
/>
)}
</Fragment>
)
})}
</div>
</>
)
}
export default TagsOperationDescriptionSectionResponses

View File

@@ -0,0 +1,36 @@
import { useBaseSpecs } from "@/providers/base-specs"
import type { OpenAPIV3 } from "openapi-types"
import { Card } from "docs-ui"
export type TagsOperationDescriptionSectionSecurityProps = {
security: OpenAPIV3.SecurityRequirementObject[]
}
const TagsOperationDescriptionSectionSecurity = ({
security,
}: TagsOperationDescriptionSectionSecurityProps) => {
const { getSecuritySchema } = useBaseSpecs()
const getDescription = () => {
let str = ""
security.forEach((item) => {
if (str.length) {
str += " or "
}
str += getSecuritySchema(Object.keys(item)[0])?.["x-displayName"]
})
return str
}
return (
<div className="my-2">
<Card
title="Authorization"
text={getDescription()}
href="#authentication"
/>
</div>
)
}
export default TagsOperationDescriptionSectionSecurity

View File

@@ -0,0 +1,107 @@
"use client"
import type { Operation } from "@/types/openapi"
import type { TagsOperationDescriptionSectionSecurityProps } from "./Security"
import type { TagsOperationDescriptionSectionRequestProps } from "./RequestBody"
import type { TagsOperationDescriptionSectionResponsesProps } from "./Responses"
import dynamic from "next/dynamic"
import TagsOperationDescriptionSectionParameters from "./Parameters"
import MDXContentClient from "@/components/MDXContent/Client"
import type { TagsOperationFeatureFlagNoticeProps } from "../FeatureFlagNotice"
import { useArea } from "../../../../providers/area"
import { Feedback, Badge, NextLink } from "docs-ui"
import { usePathname } from "next/navigation"
import formatReportLink from "../../../../utils/format-report-link"
const TagsOperationDescriptionSectionSecurity =
dynamic<TagsOperationDescriptionSectionSecurityProps>(
async () => import("./Security")
) as React.FC<TagsOperationDescriptionSectionSecurityProps>
const TagsOperationDescriptionSectionRequest =
dynamic<TagsOperationDescriptionSectionRequestProps>(
async () => import("./RequestBody")
) as React.FC<TagsOperationDescriptionSectionRequestProps>
const TagsOperationDescriptionSectionResponses =
dynamic<TagsOperationDescriptionSectionResponsesProps>(
async () => import("./Responses")
) as React.FC<TagsOperationDescriptionSectionResponsesProps>
const TagsOperationFeatureFlagNotice =
dynamic<TagsOperationFeatureFlagNoticeProps>(
async () => import("../FeatureFlagNotice")
) as React.FC<TagsOperationFeatureFlagNoticeProps>
type TagsOperationDescriptionSectionProps = {
operation: Operation
}
const TagsOperationDescriptionSection = ({
operation,
}: TagsOperationDescriptionSectionProps) => {
const { area } = useArea()
const pathname = usePathname()
return (
<>
<h2>
{operation.summary}
{operation.deprecated && (
<Badge variant="orange" className="ml-0.5">
deprecated
</Badge>
)}
{operation["x-featureFlag"] && (
<TagsOperationFeatureFlagNotice
featureFlag={operation["x-featureFlag"]}
tooltipTextClassName="font-normal text-medusa-fg-subtle dark:text-medusa-fg-subtle-dark"
badgeClassName="ml-0.5"
/>
)}
</h2>
<div className="my-1">
<MDXContentClient content={operation.description} />
</div>
<Feedback
event="survey_api-ref"
extraData={{
area,
section: operation.summary,
}}
pathName={pathname}
reportLink={formatReportLink(area, operation.summary)}
className="!my-2"
vertical={true}
question="Did this endpoint run successfully?"
/>
{operation.externalDocs && (
<>
Related guide:{" "}
<NextLink href={operation.externalDocs.url} target="_blank">
{operation.externalDocs.description || "Read More"}
</NextLink>
</>
)}
{operation.security && (
<TagsOperationDescriptionSectionSecurity
security={operation.security}
/>
)}
{operation.parameters && (
<TagsOperationDescriptionSectionParameters
parameters={operation.parameters}
/>
)}
{operation.requestBody && (
<TagsOperationDescriptionSectionRequest
requestBody={operation.requestBody}
/>
)}
<TagsOperationDescriptionSectionResponses
responses={operation.responses}
/>
</>
)
}
export default TagsOperationDescriptionSection

View File

@@ -0,0 +1,39 @@
import { Badge, NextLink, Tooltip } from "docs-ui"
export type TagsOperationFeatureFlagNoticeProps = {
featureFlag: string
type?: "endpoint" | "parameter"
tooltipTextClassName?: string
badgeClassName?: string
}
const TagsOperationFeatureFlagNotice = ({
featureFlag,
type = "endpoint",
tooltipTextClassName,
badgeClassName,
}: TagsOperationFeatureFlagNoticeProps) => {
return (
<Tooltip
tooltipChildren={
<span className={tooltipTextClassName}>
To use this {type}, make sure to
<br />
<NextLink
href="https://docs.medusajs.com/development/feature-flags/toggle"
target="__blank"
>
enable its feature flag: <code>{featureFlag}</code>
</NextLink>
</span>
}
clickable
>
<Badge variant="green" className={badgeClassName}>
feature flag
</Badge>
</Tooltip>
)
}
export default TagsOperationFeatureFlagNotice

View File

@@ -0,0 +1,152 @@
import MDXContentClient from "@/components/MDXContent/Client"
import type { SchemaObject } from "@/types/openapi"
import clsx from "clsx"
import dynamic from "next/dynamic"
import { Fragment } from "react"
import { NextLink, type InlineCodeProps, capitalize } from "docs-ui"
const InlineCode = dynamic<InlineCodeProps>(
async () => (await import("docs-ui")).InlineCode
) as React.FC<InlineCodeProps>
type TagOperationParametersDescriptionProps = {
schema: SchemaObject
}
const TagOperationParametersDescription = ({
schema,
}: TagOperationParametersDescriptionProps) => {
let typeDescription: React.ReactNode = <></>
switch (true) {
case schema.type === "object":
typeDescription = (
<>
{schema.type} {schema.title ? `(${schema.title})` : ""}
{schema.nullable ? ` or null` : ""}
</>
)
break
case schema.type === "array":
typeDescription = (
<>
{schema.type === "array" && formatArrayDescription(schema.items)}
{schema.nullable ? ` or null` : ""}
</>
)
break
case schema.anyOf !== undefined:
case schema.allOf !== undefined:
typeDescription = (
<>
{formatUnionDescription(schema.allOf)}
{schema.nullable ? ` or null` : ""}
</>
)
break
case schema.oneOf !== undefined:
typeDescription = (
<>
{schema.oneOf?.map((item, index) => (
<Fragment key={index}>
{index !== 0 && <> or </>}
{item.type !== "array" && <>{item.title || item.type}</>}
{item.type === "array" && (
<>array{item.items.type ? ` of ${item.items.type}s` : ""}</>
)}
</Fragment>
))}
{schema.nullable ? ` or null` : ""}
</>
)
break
default:
typeDescription = (
<>
{schema.type}
{schema.nullable ? ` or null` : ""}
{schema.format ? ` <${schema.format}>` : ""}
</>
)
}
return (
<div className={clsx("w-2/3 break-words pb-0.5")}>
{typeDescription}
{schema.default !== undefined && (
<>
<br />
<span>
Default:{" "}
<InlineCode className="break-words">
{JSON.stringify(schema.default)}
</InlineCode>
</span>
</>
)}
{schema.enum && (
<>
<br />
<span>
Enum:{" "}
{schema.enum.map((value, index) => (
<Fragment key={index}>
{index !== 0 && <>, </>}
<InlineCode key={index}>{JSON.stringify(value)}</InlineCode>
</Fragment>
))}
</span>
</>
)}
{schema.example !== undefined && (
<>
<br />
<span>
Example:{" "}
<InlineCode className="break-words">
{JSON.stringify(schema.example)}
</InlineCode>
</span>
</>
)}
{schema.description && (
<>
<br />
<MDXContentClient
content={capitalize(schema.description)}
className={clsx("!mb-0 [&>*]:!mb-0")}
scope={{
addToSidebar: false,
}}
/>
</>
)}
{schema.externalDocs && (
<>
Related guide:{" "}
<NextLink href={schema.externalDocs.url} target="_blank">
{schema.externalDocs.description || "Read More"}
</NextLink>
</>
)}
</div>
)
}
export default TagOperationParametersDescription
function formatArrayDescription(schema?: SchemaObject) {
if (!schema) {
return "Array"
}
const type =
schema.type === "object"
? `objects ${schema.title ? `(${schema.title})` : ""}`
: `${schema.type || "object"}s`
return `Array of ${type}`
}
function formatUnionDescription(arr?: SchemaObject[]) {
const types = [...new Set(arr?.map((type) => type.type || "object"))]
return <>{types.join(" or ")}</>
}

View File

@@ -0,0 +1,74 @@
import type { SchemaObject } from "@/types/openapi"
import dynamic from "next/dynamic"
import type { TooltipProps } from "docs-ui"
import type { TagsOperationFeatureFlagNoticeProps } from "../../FeatureFlagNotice"
import { Badge, NextLink } from "docs-ui"
const Tooltip = dynamic<TooltipProps>(
async () => (await import("docs-ui")).Tooltip
) as React.FC<TooltipProps>
const TagsOperationFeatureFlagNotice =
dynamic<TagsOperationFeatureFlagNoticeProps>(
async () => import("../../FeatureFlagNotice")
) as React.FC<TagsOperationFeatureFlagNoticeProps>
export type TagOperationParametersNameProps = {
name: string
isRequired?: boolean
schema: SchemaObject
}
const TagOperationParametersName = ({
name,
isRequired,
schema,
}: TagOperationParametersNameProps) => {
return (
<span className="w-1/3 break-words pr-0.5">
<span className="font-monospace">{name}</span>
{schema.deprecated && (
<Badge variant="orange" className="ml-1">
deprecated
</Badge>
)}
{schema["x-expandable"] && (
<>
<br />
<Tooltip
tooltipChildren={
<>
If this request accepts an <code>expand</code> parameter,
<br /> this field can be{" "}
<NextLink href="#expanding-fields">expanded</NextLink> into an
object.
</>
}
clickable
>
<Badge variant="blue">expandable</Badge>
</Tooltip>
</>
)}
{schema["x-featureFlag"] && (
<>
<br />
<TagsOperationFeatureFlagNotice
featureFlag={schema["x-featureFlag"]}
type="parameter"
/>
</>
)}
{isRequired && (
<>
<br />
<span className="text-medusa-tag-red-text dark:text-medusa-tag-red-text-dark text-compact-x-small">
required
</span>
</>
)}
</span>
)
}
export default TagOperationParametersName

View File

@@ -0,0 +1,24 @@
import clsx from "clsx"
export type TagsOperationParametersNestedProps =
React.HTMLAttributes<HTMLDivElement>
const TagsOperationParametersNested = ({
children,
...props
}: TagsOperationParametersNestedProps) => {
return (
<div
{...props}
className={clsx(
props.className,
"bg-docs-bg-surface dark:bg-docs-bg-surface-dark px-1 pt-1",
"border-medusa-border-base dark:border-medusa-border-base-dark my-1 rounded-sm border"
)}
>
{children}
</div>
)
}
export default TagsOperationParametersNested

View File

@@ -0,0 +1,44 @@
import type { SchemaObject } from "@/types/openapi"
import clsx from "clsx"
import type { TagOperationParametersProps } from ".."
import dynamic from "next/dynamic"
import { Loading } from "docs-ui"
const TagOperationParameters = dynamic<TagOperationParametersProps>(
async () => import(".."),
{
loading: () => <Loading />,
}
) as React.FC<TagOperationParametersProps>
type TagsOperationParametersSectionProps = {
header?: string
contentType?: string
schema: SchemaObject
}
const TagsOperationParametersSection = ({
header,
contentType,
schema,
}: TagsOperationParametersSectionProps) => {
return (
<>
{header && (
<h3
className={clsx(!contentType && "my-2", contentType && "mt-2 mb-0")}
>
{header}
</h3>
)}
{contentType && (
<span className={clsx("mb-2 inline-block")}>
Content type: {contentType}
</span>
)}
<TagOperationParameters schemaObject={schema} topLevel={true} />
</>
)
}
export default TagsOperationParametersSection

View File

@@ -0,0 +1,79 @@
import type { SchemaObject } from "@/types/openapi"
import dynamic from "next/dynamic"
import type { TagOperationParametersDefaultProps } from "../Default"
import type { TagOperationParametersProps } from "../.."
import TagsOperationParametersNested from "../../Nested"
import { Details, Loading } from "docs-ui"
const TagOperationParametersDefault =
dynamic<TagOperationParametersDefaultProps>(
async () => import("../Default"),
{
loading: () => <Loading />,
}
) as React.FC<TagOperationParametersDefaultProps>
const TagOperationParameters = dynamic<TagOperationParametersProps>(
async () => import("../.."),
{
loading: () => <Loading />,
}
) as React.FC<TagOperationParametersProps>
export type TagOperationParametersArrayProps = {
name: string
schema: SchemaObject
isRequired?: boolean
}
const TagOperationParametersArray = ({
name,
schema,
isRequired,
}: TagOperationParametersArrayProps) => {
if (schema.type !== "array") {
return <></>
}
if (
!schema.items ||
(schema.items.type !== "object" &&
schema.items.type !== "array" &&
schema.items.type !== undefined) ||
(schema.items.type === "object" &&
!schema.items.properties &&
!schema.items.allOf &&
!schema.items.anyOf &&
!schema.items.oneOf)
) {
return (
<TagOperationParametersDefault
name={name}
schema={schema}
isRequired={isRequired}
/>
)
}
return (
<Details
summaryElm={
<summary className="cursor-pointer">
<TagOperationParametersDefault
name={name}
schema={schema}
isRequired={isRequired}
expandable={true}
/>
</summary>
}
className="!border-y-0"
>
<TagsOperationParametersNested>
<TagOperationParameters schemaObject={schema.items} topLevel={true} />
</TagsOperationParametersNested>
</Details>
)
}
export default TagOperationParametersArray

View File

@@ -0,0 +1,42 @@
import type { SchemaObject } from "@/types/openapi"
import TagOperationParametersDescription from "../../Description"
import clsx from "clsx"
import TagOperationParametersName from "../../Name"
export type TagOperationParametersDefaultProps = {
name?: string
schema: SchemaObject
isRequired?: boolean
className?: string
expandable?: boolean
}
const TagOperationParametersDefault = ({
name,
schema,
isRequired,
className,
expandable = false,
}: TagOperationParametersDefaultProps) => {
return (
<div
className={clsx(
"my-0.5 inline-flex justify-between",
expandable && "w-[calc(100%-16px)]",
!expandable && "w-full pl-1",
className
)}
>
{name && (
<TagOperationParametersName
name={name}
isRequired={isRequired}
schema={schema}
/>
)}
<TagOperationParametersDescription schema={schema} />
</div>
)
}
export default TagOperationParametersDefault

View File

@@ -0,0 +1,135 @@
import type { SchemaObject } from "@/types/openapi"
import TagOperationParametersDefault from "../Default"
import dynamic from "next/dynamic"
import type { TagOperationParametersProps } from "../.."
import type { TagsOperationParametersNestedProps } from "../../Nested"
import checkRequired from "@/utils/check-required"
import { Loading, type DetailsProps } from "docs-ui"
const TagOperationParameters = dynamic<TagOperationParametersProps>(
async () => import("../.."),
{
loading: () => <Loading />,
}
) as React.FC<TagOperationParametersProps>
const TagsOperationParametersNested =
dynamic<TagsOperationParametersNestedProps>(
async () => import("../../Nested"),
{
loading: () => <Loading />,
}
) as React.FC<TagsOperationParametersNestedProps>
const Details = dynamic<DetailsProps>(
async () => (await import("docs-ui")).Details,
{
loading: () => <Loading />,
}
) as React.FC<DetailsProps>
export type TagOperationParametersObjectProps = {
name?: string
schema: SchemaObject
isRequired?: boolean
topLevel?: boolean
}
const TagOperationParametersObject = ({
name,
schema,
isRequired,
topLevel = false,
}: TagOperationParametersObjectProps) => {
if (
(schema.type !== "object" && schema.type !== undefined) ||
(!schema.properties && !name)
) {
return <></>
}
const getPropertyDescriptionElm = (expandable = false) => {
const content = (
<TagOperationParametersDefault
name={name}
schema={schema}
isRequired={isRequired}
expandable={expandable}
/>
)
return expandable ? (
<summary className="cursor-pointer">{content}</summary>
) : (
<>{content}</>
)
}
const getPropertyParameterElms = (isNested = false) => {
// sort properties to show required fields first
const sortedProperties = Object.keys(schema.properties).sort(
(property1, property2) => {
schema.properties[property1].isRequired = checkRequired(
schema,
property1
)
schema.properties[property2].isRequired = checkRequired(
schema,
property2
)
return schema.properties[property1].isRequired &&
schema.properties[property2].isRequired
? 0
: schema.properties[property1].isRequired
? -1
: 1
}
)
const content = (
<>
{sortedProperties.map((property, index) => (
<TagOperationParameters
schemaObject={{
...schema.properties[property],
parameterName: property,
}}
key={index}
isRequired={
schema.properties[property].isRequired ||
checkRequired(schema, property)
}
/>
))}
</>
)
return (
<>
{isNested && (
<TagsOperationParametersNested>
{content}
</TagsOperationParametersNested>
)}
{!isNested && <div>{content}</div>}
</>
)
}
if (!schema.properties) {
return getPropertyDescriptionElm()
}
if (topLevel) {
return getPropertyParameterElms()
}
return (
<Details
summaryElm={getPropertyDescriptionElm(true)}
className="!border-y-0"
>
{getPropertyParameterElms(true)}
</Details>
)
}
export default TagOperationParametersObject

View File

@@ -0,0 +1,132 @@
import type { SchemaObject } from "@/types/openapi"
import clsx from "clsx"
import dynamic from "next/dynamic"
import { useState } from "react"
import type { TagOperationParametersDefaultProps } from "../Default"
import type { TagsOperationParametersNestedProps } from "../../Nested"
import type { TagOperationParametersProps } from "../.."
import { Details, Loading } from "docs-ui"
const TagOperationParameters = dynamic<TagOperationParametersProps>(
async () => import("../.."),
{
loading: () => <Loading />,
}
) as React.FC<TagOperationParametersProps>
const TagOperationParametersDefault =
dynamic<TagOperationParametersDefaultProps>(
async () => import("../Default"),
{
loading: () => <Loading />,
}
) as React.FC<TagOperationParametersDefaultProps>
const TagsOperationParametersNested =
dynamic<TagsOperationParametersNestedProps>(
async () => import("../../Nested"),
{
loading: () => <Loading />,
}
) as React.FC<TagsOperationParametersNestedProps>
export type TagOperationParamatersOneOfProps = {
schema: SchemaObject
isRequired?: boolean
isNested?: boolean
}
const TagOperationParamatersOneOf = ({
schema,
isRequired = false,
isNested = false,
}: TagOperationParamatersOneOfProps) => {
const [activeTab, setActiveTab] = useState<number>(0)
const getName = (item: SchemaObject): string => {
if (item.title) {
return item.title
}
if (item.anyOf || item.allOf) {
// return the name of any of the items
const name = item.anyOf
? item.anyOf.find((i) => i.title !== undefined)?.title
: item.allOf?.find((i) => i.title !== undefined)?.title
if (name) {
return name
}
}
return item.type || ""
}
const getContent = () => {
return (
<>
<div className={clsx("flex items-center gap-1 pl-1")}>
<span className="inline-block">One of</span>
<ul className="mb-0 flex list-none gap-1">
{schema.oneOf?.map((item, index) => (
<li
key={index}
className={clsx(
"rounded-xs cursor-pointer p-0.5",
"border border-solid",
activeTab === index && [
"bg-medusa-bg-subtle border-medusa-border-strong",
"dark:bg-medusa-bg-subtle-dark dark:border-medusa-border-strong-dark",
],
activeTab !== index && [
"bg-medusa-bg-base border-medusa-border-base",
"dark:bg-medusa-bg-base-dark dark:border-medusa-border-base-dark",
]
)}
onClick={() => setActiveTab(index)}
>
{getName(item)}
</li>
))}
</ul>
</div>
{schema.oneOf && (
<>
<TagOperationParameters
schemaObject={schema.oneOf[activeTab]}
topLevel={true}
/>
</>
)}
</>
)
}
return (
<>
{isNested && (
<Details
summaryElm={
<summary className="cursor-pointer">
<TagOperationParametersDefault
schema={schema}
name={schema.parameterName || schema.title || ""}
isRequired={isRequired}
expandable={true}
/>
</summary>
}
className="!border-y-0"
>
<TagsOperationParametersNested>
{getContent()}
</TagsOperationParametersNested>
</Details>
)}
{!isNested && getContent()}
</>
)
}
export default TagOperationParamatersOneOf

View File

@@ -0,0 +1,65 @@
import type { SchemaObject } from "@/types/openapi"
import dynamic from "next/dynamic"
import type { TagOperationParametersDefaultProps } from "../Default"
import { TagOperationParametersObjectProps } from "../Object"
import { Loading } from "docs-ui"
const TagOperationParametersObject = dynamic<TagOperationParametersObjectProps>(
async () => import("../Object"),
{
loading: () => <Loading />,
}
) as React.FC<TagOperationParametersObjectProps>
const TagOperationParametersDefault =
dynamic<TagOperationParametersDefaultProps>(
async () => import("../Default"),
{
loading: () => <Loading />,
}
) as React.FC<TagOperationParametersDefaultProps>
export type TagOperationParametersUnionProps = {
name: string
schema: SchemaObject
isRequired?: boolean
topLevel?: boolean
}
const TagOperationParametersUnion = ({
name,
schema,
isRequired,
topLevel,
}: TagOperationParametersUnionProps) => {
const objectSchema = schema.anyOf
? schema.anyOf.find((item) => item.type === "object" && item.properties)
: schema.allOf?.find((item) => item.type === "object" && item.properties)
if (!objectSchema) {
return (
<TagOperationParametersDefault
schema={schema}
name={name}
isRequired={isRequired}
/>
)
}
if (!objectSchema.description) {
objectSchema.description = schema.anyOf
? schema.anyOf.find((item) => item.description !== undefined)?.description
: schema.allOf?.find((item) => item.description !== undefined)
?.description
}
return (
<TagOperationParametersObject
schema={objectSchema}
name={name}
topLevel={topLevel}
/>
)
}
export default TagOperationParametersUnion

View File

@@ -0,0 +1,116 @@
import type { SchemaObject } from "@/types/openapi"
import dynamic from "next/dynamic"
import type { TagOperationParametersObjectProps } from "./Types/Object"
import type { TagOperationParametersDefaultProps } from "./Types/Default"
import type { TagOperationParametersArrayProps } from "./Types/Array"
import type { TagOperationParametersUnionProps } from "./Types/Union"
import type { TagOperationParamatersOneOfProps } from "./Types/OneOf"
import checkRequired from "@/utils/check-required"
import { Loading } from "docs-ui"
const TagOperationParametersObject = dynamic<TagOperationParametersObjectProps>(
async () => import("./Types/Object"),
{
loading: () => <Loading />,
}
) as React.FC<TagOperationParametersObjectProps>
const TagOperationParametersDefault =
dynamic<TagOperationParametersDefaultProps>(
async () => import("./Types/Default"),
{
loading: () => <Loading />,
}
) as React.FC<TagOperationParametersDefaultProps>
const TagOperationParametersArray = dynamic<TagOperationParametersArrayProps>(
async () => import("./Types/Array"),
{
loading: () => <Loading />,
}
) as React.FC<TagOperationParametersArrayProps>
const TagOperationParametersUnion = dynamic<TagOperationParametersUnionProps>(
async () => import("./Types/Union"),
{
loading: () => <Loading />,
}
) as React.FC<TagOperationParametersUnionProps>
const TagOperationParamatersOneOf = dynamic<TagOperationParamatersOneOfProps>(
async () => import("./Types/OneOf"),
{
loading: () => <Loading />,
}
) as React.FC<TagOperationParamatersOneOfProps>
export type TagOperationParametersProps = {
schemaObject: SchemaObject
topLevel?: boolean
className?: string
isRequired?: boolean
}
const TagOperationParameters = ({
schemaObject,
className,
topLevel = false,
isRequired: originalIsRequired = false,
}: TagOperationParametersProps) => {
const isRequired =
originalIsRequired || checkRequired(schemaObject, schemaObject.title)
const propertyName = schemaObject.parameterName || schemaObject.title || ""
const getElement = () => {
if (schemaObject.anyOf || schemaObject.allOf) {
return (
<TagOperationParametersUnion
schema={schemaObject}
name={propertyName}
isRequired={isRequired}
topLevel={topLevel}
/>
)
}
if (schemaObject.oneOf) {
return (
<TagOperationParamatersOneOf
schema={schemaObject}
isNested={!topLevel}
/>
)
}
if (schemaObject.type === "array") {
return (
<TagOperationParametersArray
name={propertyName}
schema={schemaObject}
isRequired={isRequired}
/>
)
}
if (schemaObject.type === "object" || !schemaObject.type) {
return (
<TagOperationParametersObject
name={propertyName}
schema={schemaObject}
topLevel={topLevel}
isRequired={isRequired}
/>
)
}
return (
<TagOperationParametersDefault
schema={schemaObject}
name={propertyName}
isRequired={isRequired}
/>
)
return <></>
}
return <div className={className}>{getElement()}</div>
}
export default TagOperationParameters

View File

@@ -0,0 +1,126 @@
"use client"
import type { Operation } from "@/types/openapi"
import clsx from "clsx"
import type { OpenAPIV3 } from "openapi-types"
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 { useSidebar } from "docs-ui"
import type { TagOperationCodeSectionProps } from "./CodeSection"
import TagsOperationDescriptionSection from "./DescriptionSection"
import DividedLayout from "@/layouts/Divided"
import { useLoading } from "@/providers/loading"
import SectionDivider from "../../Section/Divider"
const TagOperationCodeSection = dynamic<TagOperationCodeSectionProps>(
async () => import("./CodeSection")
) as React.FC<TagOperationCodeSectionProps>
export type TagOperationProps = {
operation: Operation
method?: string
tag: OpenAPIV3.TagObject
endpointPath: string
className?: string
}
const TagOperation = ({
operation,
method,
endpointPath,
className,
}: TagOperationProps) => {
const { setActivePath } = useSidebar()
const [show, setShow] = useState(false)
const path = useMemo(
() => getSectionId([...(operation.tags || []), operation.operationId]),
[operation]
)
const nodeRef = useRef<Element | null>(null)
const { loading, removeLoading } = useLoading()
const { ref } = useInView({
threshold: 0.3,
rootMargin: `112px 0px 112px 0px`,
onChange: (changedInView) => {
if (changedInView) {
if (!show) {
if (loading) {
removeLoading()
}
setShow(true)
}
// can't use next router as it doesn't support
// changing url without scrolling
history.replaceState({}, "", `#${path}`)
setActivePath(path)
}
},
})
// Use `useCallback` so we don't recreate the function on each render
const setRefs = useCallback(
(node: Element | null) => {
// Ref's from useRef needs to have the node assigned to `current`
nodeRef.current = node
// Callback refs, like the one from `useInView`, is a function that takes the node as an argument
ref(node)
},
[ref]
)
useEffect(() => {
const enableShow = () => {
setShow(true)
}
if (nodeRef && nodeRef.current) {
removeLoading()
const currentHash = location.hash.replace("#", "")
if (currentHash === path) {
setTimeout(() => {
nodeRef.current?.scrollIntoView()
enableShow()
}, 100)
} else if (currentHash.split("_")[0] === path.split("_")[0]) {
enableShow()
}
}
}, [nodeRef, path])
return (
<div
className={clsx("relative min-h-screen w-full pb-7", className)}
id={path}
ref={setRefs}
>
<div
className={clsx(
"flex w-full justify-between gap-1 opacity-0",
!show && "invisible",
show && "animate-fadeIn"
)}
style={{
animationFillMode: "forwards",
}}
>
<DividedLayout
mainContent={
<TagsOperationDescriptionSection operation={operation} />
}
codeContent={
<TagOperationCodeSection
method={method || ""}
operation={operation}
endpointPath={endpointPath}
/>
}
/>
</div>
<SectionDivider />
</div>
)
}
export default TagOperation

View File

@@ -0,0 +1,101 @@
"use client"
import getSectionId from "@/utils/get-section-id"
import fetcher from "@/utils/swr-fetcher"
import type { OpenAPIV3 } from "openapi-types"
import useSWR from "swr"
import type { Operation, PathsObject } from "@/types/openapi"
import { SidebarItemSections, useSidebar, type SidebarItemType } from "docs-ui"
import { Fragment, useEffect, useMemo } from "react"
import dynamic from "next/dynamic"
import type { TagOperationProps } from "../Operation"
import { useArea } from "@/providers/area"
import getLinkWithBasePath from "@/utils/get-link-with-base-path"
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"
const TagOperation = dynamic<TagOperationProps>(
async () => import("../Operation")
) as React.FC<TagOperationProps>
export type TagPathsProps = {
tag: OpenAPIV3.TagObject
} & React.HTMLAttributes<HTMLDivElement>
const TagPaths = ({ tag, className }: TagPathsProps) => {
const tagSlugName = useMemo(() => getSectionId([tag.name]), [tag])
const { area } = useArea()
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 endpoint
let paths: PathsObject =
baseSpecs?.expandedTags &&
Object.hasOwn(baseSpecs.expandedTags, tagSlugName)
? baseSpecs.expandedTags[tagSlugName]
: {}
const { data } = useSWR<{
paths: PathsObject
}>(
!Object.keys(paths).length
? getLinkWithBasePath(`/tag?tagName=${tagSlugName}&area=${area}`)
: null,
fetcher,
{
errorRetryInterval: 2000,
}
)
paths = data?.paths || paths
useEffect(() => {
if (paths) {
const parentItem = findItemInSection(
items[SidebarItemSections.BOTTOM],
{ path: tagSlugName },
false
)
if (!parentItem?.children?.length) {
const items: SidebarItemType[] = getTagChildSidebarItems(paths)
addItems(items, {
section: SidebarItemSections.BOTTOM,
parent: {
path: tagSlugName,
changeLoaded: true,
},
})
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [paths])
return (
<div className={clsx("relative", className)}>
{loading && <DividedLoading className="mt-7" />}
{Object.entries(paths).map(([endpointPath, operations], pathIndex) => (
<Fragment key={pathIndex}>
{Object.entries(operations).map(
([method, operation], operationIndex) => (
<TagOperation
method={method}
operation={operation as Operation}
tag={tag}
key={`${pathIndex}-${operationIndex}`}
endpointPath={endpointPath}
className={clsx("pt-7")}
/>
)
)}
</Fragment>
))}
</div>
)
}
export default TagPaths

View File

@@ -0,0 +1,126 @@
"use client"
import getSectionId from "@/utils/get-section-id"
import type { OpenAPIV3 } from "openapi-types"
import { useInView } from "react-intersection-observer"
import { useEffect, useMemo, useState } from "react"
import { 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"
import { useArea } from "../../../providers/area"
import SectionDivider from "../../Section/Divider"
import clsx from "clsx"
import { Feedback, Loading, NextLink } from "docs-ui"
import { usePathname } from "next/navigation"
import formatReportLink from "../../../utils/format-report-link"
export type TagSectionProps = {
tag: OpenAPIV3.TagObject
} & React.HTMLAttributes<HTMLDivElement>
const Section = dynamic<SectionProps>(
async () => import("../../Section")
) as React.FC<SectionProps>
const MDXContentClient = dynamic<MDXContentClientProps>(
async () => import("../../MDXContent/Client"),
{
loading: () => <Loading />,
}
) as React.FC<MDXContentClientProps>
const TagSection = ({ tag }: TagSectionProps) => {
const { activePath, setActivePath } = useSidebar()
const [loadPaths, setLoadPaths] = useState(false)
const slugTagName = useMemo(() => getSectionId([tag.name]), [tag])
const { area } = useArea()
const pathname = usePathname()
const { ref } = useInView({
threshold: 0.5,
rootMargin: `112px 0px 112px 0px`,
onChange: (inView) => {
if (inView && !loadPaths) {
setLoadPaths(true)
}
if (inView) {
// ensure that the hash link doesn't change if it links to an inner path
const currentHashArr = location.hash.replace("#", "").split("_")
if (currentHashArr.length < 2 || currentHashArr[0] !== slugTagName) {
// can't use next router as it doesn't support
// changing url without scrolling
history.replaceState({}, "", `#${slugTagName}`)
setActivePath(slugTagName)
}
}
},
})
useEffect(() => {
if (activePath && activePath.includes(slugTagName)) {
const tagName = activePath.split("_")
if (tagName.length === 1 && tagName[0] === slugTagName) {
const elm = document.getElementById(tagName[0]) as Element
elm?.scrollIntoView()
} else if (tagName.length > 1 && tagName[0] === slugTagName) {
setLoadPaths(true)
}
}
}, [slugTagName, activePath])
return (
<div
className={clsx("min-h-screen", !loadPaths && "relative")}
id={slugTagName}
>
<DividedLayout
ref={ref}
mainContent={
<SectionContainer>
<h2>{tag.name}</h2>
{tag.description && (
<Section>
<MDXContentClient
content={tag.description}
scope={{
addToSidebar: false,
}}
/>
</Section>
)}
{tag.externalDocs && (
<>
Related guide:{" "}
<NextLink href={tag.externalDocs.url} target="_blank">
{tag.externalDocs.description || "Read More"}
</NextLink>
</>
)}
<Feedback
event="survey_api-ref"
extraData={{
area,
section: tag.name,
}}
pathName={pathname}
reportLink={formatReportLink(area, tag.name)}
/>
</SectionContainer>
}
codeContent={<></>}
/>
{loadPaths && (
<LoadingProvider initialLoading={true}>
<TagPaths tag={tag} />
</LoadingProvider>
)}
{!loadPaths && <SectionDivider />}
</div>
)
}
export default TagSection

View File

@@ -0,0 +1,97 @@
"use client"
import type { OpenAPIV3 } from "openapi-types"
import { useEffect, useState } from "react"
import useSWR from "swr"
import fetcher from "@/utils/swr-fetcher"
import { useBaseSpecs } from "@/providers/base-specs"
import dynamic from "next/dynamic"
import type { TagSectionProps } from "./Section"
import { useArea } from "@/providers/area"
import getLinkWithBasePath from "@/utils/get-link-with-base-path"
import { SidebarItemSections, 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"
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]
: ""
}
const Tags = () => {
const [tags, setTags] = useState<OpenAPIV3.TagObject[]>([])
const [loadData, setLoadData] = useState<boolean>(false)
const [expand, setExpand] = useState<string>("")
const { baseSpecs, setBaseSpecs } = useBaseSpecs()
const { addItems } = useSidebar()
const { area } = useArea()
const { data } = useSWR<ExpandedDocument>(
loadData && !baseSpecs
? getLinkWithBasePath(`/base-specs?area=${area}&expand=${expand}`)
: null,
fetcher,
{
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) {
addItems(
baseSpecs.tags?.map((tag) => {
const tagPathName = getSectionId([tag.name.toLowerCase()])
const childItems =
baseSpecs.expandedTags &&
Object.hasOwn(baseSpecs.expandedTags, tagPathName)
? getTagChildSidebarItems(baseSpecs.expandedTags[tagPathName])
: []
return {
path: tagPathName,
title: tag.name,
children: childItems,
loaded: childItems.length > 0,
}
}) || [],
{
section: SidebarItemSections.BOTTOM,
}
)
}
}, [baseSpecs, addItems])
return (
<>
{tags.map((tag, index) => (
<TagSection tag={tag} key={index} />
))}
</>
)
}
export default Tags