diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/Body/FileArrayFormBodyItem/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/Body/FileArrayFormBodyItem/index.tsx new file mode 100644 index 000000000..26641074b --- /dev/null +++ b/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/Body/FileArrayFormBodyItem/index.tsx @@ -0,0 +1,77 @@ +/* ============================================================================ + * Copyright (c) Palo Alto Networks + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * ========================================================================== */ + +import React, { useState } from "react"; +import FormFileUpload from "@theme/ApiExplorer/FormFileUpload"; +import { useTypedDispatch } from "@theme/ApiItem/hooks"; +import { FileContent, setFileArrayFormBody } from "../slice"; + +interface FileArrayFormItemProps { + id: string; + description: string | undefined; +} + +export default function FileArrayFormBodyItem({ + id, + description, +}: FileArrayFormItemProps): React.JSX.Element { + const dispatch = useTypedDispatch(); + const [fileItems, setFileItems] = useState< + Map + >(new Map([[0, undefined]])); + + const handleFileChange = (index: number, file: any) => { + const newItems = new Map(fileItems); + + if (file === undefined) { + newItems.delete(index); + + setFileItems(newItems); + + dispatch( + setFileArrayFormBody({ + key: id, + value: [...newItems.values()].filter((item) => item !== undefined), + }) + ); + return; + } + + let maxIndex = 0; + + newItems.keys().forEach((item) => { + maxIndex = item > maxIndex ? item : maxIndex; + }); + newItems.set(index, { + src: `/path/to/${file.name}`, + content: file, + }); + newItems.set(index + 1, undefined); + + setFileItems(newItems); + + dispatch( + setFileArrayFormBody({ + key: id, + value: [...newItems.values()].filter((item) => item !== undefined), + }) + ); + }; + + return ( +
+ {[...fileItems.keys()].map((index) => ( +
+ handleFileChange(index, file)} + /> +
+ ))} +
+ ); +} diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/Body/FormBodyItem/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/Body/FormBodyItem/index.tsx new file mode 100644 index 000000000..6536edc48 --- /dev/null +++ b/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/Body/FormBodyItem/index.tsx @@ -0,0 +1,120 @@ +/* ============================================================================ + * Copyright (c) Palo Alto Networks + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * ========================================================================== */ + +import React from "react"; +import FormFileUpload from "@theme/ApiExplorer/FormFileUpload"; +import FormSelect from "@theme/ApiExplorer/FormSelect"; +import FormTextInput from "@theme/ApiExplorer/FormTextInput"; +import LiveApp from "@theme/ApiExplorer/LiveEditor"; +import { useTypedDispatch } from "@theme/ApiItem/hooks"; +import { SchemaObject } from "docusaurus-plugin-openapi-docs/src/openapi/types"; +import { clearFormBodyKey, setFileFormBody, setStringFormBody } from "../slice"; +import FileArrayFormBodyItem from "../FileArrayFormBodyItem"; + +interface FormBodyItemProps { + schemaObject: SchemaObject; + id: string; + schema: SchemaObject; +} + +export default function FormBodyItem({ + schemaObject, + id, + schema, +}: FormBodyItemProps): React.JSX.Element { + const dispatch = useTypedDispatch(); + + if ( + schemaObject.type === "array" && + schemaObject.items?.format === "binary" + ) { + return ( + + ); + } + + if (schemaObject.format === "binary") { + return ( + { + if (file === undefined) { + dispatch(clearFormBodyKey(id)); + return; + } + dispatch( + setFileFormBody({ + key: id, + value: { + src: `/path/to/${file.name}`, + content: file, + }, + }) + ); + }} + /> + ); + } + + if ( + schemaObject.type === "object" && + (schemaObject.example || schemaObject.examples) + ) { + const objectExample = JSON.stringify( + schemaObject.example ?? schemaObject.examples[0], + null, + 2 + ); + + return ( + + dispatch(setStringFormBody({ key: id, value: code })) + } + > + {objectExample} + + ); + } + + if ( + schemaObject.enum && + schemaObject.enum.every((value) => typeof value === "string") + ) { + return ( + ) => { + const val = e.target.value; + if (val === "---") { + dispatch(clearFormBodyKey(id)); + } else { + dispatch( + setStringFormBody({ + key: id, + value: val, + }) + ); + } + }} + /> + ); + } + // TODO: support all the other types. + return ( + ) => { + dispatch(setStringFormBody({ key: id, value: e.target.value })); + }} + /> + ); +} diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/Body/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/Body/index.tsx index f4d9c897d..f04d47c7e 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/Body/index.tsx +++ b/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/Body/index.tsx @@ -12,8 +12,6 @@ import { translate } from "@docusaurus/Translate"; import json2xml from "@theme/ApiExplorer/Body/json2xml"; import FormFileUpload from "@theme/ApiExplorer/FormFileUpload"; import FormItem from "@theme/ApiExplorer/FormItem"; -import FormSelect from "@theme/ApiExplorer/FormSelect"; -import FormTextInput from "@theme/ApiExplorer/FormTextInput"; import LiveApp from "@theme/ApiExplorer/LiveEditor"; import { useTypedDispatch, useTypedSelector } from "@theme/ApiItem/hooks"; import Markdown from "@theme/Markdown"; @@ -23,13 +21,8 @@ import { OPENAPI_BODY, OPENAPI_REQUEST } from "@theme/translationIds"; import { RequestBodyObject } from "docusaurus-plugin-openapi-docs/src/openapi/types"; import format from "xml-formatter"; -import { - clearFormBodyKey, - clearRawBody, - setFileFormBody, - setFileRawBody, - setStringFormBody, -} from "./slice"; +import { clearRawBody, setFileRawBody, setStringRawBody } from "./slice"; +import FormBodyItem from "./FormBodyItem"; export interface Props { jsonRequestBodyExample: string; @@ -128,96 +121,23 @@ function Body({ ) { return ( -
- {Object.entries(schema.properties ?? {}).map(([key, val]: any) => { - if (val.format === "binary") { - return ( - - { - if (file === undefined) { - dispatch(clearFormBodyKey(key)); - return; - } - dispatch( - setFileFormBody({ - key: key, - value: { - src: `/path/to/${file.name}`, - content: file, - }, - }) - ); - }} - /> - - ); - } - - if (val.enum) { - return ( - - ) => { - const val = e.target.value; - if (val === "---") { - dispatch(clearFormBodyKey(key)); - } else { - dispatch( - setStringFormBody({ - key: key, - value: val, - }) - ); - } - }} - /> - - ); - } - // TODO: support all the other types. - return ( - - ) => { - dispatch( - setStringFormBody({ key: key, value: e.target.value }) - ); - }} - /> - - ); - })} -
+ {Object.entries(schema.properties ?? {}).map(([key, val]: any) => { + return ( + + + + ); + })}
); } @@ -315,7 +235,11 @@ function Body({ value="Example (from schema)" default > - + dispatch(setStringRawBody(code))} + language={language} + required={required} + > {defaultBody} @@ -324,7 +248,7 @@ function Body({ {example.summary && {example.summary}} {exampleBody && ( dispatch(setStringRawBody(code))} language={language} required={required} > @@ -350,7 +274,11 @@ function Body({ value="Example (from schema)" default > - + dispatch(setStringRawBody(code))} + language={language} + required={required} + > {defaultBody} @@ -364,7 +292,10 @@ function Body({ > {example.summary && {example.summary}} {example.body && ( - + dispatch(setStringRawBody(code))} + language={language} + > {example.body} )} @@ -378,7 +309,11 @@ function Body({ return ( - + dispatch(setStringRawBody(code))} + language={language} + required={required} + > {defaultBody} diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/Body/slice.ts b/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/Body/slice.ts index 157875e01..e577da0d4 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/Body/slice.ts +++ b/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/Body/slice.ts @@ -15,12 +15,24 @@ export interface FileContent { }; } +export interface FileArrayContent { + type: "file[]"; + value: { + src: string; + content: Blob; + }[]; +} + export interface StringContent { type: "string"; value?: string; } -export type Content = FileContent | StringContent | undefined; +export type Content = + | FileContent + | FileArrayContent + | StringContent + | undefined; export interface FormBody { type: "form"; @@ -118,6 +130,32 @@ export const slice = createSlice({ }; return state; }, + setFileArrayFormBody: ( + state, + action: PayloadAction<{ + key: string; + value: FileArrayContent["value"]; + }> + ) => { + if (state?.type !== "form") { + return { + type: "form", + content: { + [action.payload.key]: { + type: "file[]", + value: action.payload.value, + }, + }, + }; + } + + state.content[action.payload.key] = { + type: "file[]", + value: action.payload.value, + }; + + return state; + }, }, }); @@ -128,6 +166,7 @@ export const { clearFormBodyKey, setStringFormBody, setFileFormBody, + setFileArrayFormBody, } = slice.actions; export default slice.reducer; diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/LiveEditor/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/LiveEditor/index.tsx index 2f2400796..b9d05ca08 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/LiveEditor/index.tsx +++ b/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/LiveEditor/index.tsx @@ -11,7 +11,6 @@ import { usePrismTheme } from "@docusaurus/theme-common"; import { translate } from "@docusaurus/Translate"; import useIsBrowser from "@docusaurus/useIsBrowser"; import { ErrorMessage } from "@hookform/error-message"; -import { setStringRawBody } from "@theme/ApiExplorer/Body/slice"; import { OPENAPI_FORM } from "@theme/translationIds"; import clsx from "clsx"; import { Controller, useFormContext } from "react-hook-form"; @@ -56,8 +55,8 @@ function App({ const [code, setCode] = React.useState(children.replace(/\n$/, "")); useEffect(() => { - action(setStringRawBody(code)); - }, [action, code]); + action(code); + }, [code]); const { control, diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/buildPostmanRequest.ts b/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/buildPostmanRequest.ts index 78e57744c..b52d38d97 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/buildPostmanRequest.ts +++ b/packages/docusaurus-theme-openapi-docs/src/theme/ApiExplorer/buildPostmanRequest.ts @@ -313,7 +313,11 @@ function setBody(clonedPostman: sdk.Request, body: Body) { switch (clonedPostman.body.mode) { case "raw": { // check file even though it should already be set from above - if (body.type !== "raw" || body.content?.type === "file") { + if ( + body.type !== "raw" || + body.content?.type === "file" || + body.content?.type === "file[]" + ) { clonedPostman.body = undefined; return; } @@ -328,15 +332,23 @@ function setBody(clonedPostman: sdk.Request, body: Body) { clonedPostman.body.raw = `${body.content?.value}`; return; } - const params = Object.entries(body.content) + const params: sdk.FormParam[] = []; + Object.entries(body.content) .filter((entry): entry is [string, NonNullable] => !!entry[1]) - .map(([key, content]) => { + .forEach(([key, content]) => { if (content.type === "file") { - return new sdk.FormParam({ key: key, ...content }); + params.push(new sdk.FormParam({ key: key, ...content })); + } else if (content.type === "file[]") { + content.value.forEach((file) => + params.push(new sdk.FormParam({ key, value: file })) + ); + } else { + params.push(new sdk.FormParam({ key: key, value: content.value })); } - return new sdk.FormParam({ key: key, value: content.value }); }); - clonedPostman.body.formdata?.assimilate(params, false); + params.forEach((param) => { + clonedPostman.body?.formdata?.add(param); + }); return; } case "urlencoded": { @@ -350,7 +362,11 @@ function setBody(clonedPostman: sdk.Request, body: Body) { const params = Object.entries(body.content) .filter((entry): entry is [string, NonNullable] => !!entry[1]) .map(([key, content]) => { - if (content.type !== "file" && content.value) { + if ( + content.type !== "file" && + content.type !== "file[]" && + content.value + ) { return new sdk.QueryParam({ key: key, value: content.value }); } return undefined;