docs: improve API testing feature (#7311)
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
"use client"
|
||||
|
||||
import React, { useEffect, useState } from "react"
|
||||
import { ApiRunnerParamInput, ApiRunnerParamInputProps } from "../Default"
|
||||
import clsx from "clsx"
|
||||
import setObjValue from "@/utils/set-obj-value"
|
||||
import { Button } from "../../../.."
|
||||
import { Minus, Plus } from "@medusajs/icons"
|
||||
|
||||
export const ApiRunnerParamArrayInput = ({
|
||||
paramName,
|
||||
paramValue,
|
||||
objPath,
|
||||
setValue,
|
||||
}: ApiRunnerParamInputProps) => {
|
||||
const [itemsValue, setItemsValue] = useState<typeof paramValue>(paramValue)
|
||||
|
||||
useEffect(() => {
|
||||
setValue((prev: unknown) => {
|
||||
return typeof prev === "object"
|
||||
? setObjValue({
|
||||
obj: { ...prev },
|
||||
value: itemsValue,
|
||||
path: `${objPath.length ? `${objPath}.` : ""}${paramName}`,
|
||||
})
|
||||
: itemsValue
|
||||
})
|
||||
}, [itemsValue])
|
||||
|
||||
if (!Array.isArray(paramValue)) {
|
||||
return (
|
||||
<ApiRunnerParamInput
|
||||
paramName={paramName}
|
||||
paramValue={paramValue}
|
||||
objPath={objPath}
|
||||
setValue={setValue}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<fieldset
|
||||
className={clsx(
|
||||
"border border-medusa-border-strong rounded",
|
||||
"p-docs_0.5"
|
||||
)}
|
||||
>
|
||||
<legend className="px-docs_0.5">
|
||||
<code>{paramName}</code> Array Items
|
||||
</legend>
|
||||
{(itemsValue as unknown[]).map((value, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={clsx(
|
||||
index > 0 &&
|
||||
"flex gap-docs_0.5 items-center justify-center mt-docs_0.5"
|
||||
)}
|
||||
>
|
||||
<ApiRunnerParamInput
|
||||
paramName={`[${index}]`}
|
||||
paramValue={value}
|
||||
objPath={""}
|
||||
setValue={setItemsValue}
|
||||
/>
|
||||
{index > 0 && (
|
||||
<Button
|
||||
buttonType="icon"
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setItemsValue((prev: unknown[]) => prev.splice(index, 1))
|
||||
}}
|
||||
className="mt-0.5"
|
||||
>
|
||||
<Minus />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
buttonType="icon"
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setItemsValue((prev: unknown[]) => [
|
||||
...prev,
|
||||
Array.isArray(prev[0])
|
||||
? [...prev[0]]
|
||||
: typeof prev[0] === "object"
|
||||
? Object.assign({}, prev[0])
|
||||
: prev[0],
|
||||
])
|
||||
}}
|
||||
className="mt-0.5"
|
||||
>
|
||||
<Plus />
|
||||
</Button>
|
||||
</fieldset>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import React from "react"
|
||||
import { InputText } from "../../../.."
|
||||
import setObjValue from "../../../../utils/set-obj-value"
|
||||
import { ApiRunnerParamObjectInput } from "../Object"
|
||||
import { ApiRunnerParamArrayInput } from "../Array"
|
||||
|
||||
export type ApiRunnerParamInputProps = {
|
||||
paramName: string
|
||||
paramValue: unknown
|
||||
objPath: string
|
||||
setValue: React.Dispatch<React.SetStateAction<unknown>>
|
||||
}
|
||||
|
||||
export const ApiRunnerParamInput = ({
|
||||
paramName,
|
||||
paramValue,
|
||||
objPath,
|
||||
setValue,
|
||||
}: ApiRunnerParamInputProps) => {
|
||||
if (Array.isArray(paramValue)) {
|
||||
return (
|
||||
<ApiRunnerParamArrayInput
|
||||
paramName={paramName}
|
||||
paramValue={paramValue}
|
||||
objPath={objPath}
|
||||
setValue={setValue}
|
||||
/>
|
||||
)
|
||||
}
|
||||
if (typeof paramValue === "object") {
|
||||
return (
|
||||
<ApiRunnerParamObjectInput
|
||||
paramName={paramName}
|
||||
paramValue={paramValue}
|
||||
objPath={objPath}
|
||||
setValue={setValue}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<InputText
|
||||
name={paramName}
|
||||
onChange={(e) => {
|
||||
setValue((prev: unknown) => {
|
||||
if (Array.isArray(prev)) {
|
||||
// try to get index from param name
|
||||
const splitPath = objPath.split(".")
|
||||
// if param is in an object in the array, the index is
|
||||
// the last item of the `objPath`. Otherwise, it's in the param name
|
||||
const index = (
|
||||
objPath.length > 0 ? splitPath[splitPath.length - 1] : paramName
|
||||
)
|
||||
.replace("[", "")
|
||||
.replace("]", "")
|
||||
const intIndex = parseInt(index)
|
||||
|
||||
// if we can't get the index from the param name or obj path
|
||||
// just insert the value to the end of the array.
|
||||
if (Number.isNaN(intIndex)) {
|
||||
return [...prev, e.target.value]
|
||||
}
|
||||
|
||||
// if the param is within an object, the value to be set
|
||||
// is the updated value of the object. Otherwise, it's just the
|
||||
// value of the item.
|
||||
const transformedValue =
|
||||
prev.length > 0 && typeof prev[0] === "object"
|
||||
? setObjValue({
|
||||
obj: { ...prev[intIndex] },
|
||||
value: e.target.value,
|
||||
path: paramName,
|
||||
})
|
||||
: e.target.value
|
||||
|
||||
return [
|
||||
...prev.slice(0, intIndex),
|
||||
transformedValue,
|
||||
...prev.slice(intIndex + 1),
|
||||
]
|
||||
}
|
||||
|
||||
return typeof prev === "object"
|
||||
? setObjValue({
|
||||
obj: { ...prev },
|
||||
value: e.target.value,
|
||||
path: `${objPath.length ? `${objPath}.` : ""}${paramName}`,
|
||||
})
|
||||
: e.target.value
|
||||
})
|
||||
}}
|
||||
placeholder={paramName}
|
||||
value={
|
||||
typeof paramValue === "string"
|
||||
? (paramValue as string)
|
||||
: typeof paramValue === "number"
|
||||
? (paramValue as number)
|
||||
: `${paramValue}`
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import React from "react"
|
||||
import { ApiRunnerParamInput, ApiRunnerParamInputProps } from "../Default"
|
||||
import clsx from "clsx"
|
||||
|
||||
export const ApiRunnerParamObjectInput = ({
|
||||
paramName,
|
||||
paramValue,
|
||||
objPath,
|
||||
...props
|
||||
}: ApiRunnerParamInputProps) => {
|
||||
if (typeof paramValue !== "object") {
|
||||
return (
|
||||
<ApiRunnerParamInput
|
||||
paramName={paramName}
|
||||
paramValue={paramValue}
|
||||
objPath={objPath}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<fieldset
|
||||
className={clsx(
|
||||
"border border-medusa-border-strong rounded",
|
||||
"p-docs_0.5"
|
||||
)}
|
||||
>
|
||||
<legend className="px-docs_0.5">
|
||||
<code>{paramName}</code> Properties
|
||||
</legend>
|
||||
{Object.entries(paramValue as Record<string, unknown>).map(
|
||||
([key, value], index) => (
|
||||
<ApiRunnerParamInput
|
||||
paramName={key}
|
||||
paramValue={value}
|
||||
objPath={`${objPath.length ? `${objPath}.` : ""}${paramName}`}
|
||||
key={index}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</fieldset>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import React from "react"
|
||||
import { ApiRunnerParamInput } from "./Default"
|
||||
|
||||
export type ApiRunnerParamInputsProps = {
|
||||
data: Record<string, unknown>
|
||||
title: string
|
||||
baseObjPath: string
|
||||
setValue: React.Dispatch<React.SetStateAction<unknown>>
|
||||
}
|
||||
|
||||
export const ApiRunnerParamInputs = ({
|
||||
data,
|
||||
title,
|
||||
baseObjPath,
|
||||
setValue,
|
||||
}: ApiRunnerParamInputsProps) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-docs_0.5">
|
||||
<span className="text-compact-medium-plus text-medusa-fg-base">
|
||||
{title}
|
||||
</span>
|
||||
<div className="flex gap-docs_0.5">
|
||||
{Object.keys(data).map((pathParam, index) => (
|
||||
<ApiRunnerParamInput
|
||||
paramName={pathParam}
|
||||
paramValue={data[pathParam]}
|
||||
objPath={baseObjPath}
|
||||
setValue={setValue}
|
||||
key={index}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -2,11 +2,12 @@
|
||||
|
||||
import React from "react"
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import { useRequestRunner } from "../../../hooks"
|
||||
import { CodeBlock } from ".."
|
||||
import { Card } from "../../Card"
|
||||
import { Button, InputText } from "../../.."
|
||||
import { ApiMethod, ApiDataOptions, ApiTestingOptions } from "types"
|
||||
import { useRequestRunner } from "../../hooks"
|
||||
import { CodeBlock } from "../CodeBlock"
|
||||
import { Card } from "../Card"
|
||||
import { Button } from "../.."
|
||||
import { ApiMethod, ApiTestingOptions } from "types"
|
||||
import { ApiRunnerParamInputs } from "./ParamInputs"
|
||||
|
||||
type ApiRunnerProps = {
|
||||
apiMethod: ApiMethod
|
||||
@@ -72,69 +73,46 @@ export const ApiRunner = ({
|
||||
}
|
||||
}, [isRunning, ran])
|
||||
|
||||
const getParamsElms = ({
|
||||
data,
|
||||
title,
|
||||
nameInApiOptions,
|
||||
}: {
|
||||
data: ApiDataOptions
|
||||
title: string
|
||||
nameInApiOptions: "pathData" | "bodyData" | "queryData"
|
||||
}) => (
|
||||
<div className="flex flex-col gap-docs_0.5">
|
||||
<span className="text-compact-medium-plus text-medusa-fg-base">
|
||||
{title}
|
||||
</span>
|
||||
<div className="flex gap-docs_0.5">
|
||||
{Object.keys(data).map((pathParam, index) => (
|
||||
<InputText
|
||||
name={pathParam}
|
||||
onChange={(e) =>
|
||||
setApiTestingOptions((prev) => ({
|
||||
...prev,
|
||||
[nameInApiOptions]: {
|
||||
...prev[nameInApiOptions],
|
||||
[pathParam]: e.target.value,
|
||||
},
|
||||
}))
|
||||
}
|
||||
key={index}
|
||||
placeholder={pathParam}
|
||||
value={
|
||||
typeof data[pathParam] === "string"
|
||||
? (data[pathParam] as string)
|
||||
: typeof data[pathParam] === "number"
|
||||
? (data[pathParam] as number)
|
||||
: `${data[pathParam]}`
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
{manualTestTrigger && (
|
||||
<Card className="font-base mb-docs_1" contentClassName="gap-docs_0.5">
|
||||
{apiTestingOptions.pathData &&
|
||||
getParamsElms({
|
||||
data: apiTestingOptions.pathData,
|
||||
title: "Path Parameters",
|
||||
nameInApiOptions: "pathData",
|
||||
})}
|
||||
{apiTestingOptions.bodyData &&
|
||||
getParamsElms({
|
||||
data: apiTestingOptions.bodyData,
|
||||
title: "Request Body Parameters",
|
||||
nameInApiOptions: "bodyData",
|
||||
})}
|
||||
{apiTestingOptions.queryData &&
|
||||
getParamsElms({
|
||||
data: apiTestingOptions.queryData,
|
||||
title: "Request Query Parameters",
|
||||
nameInApiOptions: "queryData",
|
||||
})}
|
||||
{apiTestingOptions.pathData && (
|
||||
<ApiRunnerParamInputs
|
||||
data={apiTestingOptions.pathData}
|
||||
title="Path Parameters"
|
||||
baseObjPath="pathData"
|
||||
setValue={
|
||||
setApiTestingOptions as React.Dispatch<
|
||||
React.SetStateAction<unknown>
|
||||
>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{apiTestingOptions.bodyData && (
|
||||
<ApiRunnerParamInputs
|
||||
data={apiTestingOptions.bodyData}
|
||||
title="Request Body Parameters"
|
||||
baseObjPath="bodyData"
|
||||
setValue={
|
||||
setApiTestingOptions as React.Dispatch<
|
||||
React.SetStateAction<unknown>
|
||||
>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{apiTestingOptions.queryData && (
|
||||
<ApiRunnerParamInputs
|
||||
data={apiTestingOptions.queryData}
|
||||
title="Request Query Parameters"
|
||||
baseObjPath="queryData"
|
||||
setValue={
|
||||
setApiTestingOptions as React.Dispatch<
|
||||
React.SetStateAction<unknown>
|
||||
>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => {
|
||||
setIsRunning(true)
|
||||
@@ -3,14 +3,13 @@
|
||||
import React, { useMemo, useState } from "react"
|
||||
import clsx from "clsx"
|
||||
import { HighlightProps, Highlight, themes } from "prism-react-renderer"
|
||||
import { CopyButton, Tooltip, Link } from "@/components"
|
||||
import { ApiRunner, CopyButton, Tooltip, Link } from "@/components"
|
||||
import { useColorMode } from "@/providers"
|
||||
import { ExclamationCircle, PlaySolid, SquareTwoStack } from "@medusajs/icons"
|
||||
import { CodeBlockHeader, CodeBlockHeaderMeta } from "./Header"
|
||||
import { CodeBlockLine } from "./Line"
|
||||
import { ApiAuthType, ApiDataOptions, ApiMethod } from "types"
|
||||
import { CSSTransition } from "react-transition-group"
|
||||
import { ApiRunner } from "./ApiRunner"
|
||||
import { GITHUB_ISSUES_PREFIX } from "../.."
|
||||
|
||||
export type Highlight = {
|
||||
@@ -69,7 +68,10 @@ export const CodeBlock = ({
|
||||
const { colorMode } = useColorMode()
|
||||
const [showTesting, setShowTesting] = useState(false)
|
||||
const canShowApiTesting = useMemo(
|
||||
() => apiTesting && rest.testApiMethod && rest.testApiUrl,
|
||||
() =>
|
||||
apiTesting !== undefined &&
|
||||
rest.testApiMethod !== undefined &&
|
||||
rest.testApiUrl !== undefined,
|
||||
[apiTesting, rest]
|
||||
)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from "./AiAssistant"
|
||||
export * from "./AiAssistant/CommandIcon"
|
||||
export * from "./ApiRunner"
|
||||
export * from "./Badge"
|
||||
export * from "./Bannerv2"
|
||||
export * from "./Bordered"
|
||||
|
||||
@@ -11,5 +11,6 @@ export * from "./get-scrolled-top"
|
||||
export * from "./is-elm-window"
|
||||
export * from "./is-in-view"
|
||||
export * from "./learning-paths"
|
||||
export * from "./set-obj-value"
|
||||
export * from "./sidebar-attach-href-common-options"
|
||||
export * from "./swr-fetcher"
|
||||
|
||||
38
www/packages/docs-ui/src/utils/set-obj-value.ts
Normal file
38
www/packages/docs-ui/src/utils/set-obj-value.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
type Params = {
|
||||
obj: Record<string, unknown>
|
||||
value: unknown
|
||||
path: string
|
||||
}
|
||||
|
||||
export default function setObjValue({
|
||||
obj,
|
||||
value,
|
||||
path,
|
||||
}: Params): Record<string, unknown> {
|
||||
// split path by delimiter
|
||||
const splitPath = path.split(".")
|
||||
const targetKey = splitPath[0]
|
||||
|
||||
if (!Object.hasOwn(obj, targetKey)) {
|
||||
obj[targetKey] = {}
|
||||
}
|
||||
|
||||
if (splitPath.length === 1) {
|
||||
obj[targetKey] = value
|
||||
return obj
|
||||
}
|
||||
|
||||
if (typeof obj[targetKey] !== "object") {
|
||||
throw new Error(
|
||||
`value of ${targetKey} is not an object, so can't set nested value`
|
||||
)
|
||||
}
|
||||
|
||||
setObjValue({
|
||||
obj: obj[targetKey] as Record<string, unknown>,
|
||||
value,
|
||||
path: splitPath.slice(1).join("."),
|
||||
})
|
||||
|
||||
return obj
|
||||
}
|
||||
@@ -11,7 +11,7 @@
|
||||
"tsBuildInfoFile": "./dist/.tsbuildinfo-client",
|
||||
"noEmit": false,
|
||||
"jsx": "react",
|
||||
"lib": ["dom", "ES2015", "es2021"],
|
||||
"lib": ["dom", "ES2015", "es2022"],
|
||||
"module": "ESNext",
|
||||
"target": "es6",
|
||||
"declaration": true,
|
||||
|
||||
Reference in New Issue
Block a user