docs: improve API testing feature (#7311)

This commit is contained in:
Shahed Nasser
2024-05-14 10:12:53 +03:00
committed by GitHub
parent 5b26f5f2cf
commit 70c4ffff8b
10 changed files with 368 additions and 68 deletions

View File

@@ -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>
)
}

View File

@@ -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}`
}
/>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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)

View File

@@ -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]
)

View File

@@ -1,5 +1,6 @@
export * from "./AiAssistant"
export * from "./AiAssistant/CommandIcon"
export * from "./ApiRunner"
export * from "./Badge"
export * from "./Bannerv2"
export * from "./Bordered"

View File

@@ -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"

View 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
}

View File

@@ -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,