tags in markdown map to CodeBlocks. They may contain JSX children. When
+// the children is not a simple string, we just return a styled block without
+// actually highlighting.
+export default function CodeBlockJSX({
+ children,
+ className,
+}: Props): React.JSX.Element {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/docs/src/theme/ApiExplorer/ApiCodeBlock/Content/String.tsx b/docs/src/theme/ApiExplorer/ApiCodeBlock/Content/String.tsx
new file mode 100644
index 00000000..46042522
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/ApiCodeBlock/Content/String.tsx
@@ -0,0 +1,127 @@
+import React from "react";
+
+import { useThemeConfig, usePrismTheme } from "@docusaurus/theme-common";
+import {
+ parseCodeBlockTitle,
+ parseLanguage,
+ parseLines,
+ containsLineNumbers,
+ useCodeWordWrap,
+} from "@docusaurus/theme-common/internal";
+import Container from "@theme/ApiExplorer/ApiCodeBlock/Container";
+import CopyButton from "@theme/ApiExplorer/ApiCodeBlock/CopyButton";
+import ExpandButton from "@theme/ApiExplorer/ApiCodeBlock/ExpandButton";
+import Line from "@theme/ApiExplorer/ApiCodeBlock/Line";
+import WordWrapButton from "@theme/ApiExplorer/ApiCodeBlock/WordWrapButton";
+import type { Props } from "@theme/CodeBlock/Content/String";
+import clsx from "clsx";
+import { Highlight, Language } from "prism-react-renderer";
+
+export default function CodeBlockString({
+ children,
+ className: blockClassName = "",
+ metastring,
+ title: titleProp,
+ showLineNumbers: showLineNumbersProp,
+ language: languageProp,
+}: Props): React.JSX.Element {
+ const {
+ prism: { defaultLanguage, magicComments },
+ } = useThemeConfig();
+ const language =
+ languageProp ?? parseLanguage(blockClassName) ?? defaultLanguage;
+ const prismTheme = usePrismTheme();
+ const wordWrap = useCodeWordWrap();
+ // We still parse the metastring in case we want to support more syntax in the
+ // future. Note that MDX doesn't strip quotes when parsing metastring:
+ // "title=\"xyz\"" => title: "\"xyz\""
+ const title = parseCodeBlockTitle(metastring) || titleProp;
+ const { lineClassNames, code } = parseLines(children, {
+ metastring,
+ language,
+ magicComments,
+ });
+ const showLineNumbers =
+ showLineNumbersProp ?? containsLineNumbers(metastring);
+
+ return (
+
+ {title && (
+ {title}
+ )}
+
+
+ {({ className, tokens, getLineProps, getTokenProps }) => (
+
+
+ {tokens.map((line, i) => (
+
+ ))}
+
+
+ )}
+
+
+ {(wordWrap.isEnabled || wordWrap.isCodeScrollable) && (
+ wordWrap.toggle()}
+ isEnabled={wordWrap.isEnabled}
+ />
+ )}
+
+
+
+
+
+ );
+}
diff --git a/docs/src/theme/ApiExplorer/ApiCodeBlock/Content/_Content.scss b/docs/src/theme/ApiExplorer/ApiCodeBlock/Content/_Content.scss
new file mode 100644
index 00000000..acc774e1
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/ApiCodeBlock/Content/_Content.scss
@@ -0,0 +1,91 @@
+.openapi-explorer__code-block-content {
+ height: 100%;
+ position: relative;
+ /* rtl:ignore */
+ direction: ltr;
+ border-radius: inherit;
+}
+
+.openapi-explorer__code-block-title {
+ border-bottom: 1px solid var(--ifm-color-emphasis-300);
+ font-size: var(--ifm-code-font-size);
+ font-weight: 500;
+ padding: 0.75rem var(--ifm-pre-padding);
+ border-top-left-radius: inherit;
+ border-top-right-radius: inherit;
+}
+
+.openapi-explorer__code-block {
+ height: 100%;
+ border-radius: var(--ifm-global-radius);
+ --ifm-pre-background: var(--prism-background-color);
+ margin: 0;
+ padding: 0;
+}
+
+.openapi-explorer__code-block-title
+ + .openapi-explorer__code-block-content
+ .openapi-explorer__code-block {
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+}
+
+.openapi-explorer__code-block-standalone {
+ padding: 0;
+}
+
+.openapi-explorer__code-block-lines {
+ font: inherit;
+ /* rtl:ignore */
+ float: left;
+ min-width: 100%;
+ padding: var(--ifm-pre-padding);
+}
+
+.openapi-explorer__code-block-lines-numbering {
+ // This causes max-height to unset
+ // display: table;
+ padding: var(--ifm-pre-padding) 0;
+}
+
+@media print {
+ .openapi-explorer__code-block-lines {
+ white-space: pre-wrap;
+ }
+}
+
+.openapi-explorer__code-block-btn-group {
+ display: flex;
+ column-gap: 0.2rem;
+ position: absolute;
+ right: calc(var(--ifm-pre-padding) / 2);
+ top: calc(var(--ifm-pre-padding) / 2);
+}
+
+.openapi-explorer__code-block-btn-group button {
+ display: flex;
+ align-items: center;
+ background: var(--prism-background-color);
+ color: var(--prism-color);
+ border: 1px solid var(--ifm-color-emphasis-300);
+ border-radius: var(--ifm-global-radius);
+ padding: 0.4rem;
+ line-height: 0;
+ transition: opacity 200ms ease-in-out;
+ opacity: 0;
+}
+
+.openapi-explorer__code-block-btn-group button:focus-visible,
+.openapi-explorer__code-block-btn-group button:hover {
+ opacity: 1 !important;
+}
+
+.theme-code-block:hover .openapi-explorer__code-block-btn-group button {
+ opacity: 0.4;
+}
+
+@media screen and (max-width: 996px) {
+ .openapi-explorer__expand-btn {
+ display: none !important;
+ }
+}
diff --git a/docs/src/theme/ApiExplorer/ApiCodeBlock/CopyButton/_CopyButton.scss b/docs/src/theme/ApiExplorer/ApiCodeBlock/CopyButton/_CopyButton.scss
new file mode 100644
index 00000000..e245fe1b
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/ApiCodeBlock/CopyButton/_CopyButton.scss
@@ -0,0 +1,44 @@
+.theme-code-block:hover {
+ .openapi-explorer__code-block-copy-btn--copied {
+ opacity: 1 !important;
+ }
+}
+
+.openapi-explorer__code-block-copy-btn-icons {
+ position: relative;
+ width: 1.125rem;
+ height: 1.125rem;
+}
+
+.openapi-explorer__code-block-copy-btn-icon,
+.openapi-explorer__code-block-copy-btn-icon--success {
+ position: absolute;
+ top: 0;
+ left: 0;
+ fill: currentColor;
+ opacity: inherit;
+ width: inherit;
+ height: inherit;
+ transition: all 0.15s ease;
+}
+
+.openapi-explorer__code-block-copy-btn-icon--success {
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%) scale(0.33);
+ opacity: 0;
+ color: #00d600;
+}
+
+.openapi-explorer__code-block-copy-btn--copied {
+ .openapi-explorer__code-block-copy-btn-icon {
+ transform: scale(0.33);
+ opacity: 0;
+ }
+
+ .openapi-explorer__code-block-copy-btn-icon--success {
+ transform: translate(-50%, -50%) scale(1);
+ opacity: 1;
+ transition-delay: 0.075s;
+ }
+}
diff --git a/docs/src/theme/ApiExplorer/ApiCodeBlock/CopyButton/index.tsx b/docs/src/theme/ApiExplorer/ApiCodeBlock/CopyButton/index.tsx
new file mode 100644
index 00000000..0431e508
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/ApiCodeBlock/CopyButton/index.tsx
@@ -0,0 +1,72 @@
+import React, { useCallback, useState, useRef, useEffect } from "react";
+
+import { CopyButtonProps } from "@docusaurus/theme-common/internal";
+import { translate } from "@docusaurus/Translate";
+import clsx from "clsx";
+import copy from "copy-text-to-clipboard";
+
+export default function CopyButton({
+ code,
+ className,
+}: CopyButtonProps): React.JSX.Element {
+ const [isCopied, setIsCopied] = useState(false);
+ const copyTimeout = useRef(undefined);
+ const handleCopyCode = useCallback(() => {
+ copy(code);
+ setIsCopied(true);
+ copyTimeout.current = window.setTimeout(() => {
+ setIsCopied(false);
+ }, 1000);
+ }, [code]);
+
+ useEffect(() => () => window.clearTimeout(copyTimeout.current), []);
+
+ return (
+
+ );
+}
diff --git a/docs/src/theme/ApiExplorer/ApiCodeBlock/ExitButton/_ExitButton.scss b/docs/src/theme/ApiExplorer/ApiCodeBlock/ExitButton/_ExitButton.scss
new file mode 100644
index 00000000..4aaf9602
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/ApiCodeBlock/ExitButton/_ExitButton.scss
@@ -0,0 +1,16 @@
+.openapi-explorer__code-block-exit-btn-icons {
+ position: relative;
+ width: 1.125rem;
+ height: 1.125rem;
+}
+
+.openapi-explorer__code-block-exit-btn-icon {
+ position: absolute;
+ top: 0;
+ left: 0;
+ fill: currentColor;
+ opacity: inherit;
+ width: inherit;
+ height: inherit;
+ transition: all 0.15s ease;
+}
diff --git a/docs/src/theme/ApiExplorer/ApiCodeBlock/ExitButton/index.tsx b/docs/src/theme/ApiExplorer/ApiCodeBlock/ExitButton/index.tsx
new file mode 100644
index 00000000..95b33897
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/ApiCodeBlock/ExitButton/index.tsx
@@ -0,0 +1,48 @@
+import React from "react";
+
+import { translate } from "@docusaurus/Translate";
+import clsx from "clsx";
+
+export interface Props {
+ readonly className: string;
+ readonly handler: () => void;
+}
+
+export default function ExitButton({
+ className,
+ handler,
+}: Props): React.JSX.Element {
+ return (
+
+ );
+}
diff --git a/docs/src/theme/ApiExplorer/ApiCodeBlock/ExpandButton/_ExpandButton.scss b/docs/src/theme/ApiExplorer/ApiCodeBlock/ExpandButton/_ExpandButton.scss
new file mode 100644
index 00000000..dcf38279
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/ApiCodeBlock/ExpandButton/_ExpandButton.scss
@@ -0,0 +1,62 @@
+.openapi-explorer__expand-modal-content {
+ padding: none;
+ border: thin solid var(--ifm-toc-border-color);
+ border-radius: var(--ifm-global-radius);
+ max-width: 95%;
+ width: 65vw;
+ height: 65vh;
+ overflow: auto;
+}
+
+.openapi-explorer__expand-modal-overlay {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: fixed;
+ inset: 0px;
+ background-color: rgba(0, 0, 0, 0.9);
+ z-index: 201;
+}
+
+.theme-code-block:hover .openapi-explorer__code-block-expand-btn--copied {
+ opacity: 1 !important;
+}
+
+.openapi-explorer__code-block-expand-btn-icons {
+ position: relative;
+ width: 1.125rem;
+ height: 1.125rem;
+}
+
+.openapi-explorer__code-block-expand-btn-icon,
+.openapi-explorer__code-block-expand-btn-icon--success {
+ position: absolute;
+ top: 0;
+ left: 0;
+ fill: currentColor;
+ opacity: inherit;
+ width: inherit;
+ height: inherit;
+ transition: all 0.15s ease;
+}
+
+.openapi-explorer__code-block-expand-btn-icon--success {
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%) scale(0.33);
+ opacity: 0;
+ color: #00d600;
+}
+
+.openapi-explorer__code-block-expand-btn--copied
+ .openapi-explorer__code-block-expand-btn-icon {
+ transform: scale(0.33);
+ opacity: 0;
+}
+
+.openapi-explorer__code-block-expand-btn--copied
+ .openapi-explorer__code-block-expand-btn-icon--success {
+ transform: translate(-50%, -50%) scale(1);
+ opacity: 1;
+ transition-delay: 0.075s;
+}
diff --git a/docs/src/theme/ApiExplorer/ApiCodeBlock/ExpandButton/index.tsx b/docs/src/theme/ApiExplorer/ApiCodeBlock/ExpandButton/index.tsx
new file mode 100644
index 00000000..14db1367
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/ApiCodeBlock/ExpandButton/index.tsx
@@ -0,0 +1,159 @@
+import React, { useEffect, useState } from "react";
+
+import { usePrismTheme } from "@docusaurus/theme-common";
+import { translate } from "@docusaurus/Translate";
+import Container from "@theme/ApiExplorer/ApiCodeBlock/Container";
+import CopyButton from "@theme/ApiExplorer/ApiCodeBlock/CopyButton";
+import ExitButton from "@theme/ApiExplorer/ApiCodeBlock/ExitButton";
+import Line from "@theme/ApiExplorer/ApiCodeBlock/Line";
+import clsx from "clsx";
+import { Highlight, Language } from "prism-react-renderer";
+import Modal from "react-modal";
+
+export interface Props {
+ readonly code: string;
+ readonly className: string;
+ readonly language: Language;
+ readonly showLineNumbers: boolean;
+ readonly blockClassName: string;
+ readonly title: string | undefined;
+ readonly lineClassNames: { [lineIndex: number]: string[] };
+}
+
+export default function ExpandButton({
+ code,
+ className,
+ language,
+ showLineNumbers,
+ blockClassName,
+ title,
+ lineClassNames,
+}: Props): React.JSX.Element {
+ const [isModalOpen, setIsModalOpen] = useState(false);
+ const prismTheme = usePrismTheme();
+
+ useEffect(() => {
+ Modal.setAppElement("body");
+ }, []);
+
+ return (
+ <>
+
+ setIsModalOpen(false)}
+ contentLabel="Code Snippet"
+ >
+
+ {title && (
+ {title}
+ )}
+
+
+ {({ className, tokens, getLineProps, getTokenProps }) => (
+
+
+ {tokens.map((line, i) => (
+
+ ))}
+
+
+ )}
+
+
+
+ setIsModalOpen(false)}
+ />
+
+
+
+
+ >
+ );
+}
diff --git a/docs/src/theme/ApiExplorer/ApiCodeBlock/Line/_Line.scss b/docs/src/theme/ApiExplorer/ApiCodeBlock/Line/_Line.scss
new file mode 100644
index 00000000..8a83a0d0
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/ApiCodeBlock/Line/_Line.scss
@@ -0,0 +1,32 @@
+:where(:root) {
+ --docusaurus-highlighted-code-line-bg: rgb(72 77 91);
+}
+
+:where([data-theme="dark"]) {
+ --docusaurus-highlighted-code-line-bg: rgb(100 100 100);
+}
+
+.openapi-explorer__code-block-code-line {
+ display: table-row;
+ counter-increment: line-count;
+}
+
+.openapi-explorer__code-block-code-line-number {
+ display: table-cell;
+ text-align: right;
+ width: 1%;
+ position: sticky;
+ left: 0;
+ padding: 0 var(--ifm-pre-padding);
+ background: var(--ifm-pre-background);
+ overflow-wrap: normal;
+}
+
+.openapi-explorer__code-block-code-line-number::before {
+ content: counter(line-count);
+ opacity: 0.4;
+}
+
+.openapi-explorer__code-block-code-line-number {
+ padding-right: var(--ifm-pre-padding);
+}
diff --git a/docs/src/theme/ApiExplorer/ApiCodeBlock/Line/index.tsx b/docs/src/theme/ApiExplorer/ApiCodeBlock/Line/index.tsx
new file mode 100644
index 00000000..87e5de92
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/ApiCodeBlock/Line/index.tsx
@@ -0,0 +1,41 @@
+import React from "react";
+
+import { LineProps } from "@docusaurus/theme-common/internal";
+import clsx from "clsx";
+
+export default function CodeBlockLine({
+ line,
+ classNames,
+ showLineNumbers,
+ getLineProps,
+ getTokenProps,
+}: LineProps): React.JSX.Element {
+ if (line.length === 1 && line[0].content === "\n") {
+ line[0].content = "";
+ }
+ const lineProps = getLineProps({
+ line,
+ className: clsx(
+ classNames,
+ showLineNumbers && "openapi-explorer__code-block-code-line",
+ ),
+ });
+ const lineTokens = line.map((token, key) => (
+
+ ));
+ return (
+
+ {showLineNumbers ? (
+ <>
+
+
+ {lineTokens}
+
+ >
+ ) : (
+ lineTokens
+ )}
+
+
+ );
+}
diff --git a/docs/src/theme/ApiExplorer/ApiCodeBlock/WordWrapButton/_WordWrapButton.scss b/docs/src/theme/ApiExplorer/ApiCodeBlock/WordWrapButton/_WordWrapButton.scss
new file mode 100644
index 00000000..cf2c8aa9
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/ApiCodeBlock/WordWrapButton/_WordWrapButton.scss
@@ -0,0 +1,10 @@
+.openapi-explorer__code-block-word-wrap-btn-icon {
+ width: 1.2rem;
+ height: 1.2rem;
+}
+
+.openapi-explorer__code-block-word-wrap-btn--enabled {
+ .openapi-explorer__code-block-word-wrap-btn-icon {
+ color: var(--ifm-color-primary);
+ }
+}
diff --git a/docs/src/theme/ApiExplorer/ApiCodeBlock/WordWrapButton/index.tsx b/docs/src/theme/ApiExplorer/ApiCodeBlock/WordWrapButton/index.tsx
new file mode 100644
index 00000000..5f0e60cb
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/ApiCodeBlock/WordWrapButton/index.tsx
@@ -0,0 +1,47 @@
+import React from "react";
+
+import { translate } from "@docusaurus/Translate";
+import clsx from "clsx";
+
+export interface Props {
+ readonly className?: string;
+ readonly onClick: React.MouseEventHandler;
+ readonly isEnabled: boolean;
+}
+
+export default function WordWrapButton({
+ className,
+ onClick,
+ isEnabled,
+}: Props): React.JSX.Element | null {
+ const title = translate({
+ id: "theme.CodeBlock.wordWrapToggle",
+ message: "Toggle word wrap",
+ description:
+ "The title attribute for toggle word wrapping button of code block lines",
+ });
+ return (
+
+ );
+}
diff --git a/docs/src/theme/ApiExplorer/ApiCodeBlock/index.tsx b/docs/src/theme/ApiExplorer/ApiCodeBlock/index.tsx
new file mode 100644
index 00000000..62dc4f00
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/ApiCodeBlock/index.tsx
@@ -0,0 +1,39 @@
+import React, { isValidElement, ReactNode } from "react";
+
+import { CodeBlockProps } from "@docusaurus/theme-common/internal";
+import useIsBrowser from "@docusaurus/useIsBrowser";
+import ElementContent from "@theme/ApiExplorer/ApiCodeBlock/Content/Element";
+import StringContent from "@theme/ApiExplorer/ApiCodeBlock/Content/String";
+
+/**
+ * Best attempt to make the children a plain string so it is copyable. If there
+ * are react elements, we will not be able to copy the content, and it will
+ * return `children` as-is; otherwise, it concatenates the string children
+ * together.
+ */
+function maybeStringifyChildren(children: ReactNode): ReactNode {
+ if (React.Children.toArray(children).some((el) => isValidElement(el))) {
+ return children;
+ }
+ // The children is now guaranteed to be one/more plain strings
+ return Array.isArray(children) ? children.join("") : (children as string);
+}
+export default function ApiCodeBlock({
+ children: rawChildren,
+ ...props
+}: CodeBlockProps) {
+ // The Prism theme on SSR is always the default theme but the site theme can
+ // be in a different mode. React hydration doesn't update DOM styles that come
+ // from SSR. Hence force a re-render after mounting to apply the current
+ // relevant styles.
+ const isBrowser = useIsBrowser();
+ const children = maybeStringifyChildren(rawChildren);
+ const CodeBlockComp =
+ typeof children === "string" ? StringContent : ElementContent;
+
+ return (
+
+ {children as string}
+
+ );
+}
diff --git a/docs/src/theme/ApiExplorer/Authorization/auth-types.ts b/docs/src/theme/ApiExplorer/Authorization/auth-types.ts
new file mode 100644
index 00000000..125d18c9
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/Authorization/auth-types.ts
@@ -0,0 +1,23 @@
+export function getAuthDataKeys(security: { [key: string]: any }) {
+ // Bearer Auth
+ if (security.type === "http" && security.scheme === "bearer") {
+ return ["token"];
+ }
+
+ if (security.type === "oauth2") {
+ return ["token"];
+ }
+
+ // Basic Auth
+ if (security.type === "http" && security.scheme === "basic") {
+ return ["username", "password"];
+ }
+
+ // API Auth
+ if (security.type === "apiKey") {
+ return ["apiKey"];
+ }
+
+ // none
+ return [];
+}
diff --git a/docs/src/theme/ApiExplorer/Authorization/index.tsx b/docs/src/theme/ApiExplorer/Authorization/index.tsx
new file mode 100644
index 00000000..42a7c61d
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/Authorization/index.tsx
@@ -0,0 +1,151 @@
+import React from "react";
+
+import FormItem from "@theme/ApiExplorer/FormItem";
+import FormSelect from "@theme/ApiExplorer/FormSelect";
+import FormTextInput from "@theme/ApiExplorer/FormTextInput";
+import { useTypedDispatch, useTypedSelector } from "@theme/ApiItem/hooks";
+
+import { setAuthData, setSelectedAuth } from "./slice";
+
+function Authorization() {
+ const data = useTypedSelector((state: any) => state.auth.data);
+ const options = useTypedSelector((state: any) => state.auth.options);
+ const selected = useTypedSelector((state: any) => state.auth.selected);
+
+ const dispatch = useTypedDispatch();
+
+ if (selected === undefined) {
+ return null;
+ }
+
+ const selectedAuth = options[selected];
+
+ const optionKeys = Object.keys(options);
+
+ return (
+
+ {optionKeys.length > 1 && (
+
+ ) => {
+ dispatch(setSelectedAuth(e.target.value));
+ }}
+ />
+
+ )}
+ {selectedAuth.map((a: any) => {
+ if (a.type === "http" && a.scheme === "bearer") {
+ return (
+
+ ) => {
+ const value = e.target.value;
+ dispatch(
+ setAuthData({
+ scheme: a.key,
+ key: "token",
+ value: value ? value : undefined,
+ }),
+ );
+ }}
+ />
+
+ );
+ }
+
+ if (a.type === "oauth2") {
+ return (
+
+ ) => {
+ const value = e.target.value;
+ dispatch(
+ setAuthData({
+ scheme: a.key,
+ key: "token",
+ value: value ? value : undefined,
+ }),
+ );
+ }}
+ />
+
+ );
+ }
+
+ if (a.type === "http" && a.scheme === "basic") {
+ return (
+
+
+ ) => {
+ const value = e.target.value;
+ dispatch(
+ setAuthData({
+ scheme: a.key,
+ key: "username",
+ value: value ? value : undefined,
+ }),
+ );
+ }}
+ />
+
+
+ ) => {
+ const value = e.target.value;
+ dispatch(
+ setAuthData({
+ scheme: a.key,
+ key: "password",
+ value: value ? value : undefined,
+ }),
+ );
+ }}
+ />
+
+
+ );
+ }
+
+ if (a.type === "apiKey") {
+ return (
+
+ ) => {
+ const value = e.target.value;
+ dispatch(
+ setAuthData({
+ scheme: a.key,
+ key: "apiKey",
+ value: value ? value : undefined,
+ }),
+ );
+ }}
+ />
+
+ );
+ }
+
+ return null;
+ })}
+
+ );
+}
+
+export default Authorization;
diff --git a/docs/src/theme/ApiExplorer/Authorization/slice.ts b/docs/src/theme/ApiExplorer/Authorization/slice.ts
new file mode 100644
index 00000000..64a0fa51
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/Authorization/slice.ts
@@ -0,0 +1,139 @@
+import { createSlice, PayloadAction } from "@reduxjs/toolkit";
+import { createStorage, hashArray } from "@theme/ApiExplorer/storage-utils";
+import {
+ SecurityRequirementObject,
+ SecuritySchemeObject,
+} from "docusaurus-plugin-openapi-docs/src/openapi/types";
+/* eslint-disable import/no-extraneous-dependencies*/
+import { ThemeConfig } from "docusaurus-theme-openapi-docs/src/types";
+
+import { getAuthDataKeys } from "./auth-types";
+
+// The global definitions
+// "securitySchemes": {
+// "BearerAuth": { "type": "http", "scheme": "BeAreR" },
+// "BasicAuth": { "type": "http", "scheme": "basic" }
+// },
+
+// The operation level requirements
+// "security": [
+// { "BearerAuth": [] },
+// { "BearerAuth": [], "BasicAuth": [] }
+// ],
+
+// SLICE_STATE
+// data:
+// BearerAuth:
+// token=xxx
+// BasicAuth:
+// username=xxx
+// password=xxx
+//
+// options:
+// "BearerAuth": [{ key: "BearerAuth", scopes: [], ...rest }]
+// "BearerAuth and BasicAuth": [{ key: "BearerAuth", scopes: [], ...rest }, { key: "BasicAuth", scopes: [], ...rest }]
+//
+// selected: "BearerAuth and BasicAuth"
+
+// LOCAL_STORAGE
+// hash(SLICE_STATE.options) -> "BearerAuth and BasicAuth"
+// BearerAuth -> { token: xxx }
+// BasicAuth -> { username: xxx, password: xxx }
+
+export function createAuth({
+ security,
+ securitySchemes,
+ options: opts,
+}: {
+ security?: SecurityRequirementObject[];
+ securitySchemes?: {
+ [key: string]: SecuritySchemeObject;
+ };
+ options?: ThemeConfig["api"];
+}): AuthState {
+ const storage = createStorage("sessionStorage");
+
+ let data: AuthState["data"] = {};
+ let options: AuthState["options"] = {};
+
+ for (const option of security ?? []) {
+ const id = Object.keys(option).join(" and ");
+ for (const [schemeID, scopes] of Object.entries(option)) {
+ const scheme = securitySchemes?.[schemeID];
+ if (scheme) {
+ if (options[id] === undefined) {
+ options[id] = [];
+ }
+ const dataKeys = getAuthDataKeys(scheme);
+ for (const key of dataKeys) {
+ if (data[schemeID] === undefined) {
+ data[schemeID] = {};
+ }
+
+ let persisted = undefined;
+ try {
+ persisted = JSON.parse(storage.getItem(schemeID) ?? "")[key];
+ } catch {}
+
+ data[schemeID][key] = persisted;
+ }
+ options[id].push({
+ ...scheme,
+ key: schemeID,
+ scopes,
+ });
+ }
+ }
+ }
+
+ let persisted = undefined;
+ try {
+ persisted = storage.getItem(hashArray(Object.keys(options))) ?? undefined;
+ } catch {}
+
+ return {
+ data,
+ options,
+ selected: persisted ?? Object.keys(options)[0],
+ };
+}
+
+export type Scheme = {
+ key: string;
+ scopes: string[];
+} & SecuritySchemeObject;
+
+export interface AuthState {
+ data: {
+ [scheme: string]: {
+ [key: string]: string | undefined;
+ };
+ };
+ options: {
+ [key: string]: Scheme[];
+ };
+ selected?: string;
+}
+
+const initialState: AuthState = {} as any;
+
+export const slice = createSlice({
+ name: "auth",
+ initialState,
+ reducers: {
+ setAuthData: (
+ state,
+ action: PayloadAction<{ scheme: string; key: string; value?: string }>,
+ ) => {
+ const { scheme, key, value } = action.payload;
+ state.data[scheme][key] = value;
+ },
+ setSelectedAuth: (state, action: PayloadAction) => {
+ state.selected = action.payload;
+ },
+ },
+});
+
+export const { setAuthData, setSelectedAuth } = slice.actions;
+
+export default slice.reducer;
diff --git a/docs/src/theme/ApiExplorer/Body/index.tsx b/docs/src/theme/ApiExplorer/Body/index.tsx
new file mode 100644
index 00000000..054fe7ae
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/Body/index.tsx
@@ -0,0 +1,369 @@
+import React from "react";
+
+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";
+import SchemaTabs from "@theme/SchemaTabs";
+import TabItem from "@theme/TabItem";
+import { RequestBodyObject } from "docusaurus-plugin-openapi-docs/src/openapi/types";
+import format from "xml-formatter";
+
+import {
+ clearFormBodyKey,
+ clearRawBody,
+ setFileFormBody,
+ setFileRawBody,
+ setStringFormBody,
+} from "./slice";
+
+export interface Props {
+ jsonRequestBodyExample: string;
+ requestBodyMetadata?: RequestBodyObject;
+ methods?: any;
+ required?: boolean;
+}
+
+function BodyWrap({
+ requestBodyMetadata,
+ jsonRequestBodyExample,
+ methods,
+ required,
+}: Props) {
+ const contentType = useTypedSelector((state: any) => state.contentType.value);
+
+ // NOTE: We used to check if body was required, but opted to always show the request body
+ // to reduce confusion, see: https://github.com/cloud-annotations/docusaurus-openapi/issues/145
+
+ // No body
+ if (contentType === undefined) {
+ return null;
+ }
+
+ return (
+
+ );
+}
+
+function Body({
+ requestBodyMetadata,
+ jsonRequestBodyExample,
+ methods,
+ required,
+}: Props) {
+ const contentType = useTypedSelector((state: any) => state.contentType.value);
+ const dispatch = useTypedDispatch();
+
+ // Lot's of possible content-types:
+ // - application/json
+ // - application/xml
+ // - text/plain
+ // - text/css
+ // - text/html
+ // - text/javascript
+ // - application/javascript
+ // - multipart/form-data
+ // - application/x-www-form-urlencoded
+ // - image/svg+xml;charset=US-ASCII
+
+ // Show editor:
+ // - application/json
+ // - application/xml
+ // - */*
+
+ // Show form:
+ // - multipart/form-data
+ // - application/x-www-form-urlencoded
+
+ const schema = requestBodyMetadata?.content?.[contentType]?.schema;
+ const example = requestBodyMetadata?.content?.[contentType]?.example;
+ const examples = requestBodyMetadata?.content?.[contentType]?.examples;
+
+ if (schema?.format === "binary") {
+ return (
+
+ {
+ if (file === undefined) {
+ dispatch(clearRawBody());
+ return;
+ }
+ dispatch(
+ setFileRawBody({
+ src: `/path/to/${file.name}`,
+ content: file,
+ }),
+ );
+ }}
+ />
+
+ );
+ }
+ if (
+ (contentType === "multipart/form-data" ||
+ contentType === "application/x-www-form-urlencoded") &&
+ schema?.type === "object"
+ ) {
+ 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 }),
+ );
+ }}
+ />
+
+ );
+ })}
+
+
+ );
+ }
+
+ let language = "plaintext";
+ let defaultBody = ""; //"body content";
+ let exampleBody;
+ let examplesBodies = [] as any;
+
+ if (
+ contentType.includes("application/json") ||
+ contentType.endsWith("+json")
+ ) {
+ if (jsonRequestBodyExample) {
+ defaultBody = JSON.stringify(jsonRequestBodyExample, null, 2);
+ }
+ if (example) {
+ exampleBody = JSON.stringify(example, null, 2);
+ }
+ if (examples) {
+ for (const [key, example] of Object.entries(examples)) {
+ let body = example.value;
+ try {
+ // If the value is already valid JSON we shouldn't double encode the value
+ JSON.parse(example.value);
+ } catch (e) {
+ body = JSON.stringify(example.value, null, 2);
+ }
+
+ examplesBodies.push({
+ label: key,
+ body,
+ summary: example.summary,
+ });
+ }
+ }
+ language = "json";
+ }
+
+ if (contentType === "application/xml" || contentType.endsWith("+xml")) {
+ if (jsonRequestBodyExample) {
+ try {
+ defaultBody = format(json2xml(jsonRequestBodyExample, ""), {
+ indentation: " ",
+ lineSeparator: "\n",
+ collapseContent: true,
+ });
+ } catch {
+ defaultBody = json2xml(jsonRequestBodyExample);
+ }
+ }
+ if (example) {
+ try {
+ exampleBody = format(json2xml(example, ""), {
+ indentation: " ",
+ lineSeparator: "\n",
+ collapseContent: true,
+ });
+ } catch {
+ exampleBody = json2xml(example);
+ }
+ }
+ if (examples) {
+ for (const [key, example] of Object.entries(examples)) {
+ let formattedXmlBody;
+ try {
+ formattedXmlBody = format(example.value, {
+ indentation: " ",
+ lineSeparator: "\n",
+ collapseContent: true,
+ });
+ } catch {
+ formattedXmlBody = example.value;
+ }
+ examplesBodies.push({
+ label: key,
+ body: formattedXmlBody,
+ summary: example.summary,
+ });
+ }
+ }
+ language = "xml";
+ }
+
+ if (exampleBody) {
+ return (
+
+
+ {/* @ts-ignore */}
+
+
+ {defaultBody}
+
+
+ {/* @ts-ignore */}
+
+ {example.summary && {example.summary} }
+ {exampleBody && (
+
+ {exampleBody}
+
+ )}
+
+
+
+ );
+ }
+
+ if (examplesBodies && examplesBodies.length > 0) {
+ return (
+
+
+ {/* @ts-ignore */}
+
+
+ {defaultBody}
+
+
+ {examplesBodies.map((example: any) => {
+ return (
+ // @ts-ignore
+
+ {example.summary && {example.summary} }
+ {example.body && (
+
+ {example.body}
+
+ )}
+
+ );
+ })}
+
+
+ );
+ }
+
+ return (
+
+
+ {defaultBody}
+
+
+ );
+}
+
+export default BodyWrap;
diff --git a/docs/src/theme/ApiExplorer/Body/json2xml.js b/docs/src/theme/ApiExplorer/Body/json2xml.js
new file mode 100644
index 00000000..6092291b
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/Body/json2xml.js
@@ -0,0 +1,36 @@
+export default function json2xml(o, tab) {
+ var toXml = function (v, name, ind) {
+ var xml = "";
+ if (v instanceof Array) {
+ for (var i = 0, n = v.length; i < n; i++)
+ xml += ind + toXml(v[i], name, ind + "\t") + "\n";
+ } else if (typeof v == "object") {
+ var hasChild = false;
+ xml += ind + "<" + name;
+ for (var m in v) {
+ if (m.charAt(0) === "@")
+ xml += " " + m.substr(1) + '="' + v[m].toString() + '"';
+ else hasChild = true;
+ }
+ xml += hasChild ? ">" : "/>";
+ if (hasChild) {
+ for (var m2 in v) {
+ if (m2 === "#text") xml += v[m2];
+ else if (m2 === "#cdata") xml += "";
+ else if (m2.charAt(0) !== "@") xml += toXml(v[m2], m2, ind + "\t");
+ }
+ xml +=
+ (xml.charAt(xml.length - 1) === "\n" ? ind : "") +
+ "" +
+ name +
+ ">";
+ }
+ } else {
+ xml += ind + "<" + name + ">" + v.toString() + "" + name + ">";
+ }
+ return xml;
+ },
+ xml = "";
+ for (var m3 in o) xml += toXml(o[m3], m3, "");
+ return tab ? xml.replace(/\t/g, tab) : xml.replace(/\t|\n/g, "");
+}
diff --git a/docs/src/theme/ApiExplorer/Body/slice.ts b/docs/src/theme/ApiExplorer/Body/slice.ts
new file mode 100644
index 00000000..59c68416
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/Body/slice.ts
@@ -0,0 +1,126 @@
+import { createSlice, PayloadAction } from "@reduxjs/toolkit";
+
+export interface FileContent {
+ type: "file";
+ value: {
+ src: string;
+ content: Blob;
+ };
+}
+
+export interface StringContent {
+ type: "string";
+ value?: string;
+}
+
+export type Content = FileContent | StringContent | undefined;
+
+export interface FormBody {
+ type: "form";
+ content: {
+ [key: string]: Content;
+ };
+}
+
+export interface RawBody {
+ type: "raw";
+ content: Content;
+}
+
+export interface EmptyBody {
+ type: "empty";
+}
+
+export type Body = EmptyBody | FormBody | RawBody;
+
+export type State = Body;
+
+const initialState: State = {} as any;
+
+export const slice = createSlice({
+ name: "body",
+ initialState,
+ reducers: {
+ clearRawBody: (_state) => {
+ return {
+ type: "empty",
+ };
+ },
+ setStringRawBody: (_state, action: PayloadAction) => {
+ return {
+ type: "raw",
+ content: {
+ type: "string",
+ value: action.payload,
+ },
+ };
+ },
+ setFileRawBody: (_state, action: PayloadAction) => {
+ return {
+ type: "raw",
+ content: {
+ type: "file",
+ value: action.payload,
+ },
+ };
+ },
+ clearFormBodyKey: (state, action: PayloadAction) => {
+ if (state?.type === "form") {
+ delete state.content[action.payload];
+ }
+ },
+ setStringFormBody: (
+ state,
+ action: PayloadAction<{ key: string; value: string }>,
+ ) => {
+ if (state?.type !== "form") {
+ return {
+ type: "form",
+ content: {
+ [action.payload.key]: {
+ type: "string",
+ value: action.payload.value,
+ },
+ },
+ };
+ }
+ state.content[action.payload.key] = {
+ type: "string",
+ value: action.payload.value,
+ };
+ return state;
+ },
+ setFileFormBody: (
+ state,
+ action: PayloadAction<{ key: string; value: FileContent["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;
+ },
+ },
+});
+
+export const {
+ clearRawBody,
+ setStringRawBody,
+ setFileRawBody,
+ clearFormBodyKey,
+ setStringFormBody,
+ setFileFormBody,
+} = slice.actions;
+
+export default slice.reducer;
diff --git a/docs/src/theme/ApiExplorer/CodeSnippets/code-snippets-types.ts b/docs/src/theme/ApiExplorer/CodeSnippets/code-snippets-types.ts
new file mode 100644
index 00000000..09ae5c43
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/CodeSnippets/code-snippets-types.ts
@@ -0,0 +1,50 @@
+// https://github.com/github-linguist/linguist/blob/master/lib/linguist/popular.yml
+export type CodeSampleLanguage =
+ | "C"
+ | "C#"
+ | "C++"
+ | "CoffeeScript"
+ | "CSS"
+ | "Dart"
+ | "DM"
+ | "Elixir"
+ | "Go"
+ | "Groovy"
+ | "HTML"
+ | "Java"
+ | "JavaScript"
+ | "Kotlin"
+ | "Objective-C"
+ | "OCaml"
+ | "Perl"
+ | "PHP"
+ | "PowerShell"
+ | "Python"
+ | "R"
+ | "Ruby"
+ | "Rust"
+ | "Scala"
+ | "Shell"
+ | "Swift"
+ | "TypeScript";
+
+export interface Language {
+ highlight: string;
+ language: string;
+ codeSampleLanguage: CodeSampleLanguage;
+ logoClass: string;
+ variant: string;
+ variants: string[];
+ options?: { [key: string]: boolean };
+ sample?: string;
+ samples?: string[];
+ samplesSources?: string[];
+ samplesLabels?: string[];
+}
+
+// https://redocly.com/docs/api-reference-docs/specification-extensions/x-code-samples
+export interface CodeSample {
+ source: string;
+ lang: CodeSampleLanguage;
+ label?: string;
+}
diff --git a/docs/src/theme/ApiExplorer/CodeSnippets/index.tsx b/docs/src/theme/ApiExplorer/CodeSnippets/index.tsx
new file mode 100644
index 00000000..1ca03201
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/CodeSnippets/index.tsx
@@ -0,0 +1,331 @@
+import React, { useState, useEffect, type JSX } from "react";
+
+import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
+import ApiCodeBlock from "@theme/ApiExplorer/ApiCodeBlock";
+import buildPostmanRequest from "@theme/ApiExplorer/buildPostmanRequest";
+import CodeTabs from "@theme/ApiExplorer/CodeTabs";
+import { useTypedSelector } from "@theme/ApiItem/hooks";
+import cloneDeep from "lodash/cloneDeep";
+import codegen from "postman-code-generators";
+import sdk from "postman-collection";
+
+import { CodeSample, Language } from "./code-snippets-types";
+import {
+ getCodeSampleSourceFromLanguage,
+ mergeArraysbyLanguage,
+ mergeCodeSampleLanguage,
+ generateLanguageSet,
+} from "./languages";
+
+export const languageSet: Language[] = generateLanguageSet();
+
+export interface Props {
+ postman: sdk.Request;
+ codeSamples: CodeSample[];
+}
+
+function CodeTab({ children, hidden, className }: any): JSX.Element {
+ return (
+
+ {children}
+
+ );
+}
+
+function CodeSnippets({ postman, codeSamples }: Props) {
+ const { siteConfig } = useDocusaurusContext();
+
+ const contentType = useTypedSelector((state: any) => state.contentType.value);
+ const accept = useTypedSelector((state: any) => state.accept.value);
+ const server = useTypedSelector((state: any) => state.server.value);
+ const body = useTypedSelector((state: any) => state.body);
+
+ const pathParams = useTypedSelector((state: any) => state.params.path);
+ const queryParams = useTypedSelector((state: any) => state.params.query);
+ const cookieParams = useTypedSelector((state: any) => state.params.cookie);
+ const headerParams = useTypedSelector((state: any) => state.params.header);
+
+ const auth = useTypedSelector((state: any) => state.auth);
+ const clonedAuth = cloneDeep(auth);
+ let placeholder: string;
+
+ function cleanCredentials(obj: any) {
+ for (const key in obj) {
+ if (typeof obj[key] === "object" && obj[key] !== null) {
+ // use name as placeholder if exists
+ const comboAuthId = Object.keys(obj).join(" and ");
+ const authOptions =
+ clonedAuth?.options?.[key] ?? clonedAuth?.options?.[comboAuthId];
+ placeholder = authOptions?.[0]?.name;
+ obj[key] = cleanCredentials(obj[key]);
+ } else {
+ obj[key] = `<${placeholder ?? key}>`;
+ }
+ }
+
+ return obj;
+ }
+
+ // scrub credentials from code snippets
+ const cleanedAuth = {
+ ...clonedAuth,
+ data: cleanCredentials(clonedAuth.data),
+ };
+
+ // Create a Postman request object using cleanedAuth
+ const cleanedPostmanRequest = buildPostmanRequest(postman, {
+ queryParams,
+ pathParams,
+ cookieParams,
+ contentType,
+ accept,
+ headerParams,
+ body,
+ server,
+ auth: cleanedAuth,
+ });
+
+ // User-defined languages array
+ // Can override languageSet, change order of langs, override options and variants
+ const userDefinedLanguageSet =
+ (siteConfig?.themeConfig?.languageTabs as Language[] | undefined) ??
+ languageSet;
+
+ // Filter languageSet by user-defined langs
+ const filteredLanguageSet = languageSet.filter((ls) => {
+ return userDefinedLanguageSet?.some((lang) => {
+ return lang.language === ls.language;
+ });
+ });
+
+ // Merge user-defined langs into languageSet
+ const mergedLangs = mergeCodeSampleLanguage(
+ mergeArraysbyLanguage(userDefinedLanguageSet, filteredLanguageSet),
+ codeSamples,
+ );
+
+ // Read defaultLang from localStorage
+ const defaultLang: Language[] = mergedLangs.filter(
+ (lang) =>
+ lang.language === localStorage.getItem("docusaurus.tab.code-samples"),
+ );
+ const [selectedVariant, setSelectedVariant] = useState();
+ const [selectedSample, setSelectedSample] = useState();
+ const [language, setLanguage] = useState(() => {
+ // Return first index if only 1 user-defined language exists
+ if (mergedLangs.length === 1) {
+ return mergedLangs[0];
+ }
+ // Fall back to language in localStorage or first user-defined language
+ return defaultLang[0] ?? mergedLangs[0];
+ });
+ const [codeText, setCodeText] = useState("");
+ const [codeSampleCodeText, setCodeSampleCodeText] = useState<
+ string | (() => string)
+ >(() => getCodeSampleSourceFromLanguage(language));
+
+ useEffect(() => {
+ if (language && !!language.sample) {
+ setCodeSampleCodeText(getCodeSampleSourceFromLanguage(language));
+ }
+
+ if (language && !!language.options) {
+ codegen.convert(
+ language.language,
+ language.variant,
+ cleanedPostmanRequest,
+ language.options,
+ (error: any, snippet: string) => {
+ if (error) {
+ return;
+ }
+ setCodeText(snippet);
+ },
+ );
+ } else if (language && !language.options) {
+ const langSource = mergedLangs.filter(
+ (lang) => lang.language === language.language,
+ );
+
+ // Merges user-defined language with default languageSet
+ // This allows users to define only the minimal properties necessary in languageTabs
+ // User-defined properties should override languageSet properties
+ const mergedLanguage = { ...langSource[0], ...language };
+ codegen.convert(
+ mergedLanguage.language,
+ mergedLanguage.variant,
+ cleanedPostmanRequest,
+ mergedLanguage.options,
+ (error: any, snippet: string) => {
+ if (error) {
+ return;
+ }
+ setCodeText(snippet);
+ },
+ );
+ } else {
+ setCodeText("");
+ }
+ }, [
+ accept,
+ body,
+ contentType,
+ cookieParams,
+ headerParams,
+ language,
+ pathParams,
+ postman,
+ queryParams,
+ server,
+ cleanedPostmanRequest,
+ mergedLangs,
+ ]);
+ // no dependencies was intentionally set for this particular hook. it's safe as long as if conditions are set
+ useEffect(function onSelectedVariantUpdate() {
+ if (selectedVariant && selectedVariant !== language?.variant) {
+ codegen.convert(
+ language.language,
+ selectedVariant,
+ cleanedPostmanRequest,
+ language.options,
+ (error: any, snippet: string) => {
+ if (error) {
+ return;
+ }
+ setCodeText(snippet);
+ },
+ );
+ }
+ });
+
+ // no dependencies was intentionally set for this particular hook. it's safe as long as if conditions are set
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ useEffect(function onSelectedSampleUpdate() {
+ if (
+ language &&
+ language.samples &&
+ language.samplesSources &&
+ selectedSample &&
+ selectedSample !== language.sample
+ ) {
+ const sampleIndex = language.samples.findIndex(
+ (smp) => smp === selectedSample,
+ );
+ setCodeSampleCodeText(language.samplesSources[sampleIndex]);
+ }
+ });
+
+ if (language === undefined) {
+ return null;
+ }
+
+ return (
+ <>
+ {/* Outer language tabs */}
+
+ {mergedLangs.map((lang) => {
+ return (
+
+ {/* Inner x-codeSamples tabs */}
+ {lang.samples && (
+
+ {lang.samples.map((sample, index) => {
+ return (
+
+ {/* @ts-ignore */}
+
+ {codeSampleCodeText}
+
+
+ );
+ })}
+
+ )}
+
+ {/* Inner generated code snippets */}
+
+ {lang.variants.map((variant, index) => {
+ return (
+
+ {/* @ts-ignore */}
+
+ {codeText}
+
+
+ );
+ })}
+
+
+ );
+ })}
+
+ >
+ );
+}
+
+export default CodeSnippets;
diff --git a/docs/src/theme/ApiExplorer/CodeSnippets/languages.json b/docs/src/theme/ApiExplorer/CodeSnippets/languages.json
new file mode 100644
index 00000000..36c345a3
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/CodeSnippets/languages.json
@@ -0,0 +1,1290 @@
+[
+ {
+ "key": "csharp",
+ "label": "C#",
+ "syntax_mode": "csharp",
+ "variants": [
+ {
+ "key": "RestSharp",
+ "options": [
+ {
+ "name": "Include boilerplate",
+ "id": "includeBoilerplate",
+ "type": "boolean",
+ "default": false,
+ "description": "Include class definition and import statements in snippet"
+ },
+ {
+ "name": "Set indentation count",
+ "id": "indentCount",
+ "type": "positiveInteger",
+ "default": 2,
+ "description": "Set the number of indentation characters to add per code level"
+ },
+ {
+ "name": "Set indentation type",
+ "id": "indentType",
+ "type": "enum",
+ "availableOptions": ["Tab", "Space"],
+ "default": "Space",
+ "description": "Select the character used to indent lines of code"
+ },
+ {
+ "name": "Set request timeout",
+ "id": "requestTimeout",
+ "type": "positiveInteger",
+ "default": 0,
+ "description": "Set number of milliseconds the request should wait for a response before timing out (use 0 for infinity)"
+ },
+ {
+ "name": "Follow redirects",
+ "id": "followRedirect",
+ "type": "boolean",
+ "default": true,
+ "description": "Automatically follow HTTP redirects"
+ },
+ {
+ "name": "Trim request body fields",
+ "id": "trimRequestBody",
+ "type": "boolean",
+ "default": false,
+ "description": "Remove white space and additional lines that may affect the server's response"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "key": "curl",
+ "label": "cURL",
+ "syntax_mode": "powershell",
+ "variants": [
+ {
+ "key": "cURL",
+ "options": [
+ {
+ "name": "Generate multiline snippet",
+ "id": "multiLine",
+ "type": "boolean",
+ "default": true,
+ "description": "Split cURL command across multiple lines"
+ },
+ {
+ "name": "Use long form options",
+ "id": "longFormat",
+ "type": "boolean",
+ "default": true,
+ "description": "Use the long form for cURL options (--header instead of -H)"
+ },
+ {
+ "name": "Line continuation character",
+ "id": "lineContinuationCharacter",
+ "availableOptions": ["\\", "^", "`"],
+ "type": "enum",
+ "default": "\\",
+ "description": "Set a character used to mark the continuation of a statement on the next line (generally, \\ for OSX/Linux, ^ for Windows cmd and ` for Powershell)"
+ },
+ {
+ "name": "Quote Type",
+ "id": "quoteType",
+ "availableOptions": ["single", "double"],
+ "type": "enum",
+ "default": "single",
+ "description": "String denoting the quote type to use (single or double) for URL (Use double quotes when running curl in cmd.exe and single quotes for the rest)"
+ },
+ {
+ "name": "Set request timeout (in seconds)",
+ "id": "requestTimeoutInSeconds",
+ "type": "positiveInteger",
+ "default": 0,
+ "description": "Set number of seconds the request should wait for a response before timing out (use 0 for infinity)"
+ },
+ {
+ "name": "Follow redirects",
+ "id": "followRedirect",
+ "type": "boolean",
+ "default": true,
+ "description": "Automatically follow HTTP redirects"
+ },
+ {
+ "name": "Trim request body fields",
+ "id": "trimRequestBody",
+ "type": "boolean",
+ "default": false,
+ "description": "Remove white space and additional lines that may affect the server's response"
+ },
+ {
+ "name": "Use Silent Mode",
+ "id": "silent",
+ "type": "boolean",
+ "default": false,
+ "description": "Display the requested data without showing the cURL progress meter or error messages"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "key": "dart",
+ "label": "Dart",
+ "syntax_mode": "dart",
+ "variants": [
+ {
+ "key": "http",
+ "options": [
+ {
+ "name": "Set indentation count",
+ "id": "indentCount",
+ "type": "positiveInteger",
+ "default": 2,
+ "description": "Set the number of indentation characters to add per code level"
+ },
+ {
+ "name": "Set indentation type",
+ "id": "indentType",
+ "type": "enum",
+ "availableOptions": ["Tab", "Space"],
+ "default": "Space",
+ "description": "Select the character used to indent lines of code"
+ },
+ {
+ "name": "Set request timeout",
+ "id": "requestTimeout",
+ "type": "positiveInteger",
+ "default": 0,
+ "description": "Set number of milliseconds the request should wait for a response before timing out (use 0 for infinity)"
+ },
+ {
+ "name": "Trim request body fields",
+ "id": "trimRequestBody",
+ "type": "boolean",
+ "default": false,
+ "description": "Remove white space and additional lines that may affect the server's response"
+ },
+ {
+ "name": "Include boilerplate",
+ "id": "includeBoilerplate",
+ "type": "boolean",
+ "default": false,
+ "description": "Include class definition and import statements in snippet"
+ },
+ {
+ "name": "Follow redirects",
+ "id": "followRedirect",
+ "type": "boolean",
+ "default": true,
+ "description": "Automatically follow HTTP redirects"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "key": "go",
+ "label": "Go",
+ "syntax_mode": "golang",
+ "variants": [
+ {
+ "key": "Native",
+ "options": [
+ {
+ "name": "Set indentation count",
+ "id": "indentCount",
+ "type": "positiveInteger",
+ "default": 2,
+ "description": "Set the number of indentation characters to add per code level"
+ },
+ {
+ "name": "Set indentation type",
+ "id": "indentType",
+ "type": "enum",
+ "availableOptions": ["Tab", "Space"],
+ "default": "Space",
+ "description": "Select the character used to indent lines of code"
+ },
+ {
+ "name": "Set request timeout",
+ "id": "requestTimeout",
+ "type": "positiveInteger",
+ "default": 0,
+ "description": "Set number of milliseconds the request should wait for a response before timing out (use 0 for infinity)"
+ },
+ {
+ "name": "Follow redirects",
+ "id": "followRedirect",
+ "type": "boolean",
+ "default": true,
+ "description": "Automatically follow HTTP redirects"
+ },
+ {
+ "name": "Trim request body fields",
+ "id": "trimRequestBody",
+ "type": "boolean",
+ "default": false,
+ "description": "Remove white space and additional lines that may affect the server's response"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "key": "http",
+ "label": "HTTP",
+ "syntax_mode": "text",
+ "variants": [
+ {
+ "key": "HTTP",
+ "options": [
+ {
+ "name": "Trim request body fields",
+ "id": "trimRequestBody",
+ "type": "boolean",
+ "default": false,
+ "description": "Remove white space and additional lines that may affect the server's response"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "key": "java",
+ "label": "Java",
+ "syntax_mode": "java",
+ "variants": [
+ {
+ "key": "OkHttp",
+ "options": [
+ {
+ "name": "Include boilerplate",
+ "id": "includeBoilerplate",
+ "type": "boolean",
+ "default": false,
+ "description": "Include class definition and import statements in snippet"
+ },
+ {
+ "name": "Set indentation count",
+ "id": "indentCount",
+ "type": "positiveInteger",
+ "default": 2,
+ "description": "Set the number of indentation characters to add per code level"
+ },
+ {
+ "name": "Set indentation type",
+ "id": "indentType",
+ "type": "enum",
+ "availableOptions": ["Tab", "Space"],
+ "default": "Space",
+ "description": "Select the character used to indent lines of code"
+ },
+ {
+ "name": "Set request timeout",
+ "id": "requestTimeout",
+ "type": "positiveInteger",
+ "default": 0,
+ "description": "Set number of milliseconds the request should wait for a response before timing out (use 0 for infinity)"
+ },
+ {
+ "name": "Follow redirects",
+ "id": "followRedirect",
+ "type": "boolean",
+ "default": true,
+ "description": "Automatically follow HTTP redirects"
+ },
+ {
+ "name": "Trim request body fields",
+ "id": "trimRequestBody",
+ "type": "boolean",
+ "default": false,
+ "description": "Remove white space and additional lines that may affect the server's response"
+ }
+ ]
+ },
+ {
+ "key": "Unirest",
+ "options": [
+ {
+ "name": "Include boilerplate",
+ "id": "includeBoilerplate",
+ "type": "boolean",
+ "default": false,
+ "description": "Include class definition and import statements in snippet"
+ },
+ {
+ "name": "Set indentation count",
+ "id": "indentCount",
+ "type": "positiveInteger",
+ "default": 2,
+ "description": "Set the number of indentation characters to add per code level"
+ },
+ {
+ "name": "Set indentation type",
+ "id": "indentType",
+ "type": "enum",
+ "availableOptions": ["Tab", "Space"],
+ "default": "Space",
+ "description": "Select the character used to indent lines of code"
+ },
+ {
+ "name": "Set request timeout",
+ "id": "requestTimeout",
+ "type": "positiveInteger",
+ "default": 0,
+ "description": "Set number of milliseconds the request should wait for a response before timing out (use 0 for infinity)"
+ },
+ {
+ "name": "Follow redirects",
+ "id": "followRedirect",
+ "type": "boolean",
+ "default": true,
+ "description": "Automatically follow HTTP redirects"
+ },
+ {
+ "name": "Trim request body fields",
+ "id": "trimRequestBody",
+ "type": "boolean",
+ "default": false,
+ "description": "Remove white space and additional lines that may affect the server's response"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "key": "javascript",
+ "label": "JavaScript",
+ "syntax_mode": "javascript",
+ "variants": [
+ {
+ "key": "Fetch",
+ "options": [
+ {
+ "name": "Set indentation count",
+ "id": "indentCount",
+ "type": "positiveInteger",
+ "default": 2,
+ "description": "Set the number of indentation characters to add per code level"
+ },
+ {
+ "name": "Set indentation type",
+ "id": "indentType",
+ "type": "enum",
+ "availableOptions": ["Tab", "Space"],
+ "default": "Space",
+ "description": "Select the character used to indent lines of code"
+ },
+ {
+ "name": "Set request timeout",
+ "id": "requestTimeout",
+ "type": "positiveInteger",
+ "default": 0,
+ "description": "Set number of milliseconds the request should wait for a response before timing out (use 0 for infinity)"
+ },
+ {
+ "name": "Follow redirects",
+ "id": "followRedirect",
+ "type": "boolean",
+ "default": true,
+ "description": "Automatically follow HTTP redirects"
+ },
+ {
+ "name": "Trim request body fields",
+ "id": "trimRequestBody",
+ "type": "boolean",
+ "default": false,
+ "description": "Remove white space and additional lines that may affect the server's response"
+ }
+ ]
+ },
+ {
+ "key": "jQuery",
+ "options": [
+ {
+ "name": "Set indentation count",
+ "id": "indentCount",
+ "type": "positiveInteger",
+ "default": 2,
+ "description": "Set the number of indentation characters to add per code level"
+ },
+ {
+ "name": "Set indentation type",
+ "id": "indentType",
+ "type": "enum",
+ "availableOptions": ["Tab", "Space"],
+ "default": "Space",
+ "description": "Select the character used to indent lines of code"
+ },
+ {
+ "name": "Set request timeout",
+ "id": "requestTimeout",
+ "type": "positiveInteger",
+ "default": 0,
+ "description": "Set number of milliseconds the request should wait for a response before timing out (use 0 for infinity)"
+ },
+ {
+ "name": "Trim request body fields",
+ "id": "trimRequestBody",
+ "type": "boolean",
+ "default": false,
+ "description": "Remove white space and additional lines that may affect the server's response"
+ }
+ ]
+ },
+ {
+ "key": "XHR",
+ "options": [
+ {
+ "name": "Set indentation count",
+ "id": "indentCount",
+ "type": "positiveInteger",
+ "default": 2,
+ "description": "Set the number of indentation characters to add per code level"
+ },
+ {
+ "name": "Set indentation type",
+ "id": "indentType",
+ "type": "enum",
+ "availableOptions": ["Tab", "Space"],
+ "default": "Space",
+ "description": "Select the character used to indent lines of code"
+ },
+ {
+ "name": "Set request timeout",
+ "id": "requestTimeout",
+ "type": "positiveInteger",
+ "default": 0,
+ "description": "Set number of milliseconds the request should wait for a response before timing out (use 0 for infinity)"
+ },
+ {
+ "name": "Trim request body fields",
+ "id": "trimRequestBody",
+ "type": "boolean",
+ "default": false,
+ "description": "Remove white space and additional lines that may affect the server's response"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "key": "c",
+ "label": "C",
+ "syntax_mode": "c_cpp",
+ "variants": [
+ {
+ "key": "libcurl",
+ "options": [
+ {
+ "name": "Include boilerplate",
+ "id": "includeBoilerplate",
+ "type": "boolean",
+ "default": false,
+ "description": "Include class definition and import statements in snippet"
+ },
+ {
+ "name": "Protocol",
+ "id": "protocol",
+ "type": "enum",
+ "availableOptions": ["http", "https"],
+ "default": "https",
+ "description": "The protocol to be used to make the request"
+ },
+ {
+ "name": "Set indentation count",
+ "id": "indentCount",
+ "type": "positiveInteger",
+ "default": 2,
+ "description": "Set the number of indentation characters to add per code level"
+ },
+ {
+ "name": "Set indentation type",
+ "id": "indentType",
+ "type": "enum",
+ "availableOptions": ["Tab", "Space"],
+ "default": "Space",
+ "description": "Select the character used to indent lines of code"
+ },
+ {
+ "name": "Follow redirects",
+ "id": "followRedirect",
+ "type": "boolean",
+ "default": true,
+ "description": "Automatically follow HTTP redirects"
+ },
+ {
+ "name": "Trim request body fields",
+ "id": "trimRequestBody",
+ "type": "boolean",
+ "default": false,
+ "description": "Remove white space and additional lines that may affect the server's response"
+ },
+ {
+ "name": "Use curl_mime",
+ "id": "useMimeType",
+ "type": "boolean",
+ "default": true,
+ "description": "Use curl_mime to send multipart/form-data requests"
+ },
+ {
+ "name": "Set request timeout",
+ "id": "requestTimeout",
+ "type": "positiveInteger",
+ "default": 0,
+ "description": "Set number of milliseconds the request should wait for a response before timing out (use 0 for infinity)"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "key": "nodejs",
+ "label": "NodeJs",
+ "syntax_mode": "javascript",
+ "variants": [
+ {
+ "key": "Axios",
+ "options": [
+ {
+ "name": "Set indentation count",
+ "id": "indentCount",
+ "type": "positiveInteger",
+ "default": 2,
+ "description": "Set the number of indentation characters to add per code level"
+ },
+ {
+ "name": "Set indentation type",
+ "id": "indentType",
+ "type": "enum",
+ "availableOptions": ["Tab", "Space"],
+ "default": "Space",
+ "description": "Select the character used to indent lines of code"
+ },
+ {
+ "name": "Set request timeout",
+ "id": "requestTimeout",
+ "type": "positiveInteger",
+ "default": 0,
+ "description": "Set number of milliseconds the request should wait for a response before timing out (use 0 for infinity)"
+ },
+ {
+ "name": "Follow redirects",
+ "id": "followRedirect",
+ "type": "boolean",
+ "default": true,
+ "description": "Automatically follow HTTP redirects"
+ },
+ {
+ "name": "Trim request body fields",
+ "id": "trimRequestBody",
+ "type": "boolean",
+ "default": false,
+ "description": "Remove white space and additional lines that may affect the server's response"
+ },
+ {
+ "name": "Enable ES6 features",
+ "id": "ES6_enabled",
+ "type": "boolean",
+ "default": false,
+ "description": "Modifies code snippet to incorporate ES6 (EcmaScript) features"
+ }
+ ]
+ },
+ {
+ "key": "Native",
+ "options": [
+ {
+ "name": "Set indentation count",
+ "id": "indentCount",
+ "type": "positiveInteger",
+ "default": 2,
+ "description": "Set the number of indentation characters to add per code level"
+ },
+ {
+ "name": "Set indentation type",
+ "id": "indentType",
+ "type": "enum",
+ "availableOptions": ["Tab", "Space"],
+ "default": "Space",
+ "description": "Select the character used to indent lines of code"
+ },
+ {
+ "name": "Set request timeout",
+ "id": "requestTimeout",
+ "type": "positiveInteger",
+ "default": 0,
+ "description": "Set number of milliseconds the request should wait for a response before timing out (use 0 for infinity)"
+ },
+ {
+ "name": "Follow redirects",
+ "id": "followRedirect",
+ "type": "boolean",
+ "default": true,
+ "description": "Automatically follow HTTP redirects"
+ },
+ {
+ "name": "Trim request body fields",
+ "id": "trimRequestBody",
+ "type": "boolean",
+ "default": false,
+ "description": "Remove white space and additional lines that may affect the server's response"
+ },
+ {
+ "name": "Enable ES6 features",
+ "id": "ES6_enabled",
+ "type": "boolean",
+ "default": false,
+ "description": "Modifies code snippet to incorporate ES6 (EcmaScript) features"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "key": "objective-c",
+ "label": "Objective-C",
+ "syntax_mode": "objectivec",
+ "variants": [
+ {
+ "key": "NSURLSession",
+ "options": [
+ {
+ "name": "Set indentation count",
+ "id": "indentCount",
+ "type": "positiveInteger",
+ "default": 2,
+ "description": "Set the number of indentation characters to add per code level"
+ },
+ {
+ "name": "Set indentation type",
+ "id": "indentType",
+ "type": "enum",
+ "availableOptions": ["Tab", "Space"],
+ "default": "Space",
+ "description": "Select the character used to indent lines of code"
+ },
+ {
+ "name": "Set request timeout",
+ "id": "requestTimeout",
+ "type": "positiveInteger",
+ "default": 10000,
+ "description": "Set number of milliseconds the request should wait for a response before timing out (use 0 for infinity)"
+ },
+ {
+ "name": "Trim request body fields",
+ "id": "trimRequestBody",
+ "type": "boolean",
+ "default": false,
+ "description": "Remove white space and additional lines that may affect the server's response"
+ },
+ {
+ "name": "Include boilerplate",
+ "id": "includeBoilerplate",
+ "type": "boolean",
+ "default": false,
+ "description": "Include class definition and import statements in snippet"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "key": "ocaml",
+ "label": "OCaml",
+ "syntax_mode": "ocaml",
+ "variants": [
+ {
+ "key": "Cohttp",
+ "options": [
+ {
+ "name": "Set indentation count",
+ "id": "indentCount",
+ "type": "positiveInteger",
+ "default": 2,
+ "description": "Set the number of indentation characters to add per code level"
+ },
+ {
+ "name": "Set indentation type",
+ "id": "indentType",
+ "type": "enum",
+ "availableOptions": ["Tab", "Space"],
+ "default": "Space",
+ "description": "Select the character used to indent lines of code"
+ },
+ {
+ "name": "Trim request body fields",
+ "id": "trimRequestBody",
+ "type": "boolean",
+ "default": false,
+ "description": "Remove white space and additional lines that may affect the server's response"
+ },
+ {
+ "name": "Set request timeout",
+ "id": "requestTimeout",
+ "type": "positiveInteger",
+ "default": 0,
+ "description": "Set number of milliseconds the request should wait for a response before timing out (use 0 for infinity)"
+ },
+ {
+ "name": "Follow redirects",
+ "id": "followRedirect",
+ "type": "boolean",
+ "default": true,
+ "description": "Automatically follow HTTP redirects"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "key": "php",
+ "label": "PHP",
+ "syntax_mode": "php",
+ "variants": [
+ {
+ "key": "cURL",
+ "options": [
+ {
+ "name": "Set indentation count",
+ "id": "indentCount",
+ "type": "positiveInteger",
+ "default": 2,
+ "description": "Set the number of indentation characters to add per code level"
+ },
+ {
+ "name": "Set indentation type",
+ "id": "indentType",
+ "type": "enum",
+ "availableOptions": ["Tab", "Space"],
+ "default": "Space",
+ "description": "Select the character used to indent lines of code"
+ },
+ {
+ "name": "Set request timeout",
+ "id": "requestTimeout",
+ "type": "positiveInteger",
+ "default": 0,
+ "description": "Set number of milliseconds the request should wait for a response before timing out (use 0 for infinity)"
+ },
+ {
+ "name": "Follow redirects",
+ "id": "followRedirect",
+ "type": "boolean",
+ "default": true,
+ "description": "Automatically follow HTTP redirects"
+ },
+ {
+ "name": "Trim request body fields",
+ "id": "trimRequestBody",
+ "type": "boolean",
+ "default": false,
+ "description": "Remove white space and additional lines that may affect the server's response"
+ }
+ ]
+ },
+ {
+ "key": "Guzzle",
+ "options": [
+ {
+ "name": "Set indentation count",
+ "id": "indentCount",
+ "type": "positiveInteger",
+ "default": 2,
+ "description": "Set the number of indentation characters to add per code level"
+ },
+ {
+ "name": "Set indentation type",
+ "id": "indentType",
+ "type": "enum",
+ "availableOptions": ["Tab", "Space"],
+ "default": "Space",
+ "description": "Select the character used to indent lines of code"
+ },
+ {
+ "name": "Trim request body fields",
+ "id": "trimRequestBody",
+ "type": "boolean",
+ "default": false,
+ "description": "Remove white space and additional lines that may affect the server's response"
+ },
+ {
+ "name": "Set request timeout",
+ "id": "requestTimeout",
+ "type": "positiveInteger",
+ "default": 0,
+ "description": "Set number of milliseconds the request should wait for a response before timing out (use 0 for infinity)"
+ },
+ {
+ "name": "Follow redirects",
+ "id": "followRedirect",
+ "type": "boolean",
+ "default": true,
+ "description": "Automatically follow HTTP redirects"
+ },
+ {
+ "name": "Set communication type",
+ "id": "asyncType",
+ "type": "enum",
+ "availableOptions": ["async", "sync"],
+ "default": "async",
+ "description": "Set if the requests will be asynchronous or synchronous"
+ },
+ {
+ "name": "Include boilerplate",
+ "id": "includeBoilerplate",
+ "type": "boolean",
+ "default": false,
+ "description": "Include class definition and import statements in snippet"
+ }
+ ]
+ },
+ {
+ "key": "HTTP_Request2",
+ "options": [
+ {
+ "name": "Set indentation count",
+ "id": "indentCount",
+ "type": "positiveInteger",
+ "default": 2,
+ "description": "Set the number of indentation characters to add per code level"
+ },
+ {
+ "name": "Set indentation type",
+ "id": "indentType",
+ "type": "enum",
+ "default": "Space",
+ "availableOptions": ["Tab", "Space"],
+ "description": "Select the character used to indent lines of code"
+ },
+ {
+ "name": "Set request timeout",
+ "id": "requestTimeout",
+ "type": "positiveInteger",
+ "default": 0,
+ "description": "Set number of milliseconds the request should wait for a response before timing out (use 0 for infinity)"
+ },
+ {
+ "name": "Trim request body fields",
+ "id": "trimRequestBody",
+ "type": "boolean",
+ "default": false,
+ "description": "Remove white space and additional lines that may affect the server's response"
+ },
+ {
+ "name": "Follow redirects",
+ "id": "followRedirect",
+ "type": "boolean",
+ "default": true,
+ "description": "Automatically follow HTTP redirects"
+ }
+ ]
+ },
+ {
+ "key": "pecl_http",
+ "options": [
+ {
+ "name": "Set indentation count",
+ "id": "indentCount",
+ "type": "positiveInteger",
+ "default": 2,
+ "description": "Set the number of indentation characters to add per code level"
+ },
+ {
+ "name": "Set indentation type",
+ "id": "indentType",
+ "type": "enum",
+ "default": "Space",
+ "availableOptions": ["Tab", "Space"],
+ "description": "Select the character used to indent lines of code"
+ },
+ {
+ "name": "Set request timeout",
+ "id": "requestTimeout",
+ "type": "positiveInteger",
+ "default": 0,
+ "description": "Set number of milliseconds the request should wait for a response before timing out (use 0 for infinity)"
+ },
+ {
+ "name": "Trim request body fields",
+ "id": "trimRequestBody",
+ "type": "boolean",
+ "default": false,
+ "description": "Remove white space and additional lines that may affect the server's response"
+ },
+ {
+ "name": "Follow redirects",
+ "id": "followRedirect",
+ "type": "boolean",
+ "default": true,
+ "description": "Automatically follow HTTP redirects"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "key": "powershell",
+ "label": "PowerShell",
+ "syntax_mode": "powershell",
+ "variants": [
+ {
+ "key": "RestMethod",
+ "options": [
+ {
+ "name": "Set request timeout",
+ "id": "requestTimeout",
+ "type": "positiveInteger",
+ "default": 0,
+ "description": "Set number of milliseconds the request should wait for a response before timing out (use 0 for infinity)"
+ },
+ {
+ "name": "Follow redirects",
+ "id": "followRedirect",
+ "type": "boolean",
+ "default": true,
+ "description": "Automatically follow HTTP redirects"
+ },
+ {
+ "name": "Trim request body fields",
+ "id": "trimRequestBody",
+ "type": "boolean",
+ "default": false,
+ "description": "Remove white space and additional lines that may affect the server's response"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "key": "python",
+ "label": "Python",
+ "syntax_mode": "python",
+ "variants": [
+ {
+ "key": "http.client",
+ "options": [
+ {
+ "name": "Set indentation count",
+ "id": "indentCount",
+ "type": "positiveInteger",
+ "default": 2,
+ "description": "Set the number of indentation characters to add per code level"
+ },
+ {
+ "name": "Set indentation type",
+ "id": "indentType",
+ "type": "enum",
+ "default": "Space",
+ "availableOptions": ["Tab", "Space"],
+ "description": "Select the character used to indent lines of code"
+ },
+ {
+ "name": "Set request timeout",
+ "id": "requestTimeout",
+ "type": "positiveInteger",
+ "default": 0,
+ "description": "Set number of milliseconds the request should wait for a response before timing out (use 0 for infinity)"
+ },
+ {
+ "name": "Trim request body fields",
+ "id": "trimRequestBody",
+ "type": "boolean",
+ "default": false,
+ "description": "Remove white space and additional lines that may affect the server's response"
+ }
+ ]
+ },
+ {
+ "key": "Requests",
+ "options": [
+ {
+ "name": "Set indentation count",
+ "id": "indentCount",
+ "type": "positiveInteger",
+ "default": 2,
+ "description": "Set the number of indentation characters to add per code level"
+ },
+ {
+ "name": "Set indentation type",
+ "id": "indentType",
+ "type": "enum",
+ "availableOptions": ["Tab", "Space"],
+ "default": "Space",
+ "description": "Select the character used to indent lines of code"
+ },
+ {
+ "name": "Set request timeout",
+ "id": "requestTimeout",
+ "type": "positiveInteger",
+ "default": 0,
+ "description": "Set number of milliseconds the request should wait for a response before timing out (use 0 for infinity)"
+ },
+ {
+ "name": "Follow redirects",
+ "id": "followRedirect",
+ "type": "boolean",
+ "default": true,
+ "description": "Automatically follow HTTP redirects"
+ },
+ {
+ "name": "Trim request body fields",
+ "id": "trimRequestBody",
+ "type": "boolean",
+ "default": false,
+ "description": "Remove white space and additional lines that may affect the server's response"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "key": "r",
+ "label": "R",
+ "syntax_mode": "r",
+ "variants": [
+ {
+ "key": "httr",
+ "options": [
+ {
+ "name": "Set indentation count",
+ "id": "indentCount",
+ "type": "positiveInteger",
+ "default": 2,
+ "description": "Set the number of indentation characters to add per code level"
+ },
+ {
+ "name": "Set indentation type",
+ "id": "indentType",
+ "type": "enum",
+ "availableOptions": ["Tab", "Space"],
+ "default": "Space",
+ "description": "Select the character used to indent lines of code"
+ },
+ {
+ "name": "Trim request body fields",
+ "id": "trimRequestBody",
+ "type": "boolean",
+ "default": false,
+ "description": "Remove white space and additional lines that may affect the server's response"
+ },
+ {
+ "name": "Set request timeout",
+ "id": "requestTimeout",
+ "type": "positiveInteger",
+ "default": 0,
+ "description": "Set number of milliseconds the request should wait for a response before timing out (use 0 for infinity)"
+ }
+ ]
+ },
+ {
+ "key": "RCurl",
+ "options": [
+ {
+ "name": "Set indentation count",
+ "id": "indentCount",
+ "type": "positiveInteger",
+ "default": 2,
+ "description": "Set the number of indentation characters to add per code level"
+ },
+ {
+ "name": "Set indentation type",
+ "id": "indentType",
+ "type": "enum",
+ "availableOptions": ["Tab", "Space"],
+ "default": "Space",
+ "description": "Select the character used to indent lines of code"
+ },
+ {
+ "name": "Trim request body fields",
+ "id": "trimRequestBody",
+ "type": "boolean",
+ "default": false,
+ "description": "Remove white space and additional lines that may affect the server's response"
+ },
+ {
+ "name": "Set request timeout",
+ "id": "requestTimeout",
+ "type": "positiveInteger",
+ "default": 0,
+ "description": "Set number of milliseconds the request should wait for a response before timing out (use 0 for infinity)"
+ },
+ {
+ "name": "Follow redirects",
+ "id": "followRedirect",
+ "type": "boolean",
+ "default": true,
+ "description": "Automatically follow HTTP redirects"
+ },
+ {
+ "name": "Ignore warnings",
+ "id": "ignoreWarnings",
+ "type": "boolean",
+ "default": false,
+ "description": "Ignore warnings from R"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "key": "ruby",
+ "label": "Ruby",
+ "syntax_mode": "ruby",
+ "variants": [
+ {
+ "key": "Net::HTTP",
+ "options": [
+ {
+ "name": "Set indentation count",
+ "id": "indentCount",
+ "type": "positiveInteger",
+ "default": 2,
+ "description": "Set the number of indentation characters to add per code level"
+ },
+ {
+ "name": "Set indentation type",
+ "id": "indentType",
+ "type": "enum",
+ "availableOptions": ["Tab", "Space"],
+ "default": "Space",
+ "description": "Select the character used to indent lines of code"
+ },
+ {
+ "name": "Set request timeout",
+ "id": "requestTimeout",
+ "type": "positiveInteger",
+ "default": 0,
+ "description": "Set number of milliseconds the request should wait for a response before timing out (use 0 for infinity)"
+ },
+ {
+ "name": "Follow redirects",
+ "id": "followRedirect",
+ "type": "boolean",
+ "default": true,
+ "description": "Automatically follow HTTP redirects"
+ },
+ {
+ "name": "Trim request body fields",
+ "id": "trimRequestBody",
+ "type": "boolean",
+ "default": false,
+ "description": "Remove white space and additional lines that may affect the server's response"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "key": "shell",
+ "label": "Shell",
+ "syntax_mode": "powershell",
+ "variants": [
+ {
+ "key": "Httpie",
+ "options": [
+ {
+ "name": "Set request timeout",
+ "id": "requestTimeout",
+ "type": "positiveInteger",
+ "default": 0,
+ "description": "Set number of milliseconds the request should wait for a response before timing out (use 0 for infinity)"
+ },
+ {
+ "name": "Follow redirects",
+ "id": "followRedirect",
+ "type": "boolean",
+ "default": true,
+ "description": "Automatically follow HTTP redirects"
+ }
+ ]
+ },
+ {
+ "key": "wget",
+ "options": [
+ {
+ "name": "Set indentation count",
+ "id": "indentCount",
+ "type": "positiveInteger",
+ "default": 2,
+ "description": "Set the number of indentation characters to add per code level"
+ },
+ {
+ "name": "Set indentation type",
+ "id": "indentType",
+ "type": "enum",
+ "availableOptions": ["Tab", "Space"],
+ "default": "Space",
+ "description": "Select the character used to indent lines of code"
+ },
+ {
+ "name": "Set request timeout",
+ "id": "requestTimeout",
+ "type": "positiveInteger",
+ "default": 0,
+ "description": "Set number of milliseconds the request should wait for a response before timing out (use 0 for infinity)"
+ },
+ {
+ "name": "Follow redirects",
+ "id": "followRedirect",
+ "type": "boolean",
+ "default": true,
+ "description": "Automatically follow HTTP redirects"
+ },
+ {
+ "name": "Trim request body fields",
+ "id": "trimRequestBody",
+ "type": "boolean",
+ "default": false,
+ "description": "Remove white space and additional lines that may affect the server's response"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "key": "swift",
+ "label": "Swift",
+ "syntax_mode": "swift",
+ "variants": [
+ {
+ "key": "URLSession",
+ "options": [
+ {
+ "name": "Set indentation count",
+ "id": "indentCount",
+ "type": "positiveInteger",
+ "default": 2,
+ "description": "Set the number of indentation characters to add per code level"
+ },
+ {
+ "name": "Set indentation type",
+ "id": "indentType",
+ "type": "enum",
+ "availableOptions": ["Tab", "Space"],
+ "default": "Space",
+ "description": "Select the character used to indent lines of code"
+ },
+ {
+ "name": "Set request timeout",
+ "id": "requestTimeout",
+ "type": "positiveInteger",
+ "default": 0,
+ "description": "Set number of milliseconds the request should wait for a response before timing out (use 0 for infinity)"
+ },
+ {
+ "name": "Trim request body fields",
+ "id": "trimRequestBody",
+ "type": "boolean",
+ "default": false,
+ "description": "Remove white space and additional lines that may affect the server's response"
+ },
+ {
+ "name": "Follow redirects",
+ "id": "followRedirect",
+ "type": "boolean",
+ "default": true,
+ "description": "Automatically follow HTTP redirects"
+ }
+ ]
+ }
+ ]
+ }
+]
diff --git a/docs/src/theme/ApiExplorer/CodeSnippets/languages.ts b/docs/src/theme/ApiExplorer/CodeSnippets/languages.ts
new file mode 100644
index 00000000..2cc25fc2
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/CodeSnippets/languages.ts
@@ -0,0 +1,89 @@
+import find from "lodash/find";
+import mergeWith from "lodash/mergeWith";
+import unionBy from "lodash/unionBy";
+import codegen from "postman-code-generators";
+
+import { CodeSample, Language } from "./code-snippets-types";
+
+export function mergeCodeSampleLanguage(
+ languages: Language[],
+ codeSamples: CodeSample[],
+): Language[] {
+ return languages.map((language) => {
+ const languageCodeSamples = codeSamples.filter(
+ ({ lang }) => lang === language.codeSampleLanguage,
+ );
+
+ if (languageCodeSamples.length) {
+ const samples = languageCodeSamples.map(({ lang }) => lang);
+ const samplesLabels = languageCodeSamples.map(
+ ({ label, lang }) => label || lang,
+ );
+ const samplesSources = languageCodeSamples.map(({ source }) => source);
+
+ return {
+ ...language,
+ sample: samples[0],
+ samples,
+ samplesSources,
+ samplesLabels,
+ };
+ }
+
+ return language;
+ });
+}
+
+export const mergeArraysbyLanguage = (arr1: any, arr2: any) => {
+ const mergedArray = unionBy(arr1, arr2, "language");
+
+ return mergedArray.map((item: any) => {
+ const matchingItems = [
+ find(arr1, ["language", item["language"]]),
+ find(arr2, ["language", item["language"]]),
+ ];
+ return mergeWith({}, ...matchingItems, (objValue: any) => {
+ return objValue;
+ });
+ });
+};
+
+export function getCodeSampleSourceFromLanguage(language: Language) {
+ if (
+ language &&
+ language.sample &&
+ language.samples &&
+ language.samplesSources
+ ) {
+ const sampleIndex = language.samples.findIndex(
+ (smp) => smp === language.sample,
+ );
+ return language.samplesSources[sampleIndex];
+ }
+
+ return "";
+}
+
+export function generateLanguageSet() {
+ const languageSet: Language[] = [];
+ codegen.getLanguageList().forEach((language: any) => {
+ const variants: any = [];
+ language.variants.forEach((variant: any) => {
+ variants.push(variant.key);
+ });
+ languageSet.push({
+ highlight: language.syntax_mode,
+ language: language.key,
+ codeSampleLanguage: language.label,
+ logoClass: language.key,
+ options: {
+ longFormat: false,
+ followRedirect: true,
+ trimRequestBody: true,
+ },
+ variant: variants[0],
+ variants: variants,
+ });
+ });
+ return languageSet;
+}
diff --git a/docs/src/theme/ApiExplorer/CodeTabs/_CodeTabs.scss b/docs/src/theme/ApiExplorer/CodeTabs/_CodeTabs.scss
new file mode 100644
index 00000000..fd2c84e1
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/CodeTabs/_CodeTabs.scss
@@ -0,0 +1,501 @@
+:root {
+ --bash-background-color: transparent;
+ --bash-border-radius: none;
+ --code-tab-logo-width: 24px;
+ --code-tab-logo-height: 24px;
+}
+
+[data-theme="dark"] {
+ --bash-background-color: lightgrey;
+ --bash-border-radius: 20px;
+}
+
+.openapi-tabs__code-container {
+ margin-bottom: 1rem;
+
+ &:not(.openapi-tabs__code-container-inner) {
+ padding: 1rem;
+ background-color: var(--ifm-pre-background);
+ border-radius: var(--ifm-global-radius);
+ border: 1px solid var(--openapi-explorer-border-color);
+ box-shadow:
+ 0 2px 3px hsla(222, 8%, 43%, 0.1),
+ 0 8px 16px -10px hsla(222, 8%, 43%, 0.2);
+ transition: 300ms;
+
+ &:hover {
+ box-shadow:
+ 0 0 0 2px rgba(38, 53, 61, 0.15),
+ 0 2px 3px hsla(222, 8%, 43%, 0.15),
+ 0 16px 16px -10px hsla(222, 8%, 43%, 0.2);
+ }
+ }
+
+ .openapi-tabs__code-item {
+ display: flex;
+ flex-direction: column-reverse;
+ flex: 0 0 80px;
+ align-items: center;
+ padding: 0.5rem 0 !important;
+ margin-top: 0 !important;
+ margin-right: 0.5rem;
+ border: 1px solid transparent;
+ transition: 300ms;
+
+ &:not(.active):hover {
+ border: 1px solid var(--openapi-code-tab-border-color);
+ }
+
+ &:hover {
+ background-color: transparent;
+ }
+
+ span {
+ padding-top: 0.5rem;
+ color: var(--ifm-font-color-secondary);
+ font-size: 10px;
+ text-transform: uppercase;
+ }
+ }
+}
+
+.openapi-tabs__code-list-container {
+ display: flex;
+ justify-content: flex-start;
+ padding: 0.25rem;
+ padding-bottom: 0.6rem;
+}
+
+.openapi-tabs__code-content {
+ margin-top: unset !important;
+}
+
+.openapi-explorer__code-block code {
+ max-height: 200px;
+ font-size: var(--openapi-explorer-font-size-code);
+ padding-top: var(--ifm-pre-padding);
+}
+
+body[class="ReactModal__Body--open"] {
+ .openapi-explorer__code-block code {
+ max-height: 600px;
+ }
+}
+
+.openapi-tabs__code-item--variant {
+ color: var(--ifm-color-secondary);
+
+ &.active {
+ border-color: var(--ifm-toc-border-color);
+ }
+}
+
+.openapi-tabs__code-item--variant > span {
+ padding-top: unset !important;
+ padding-left: 0.5rem !important;
+ padding-right: 0.5rem !important;
+}
+
+.openapi-tabs__code-item--sample {
+ color: var(--ifm-color-secondary);
+
+ &.active {
+ border-color: var(--ifm-toc-border-color);
+ }
+}
+
+.openapi-tabs__code-item--sample > span {
+ padding-top: unset !important;
+ padding-left: 0.5rem !important;
+ padding-right: 0.5rem !important;
+}
+
+.openapi-tabs__code-item--python {
+ color: var(--ifm-color-success);
+
+ &::after {
+ content: "";
+ width: var(--code-tab-logo-width);
+ height: var(--code-tab-logo-height);
+ background: url("https://raw.githubusercontent.com/devicons/devicon/master/icons/python/python-original.svg")
+ no-repeat;
+ margin-block: auto;
+ }
+
+ &.active {
+ box-shadow: 0 0 0 3px var(--openapi-code-tab-shadow-color-python);
+ border-color: var(--openapi-code-tab-border-color-python);
+ }
+}
+
+.openapi-tabs__code-item--go {
+ color: var(--ifm-color-info);
+
+ &::after {
+ content: "";
+ width: var(--code-tab-logo-width);
+ height: var(--code-tab-logo-height);
+ background: url("https://raw.githubusercontent.com/devicons/devicon/master/icons/go/go-original-wordmark.svg")
+ no-repeat;
+ margin-block: auto;
+ }
+
+ &.active {
+ box-shadow: 0 0 0 3px var(--openapi-code-tab-shadow-color-go);
+ border-color: var(--openapi-code-tab-border-color-go);
+ }
+}
+
+.openapi-tabs__code-item--dart {
+ color: var(--ifm-color-info);
+
+ &::after {
+ content: "";
+ width: var(--code-tab-logo-width);
+ height: var(--code-tab-logo-height);
+ background: url("https://raw.githubusercontent.com/devicons/devicon/master/icons/dart/dart-original.svg")
+ no-repeat;
+ margin-block: auto;
+ }
+
+ &.active {
+ box-shadow: 0 0 0 3px var(--openapi-code-tab-shadow-color-dart);
+ border-color: var(--openapi-code-tab-border-color-dart);
+ }
+}
+
+.openapi-tabs__code-item--javascript {
+ color: var(--ifm-color-warning);
+
+ &::after {
+ content: "";
+ width: var(--code-tab-logo-width);
+ height: var(--code-tab-logo-height);
+ background: url("https://raw.githubusercontent.com/devicons/devicon/master/icons/javascript/javascript-original.svg")
+ no-repeat;
+ margin-block: auto;
+ }
+
+ &.active {
+ box-shadow: 0 0 0 3px var(--openapi-code-tab-shadow-color-js);
+ border-color: var(--openapi-code-tab-border-color-js);
+ }
+}
+
+.openapi-tabs__code-item--curl {
+ color: var(--ifm-color-danger);
+
+ &::after {
+ content: "";
+ width: var(--code-tab-logo-width);
+ height: var(--code-tab-logo-height);
+ background: url("https://raw.githubusercontent.com/devicons/devicon/master/icons/bash/bash-plain.svg")
+ no-repeat;
+ margin-block: auto;
+ background-color: var(--bash-background-color);
+ border-radius: var(--bash-border-radius);
+ }
+
+ &.active {
+ box-shadow: 0 0 0 3px var(--openapi-code-tab-shadow-color-curl);
+ border-color: var(--ifm-color-danger);
+ }
+}
+
+.openapi-tabs__code-item--ruby {
+ color: var(--ifm-color-danger);
+
+ &::after {
+ content: "";
+ width: var(--code-tab-logo-width);
+ height: var(--code-tab-logo-height);
+ background: url("https://raw.githubusercontent.com/devicons/devicon/master/icons/ruby/ruby-plain.svg")
+ no-repeat;
+ margin-block: auto;
+ }
+
+ &.active {
+ box-shadow: 0 0 0 3px var(--openapi-code-tab-shadow-color-ruby);
+ border-color: var(--openapi-code-tab-border-color-ruby);
+ }
+}
+
+.openapi-tabs__code-item--csharp {
+ color: var(--ifm-color-gray-500);
+
+ &::after {
+ content: "";
+ width: var(--code-tab-logo-width);
+ height: var(--code-tab-logo-height);
+ background: url("https://raw.githubusercontent.com/devicons/devicon/master/icons/csharp/csharp-original.svg")
+ no-repeat;
+ margin-block: auto;
+ }
+
+ &.active {
+ box-shadow: 0 0 0 3px var(--openapi-code-tab-shadow-color-csharp);
+ border-color: var(--openapi-code-tab-border-color-csharp);
+ }
+}
+
+.openapi-tabs__code-item--r {
+ color: var(--ifm-color-gray-500);
+
+ &::after {
+ content: "";
+ width: var(--code-tab-logo-width);
+ height: var(--code-tab-logo-height);
+ background: url("https://raw.githubusercontent.com/devicons/devicon/master/icons/r/r-original.svg")
+ no-repeat;
+ margin-block: auto;
+ }
+
+ &.active {
+ box-shadow: 0 0 0 3px var(--openapi-code-tab-shadow-color-r);
+ border-color: var(--openapi-code-tab-border-color-r);
+ }
+}
+
+.openapi-tabs__code-item--swift {
+ color: var(--ifm-color-danger);
+
+ &::after {
+ content: "";
+ width: var(--code-tab-logo-width);
+ height: var(--code-tab-logo-height);
+ background: url("https://raw.githubusercontent.com/devicons/devicon/master/icons/swift/swift-original.svg")
+ no-repeat;
+ margin-block: auto;
+ }
+
+ &.active {
+ box-shadow: 0 0 0 3px var(--openapi-code-tab-shadow-color-swift);
+ border-color: var(--openapi-code-tab-border-color-swift);
+ }
+}
+
+.openapi-tabs__code-item--c {
+ color: var(--ifm-color-info);
+
+ &::after {
+ content: "";
+ width: var(--code-tab-logo-width);
+ height: var(--code-tab-logo-height);
+ background: url("https://raw.githubusercontent.com/devicons/devicon/master/icons/c/c-original.svg")
+ no-repeat;
+ margin-block: auto;
+ }
+
+ &.active {
+ box-shadow: 0 0 0 3px var(--openapi-code-tab-shadow-color-c);
+ border-color: var(--openapi-code-tab-border-color-c);
+ }
+}
+
+.openapi-tabs__code-item--objective-c {
+ color: var(--ifm-color-info);
+
+ &::after {
+ content: "";
+ width: var(--code-tab-logo-width);
+ height: var(--code-tab-logo-height);
+ background: url("https://raw.githubusercontent.com/devicons/devicon/master/icons/objectivec/objectivec-plain.svg")
+ no-repeat;
+ margin-block: auto;
+ }
+
+ &.active {
+ box-shadow: 0 0 0 3px var(--openapi-code-tab-shadow-color-objective-c);
+ border-color: var(--openapi-code-tab-border-color-objective-c);
+ }
+}
+
+.openapi-tabs__code-item--ocaml {
+ color: var(--ifm-color-warning);
+
+ &::after {
+ content: "";
+ width: var(--code-tab-logo-width);
+ height: var(--code-tab-logo-height);
+ background: url("https://raw.githubusercontent.com/devicons/devicon/master/icons/ocaml/ocaml-original.svg")
+ no-repeat;
+ margin-block: auto;
+ }
+
+ &.active {
+ box-shadow: 0 0 0 3px var(--openapi-code-tab-shadow-color-objective-ocaml);
+ border-color: var(--openapi-code-tab-border-color-objective-ocaml);
+ }
+}
+
+.openapi-tabs__code-item--nodejs {
+ color: var(--ifm-color-success);
+
+ &::after {
+ content: "";
+ width: var(--code-tab-logo-width);
+ height: var(--code-tab-logo-height);
+ background: url("https://raw.githubusercontent.com/devicons/devicon/master/icons/nodejs/nodejs-original.svg")
+ no-repeat;
+ margin-block: auto;
+ }
+
+ &.active {
+ box-shadow: 0 0 0 3px var(--opeanpi-code-tab-shadow-color-nodejs);
+ border-color: var(--openapi-code-tab-border-color-nodejs);
+ }
+}
+
+.openapi-tabs__code-item--php {
+ color: var(--ifm-color-gray-500);
+
+ &::after {
+ content: "";
+ width: var(--code-tab-logo-width);
+ height: var(--code-tab-logo-height);
+ background: url("https://raw.githubusercontent.com/devicons/devicon/master/icons/php/php-original.svg")
+ no-repeat;
+ margin-block: auto;
+ }
+
+ &.active {
+ box-shadow: 0 0 0 3px var(--openapi-code-tab-shadow-color-php);
+ border-color: var(--openapi-code-tab-border-color-php);
+ }
+}
+
+.openapi-tabs__code-item--kotlin {
+ color: var(--ifm-color-gray-500);
+
+ &::after {
+ content: "";
+ width: var(--code-tab-logo-width);
+ height: var(--code-tab-logo-height);
+ background: url("https://raw.githubusercontent.com/devicons/devicon/master/icons/kotlin/kotlin-original.svg")
+ no-repeat;
+ margin-block: auto;
+ }
+
+ &.active {
+ box-shadow: 0 0 0 3px var(--openapi-code-tab-shadow-color-kotlin);
+ border-color: var(--openapi-code-tab-border-color-kotlin);
+ }
+}
+
+.openapi-tabs__code-item--rust {
+ color: var(--ifm-color-gray-500);
+
+ &::after {
+ content: "";
+ width: var(--code-tab-logo-width);
+ height: var(--code-tab-logo-height);
+ background: url("https://raw.githubusercontent.com/devicons/devicon/master/icons/rust/rust-original.svg")
+ no-repeat;
+ margin-block: auto;
+ }
+
+ &.active {
+ box-shadow: 0 0 0 3px var(--openapi-code-tab-shadow-color-rust);
+ border-color: var(--openapi-code-tab-border-color-rust);
+ }
+}
+
+.openapi-tabs__code-item--java {
+ color: var(--ifm-color-warning);
+
+ &::after {
+ content: "";
+ width: var(--code-tab-logo-width);
+ height: var(--code-tab-logo-height);
+ background: url("https://raw.githubusercontent.com/devicons/devicon/master/icons/java/java-original.svg")
+ no-repeat;
+ margin-block: auto;
+ }
+
+ &.active {
+ box-shadow: 0 0 0 3px var(--openapi-code-tab-shadow-color-java);
+ border-color: var(--openapi-code-tab-border-color-java);
+ }
+}
+
+.openapi-tabs__code-item--powershell {
+ color: var(--ifm-color-info);
+
+ &::after {
+ content: "";
+ width: var(--code-tab-logo-width);
+ height: var(--code-tab-logo-height);
+ background: url("https://raw.githubusercontent.com/devicons/devicon/master/icons/windows8/windows8-original.svg")
+ no-repeat;
+ margin-block: auto;
+ }
+
+ &.active {
+ box-shadow: 0 0 0 3px var(--opeanpi-code-tab-shadow-color-powershell);
+ border-color: var(--openapi-code-tab-border-color-powershell);
+ }
+}
+
+.openapi-tabs__code-item--http {
+ color: var(--ifm-color-gray-500);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: relative;
+
+ &::after {
+ content: "";
+ display: inline-block;
+ width: 32px; /* Explicitly setting width to 32 pixels */
+ height: 32px; /* Explicitly setting height to 32 pixels */
+ background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIHZpZXdCb3g9IjAgMCAzMiAzMiIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJjdXJyZW50Q29sb3IiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTEyIDIyTDggMTZMMTIgMTBNMjAgMjJMMjQgMTZMIDIwIDEwIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48L3N2Zz4=");
+ background-size: contain;
+ background-repeat: no-repeat;
+ background-position: center; /* Center the SVG */
+ margin-top: 0.5rem;
+ }
+
+ &.active {
+ box-shadow: 0 0 0 3px var(--opeanpi-code-tab-shadow-color-http);
+ border-color: var(--openapi-code-tab-border-color-http);
+ }
+}
+
+.openapi-tabs__code-item--shell {
+ color: var(--ifm-color-gray-500);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: relative;
+
+ &::after {
+ content: "";
+ display: inline-block;
+ width: 32px; /* Explicitly setting width to 32 pixels */
+ height: 32px; /* Explicitly setting height to 32 pixels */
+ background-image: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIHZpZXdCb3g9IjAgMCAzMiAzMiIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJjdXJyZW50Q29sb3IiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTEyIDIyTDggMTZMMTIgMTBNMjAgMjJMMjQgMTZMIDIwIDEwIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48L3N2Zz4=");
+ background-size: contain;
+ background-repeat: no-repeat;
+ background-position: center; /* Center the SVG */
+ margin-top: 0.5rem;
+ }
+
+ &.active {
+ box-shadow: 0 0 0 3px var(--opeanpi-code-tab-shadow-color-shell);
+ border-color: var(--openapi-code-tab-border-color-shell);
+ }
+}
+
+@media only screen and (min-width: 768px) and (max-width: 996px) {
+ .openapi-tabs__code-list {
+ justify-content: space-around;
+ }
+}
+
+.ReactModal__Body--open {
+ overflow: hidden !important;
+}
+
+.openapi-modal--open {
+ background-color: rgba(0, 0, 0, 0.7) !important;
+}
diff --git a/docs/src/theme/ApiExplorer/CodeTabs/index.tsx b/docs/src/theme/ApiExplorer/CodeTabs/index.tsx
new file mode 100644
index 00000000..1fc4e7e8
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/CodeTabs/index.tsx
@@ -0,0 +1,241 @@
+import React, { cloneElement, ReactElement, useEffect, useRef } from "react";
+
+import {
+ sanitizeTabsChildren,
+ type TabProps,
+ useScrollPositionBlocker,
+ useTabs,
+} from "@docusaurus/theme-common/internal";
+import { TabItemProps } from "@docusaurus/theme-common/lib/utils/tabsUtils";
+import useIsBrowser from "@docusaurus/useIsBrowser";
+import { Language } from "@theme/ApiExplorer/CodeSnippets";
+import clsx from "clsx";
+
+export interface Props {
+ action: {
+ [key: string]: React.Dispatch;
+ };
+ currentLanguage: Language;
+ languageSet: Language[];
+ includeVariant: boolean;
+}
+
+export interface CodeTabsProps extends Props, TabProps {
+ includeSample?: boolean;
+}
+
+function TabList({
+ action,
+ currentLanguage,
+ languageSet,
+ includeVariant,
+ includeSample,
+ className,
+ block,
+ selectedValue,
+ selectValue,
+ tabValues,
+}: CodeTabsProps & ReturnType) {
+ const tabRefs = useRef<(HTMLLIElement | null)[]>([]);
+ const tabsScrollContainerRef = useRef(null);
+ const { blockElementScrollPositionUntilNextRender } =
+ useScrollPositionBlocker();
+
+ useEffect(() => {
+ const activeTab = tabRefs.current.find(
+ (tab) => tab?.getAttribute("aria-selected") === "true",
+ );
+
+ if (activeTab && tabsScrollContainerRef.current) {
+ const container = tabsScrollContainerRef.current;
+ const containerRect = container.getBoundingClientRect();
+ const activeTabRect = activeTab.getBoundingClientRect();
+
+ // Calculate the distance to scroll to align active tab to the left
+ const glowOffset = 3;
+ const scrollOffset =
+ activeTabRect.left -
+ containerRect.left +
+ container.scrollLeft -
+ glowOffset;
+
+ // Check if the active tab is not already at the left position
+
+ if (Math.abs(scrollOffset - container.scrollLeft) > 4) {
+ // Adjust the scroll of the container
+ container.scrollLeft = scrollOffset;
+ }
+ }
+ }, []);
+
+ const handleTabChange = (
+ event:
+ | React.FocusEvent
+ | React.MouseEvent
+ | React.KeyboardEvent,
+ ) => {
+ const newTab = event.currentTarget;
+ const newTabIndex = tabRefs.current.indexOf(newTab);
+ const newTabValue = tabValues[newTabIndex]!.value;
+
+ if (newTabValue !== selectedValue) {
+ blockElementScrollPositionUntilNextRender(newTab);
+ selectValue(newTabValue);
+ }
+
+ if (action) {
+ let newLanguage: Language;
+ if (currentLanguage && includeVariant) {
+ newLanguage = languageSet.filter(
+ (lang: Language) => lang.language === currentLanguage,
+ )[0];
+ newLanguage.variant = newTabValue;
+ action.setSelectedVariant(newTabValue.toLowerCase());
+ } else if (currentLanguage && includeSample) {
+ newLanguage = languageSet.filter(
+ (lang: Language) => lang.language === currentLanguage,
+ )[0];
+ newLanguage.sample = newTabValue;
+ action.setSelectedSample(newTabValue);
+ } else {
+ newLanguage = languageSet.filter(
+ (lang: Language) => lang.language === newTabValue,
+ )[0];
+ action.setSelectedVariant(newLanguage.variants[0].toLowerCase());
+ action.setSelectedSample(newLanguage.sample);
+ }
+ action.setLanguage(newLanguage);
+ }
+ };
+
+ const handleKeydown = (event: React.KeyboardEvent) => {
+ let focusElement: HTMLLIElement | null = null;
+
+ switch (event.key) {
+ case "Enter": {
+ handleTabChange(event);
+ break;
+ }
+ case "ArrowRight": {
+ const nextTab = tabRefs.current.indexOf(event.currentTarget) + 1;
+ focusElement = tabRefs.current[nextTab] ?? tabRefs.current[0]!;
+ break;
+ }
+ case "ArrowLeft": {
+ const prevTab = tabRefs.current.indexOf(event.currentTarget) - 1;
+ focusElement =
+ tabRefs.current[prevTab] ??
+ tabRefs.current[tabRefs.current.length - 1]!;
+ break;
+ }
+ default:
+ break;
+ }
+
+ focusElement?.focus();
+ };
+
+ return (
+
+ {tabValues.map(({ value, label, attributes }) => (
+ - {
+ if (tabControl) {
+ tabRefs.current.push(tabControl);
+ }
+ }}
+ onKeyDown={handleKeydown}
+ onClick={handleTabChange}
+ {...attributes}
+ className={clsx(
+ "tabs__item",
+ "openapi-tabs__code-item",
+ attributes?.className as string,
+ {
+ active: selectedValue === value,
+ },
+ )}
+ >
+ {label ?? value}
+
+ ))}
+
+ );
+}
+
+function TabContent({
+ lazy,
+ children,
+ selectedValue,
+}: CodeTabsProps & ReturnType): React.JSX.Element | null {
+ const childTabs = (Array.isArray(children) ? children : [children]).filter(
+ Boolean,
+ ) as ReactElement[];
+ if (lazy) {
+ const selectedTabItem = childTabs.find(
+ (tabItem) => tabItem.props.value === selectedValue,
+ );
+ if (!selectedTabItem) {
+ // fail-safe or fail-fast? not sure what's best here
+ return null;
+ }
+ return cloneElement(selectedTabItem, { className: "margin-top--md" });
+ }
+ return (
+
+ {childTabs.map((tabItem, i) =>
+ cloneElement(tabItem, {
+ key: i,
+ hidden: tabItem.props.value !== selectedValue,
+ }),
+ )}
+
+ );
+}
+
+function TabsComponent(props: CodeTabsProps & Props): React.JSX.Element {
+ const tabs = useTabs(props);
+ const { className } = props;
+
+ return (
+
+
+
+
+ );
+}
+
+export default function CodeTabs(
+ props: CodeTabsProps & Props,
+): React.JSX.Element {
+ const isBrowser = useIsBrowser();
+ return (
+
+ {sanitizeTabsChildren(props.children)}
+
+ );
+}
diff --git a/docs/src/theme/ApiExplorer/ContentType/index.tsx b/docs/src/theme/ApiExplorer/ContentType/index.tsx
new file mode 100644
index 00000000..ecc95b22
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/ContentType/index.tsx
@@ -0,0 +1,31 @@
+import React from "react";
+
+import FormItem from "@theme/ApiExplorer/FormItem";
+import FormSelect from "@theme/ApiExplorer/FormSelect";
+import { useTypedDispatch, useTypedSelector } from "@theme/ApiItem/hooks";
+
+import { setContentType } from "./slice";
+
+function ContentType() {
+ const value = useTypedSelector((state: any) => state.contentType.value);
+ const options = useTypedSelector((state: any) => state.contentType.options);
+ const dispatch = useTypedDispatch();
+
+ if (options.length <= 1) {
+ return null;
+ }
+
+ return (
+
+ ) =>
+ dispatch(setContentType(e.target.value))
+ }
+ />
+
+ );
+}
+
+export default ContentType;
diff --git a/docs/src/theme/ApiExplorer/ContentType/slice.ts b/docs/src/theme/ApiExplorer/ContentType/slice.ts
new file mode 100644
index 00000000..b0c3221a
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/ContentType/slice.ts
@@ -0,0 +1,22 @@
+import { createSlice, PayloadAction } from "@reduxjs/toolkit";
+
+export interface State {
+ value: string;
+ options: string[];
+}
+
+const initialState: State = {} as any;
+
+export const slice = createSlice({
+ name: "contentType",
+ initialState,
+ reducers: {
+ setContentType: (state, action: PayloadAction) => {
+ state.value = action.payload;
+ },
+ },
+});
+
+export const { setContentType } = slice.actions;
+
+export default slice.reducer;
diff --git a/docs/src/theme/ApiExplorer/Export/index.tsx b/docs/src/theme/ApiExplorer/Export/index.tsx
new file mode 100644
index 00000000..c8a20102
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/Export/index.tsx
@@ -0,0 +1,40 @@
+import React from "react";
+
+import fileSaver from "file-saver";
+
+const saveFile = (url: string) => {
+ let fileName;
+ if (url.endsWith("json") || url.endsWith("yaml") || url.endsWith("yml")) {
+ fileName = url.substring(url.lastIndexOf("/") + 1);
+ }
+ fileSaver.saveAs(url, fileName ? fileName : "openapi.txt");
+};
+
+function Export({ url, proxy }: any) {
+ return (
+
+
+
+
+ );
+}
+
+export default Export;
diff --git a/docs/src/theme/ApiExplorer/FloatingButton/_FloatingButton.scss b/docs/src/theme/ApiExplorer/FloatingButton/_FloatingButton.scss
new file mode 100644
index 00000000..73c9627c
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/FloatingButton/_FloatingButton.scss
@@ -0,0 +1,27 @@
+.openapi-explorer__floating-btn {
+ position: relative;
+
+ button {
+ position: relative;
+ background: var(--ifm-color-emphasis-900);
+ border: none;
+ border-radius: var(--ifm-global-radius);
+ color: var(--ifm-color-emphasis-100);
+ cursor: pointer;
+ padding: 0.4rem 0.5rem;
+ opacity: 0;
+ visibility: hidden;
+ transition:
+ opacity 0.2s ease-in-out,
+ visibility 0.2s ease-in-out,
+ bottom 0.2s ease-in-out;
+ position: absolute;
+ right: calc(var(--ifm-pre-padding) / 2);
+ }
+}
+.openapi-explorer__floating-btn:hover button,
+.openapi-explorer__floating-btn:focus-visible button,
+.openapi-explorer__floating-btn button:focus-visible {
+ visibility: visible;
+ opacity: 1;
+}
diff --git a/docs/src/theme/ApiExplorer/FloatingButton/index.tsx b/docs/src/theme/ApiExplorer/FloatingButton/index.tsx
new file mode 100644
index 00000000..c1bef2ea
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/FloatingButton/index.tsx
@@ -0,0 +1,22 @@
+import React from "react";
+
+export interface Props {
+ label?: string;
+ onClick?: React.MouseEventHandler;
+ children?: React.ReactNode;
+}
+
+function FloatingButton({ label, onClick, children }: Props) {
+ return (
+
+ {label && (
+
+ )}
+ {children}
+
+ );
+}
+
+export default FloatingButton;
diff --git a/docs/src/theme/ApiExplorer/FormFileUpload/_FormFileUpload.scss b/docs/src/theme/ApiExplorer/FormFileUpload/_FormFileUpload.scss
new file mode 100644
index 00000000..0b0033c3
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/FormFileUpload/_FormFileUpload.scss
@@ -0,0 +1,72 @@
+.openapi-explorer__dropzone {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ border: 2px dashed var(--openapi-monaco-border-color);
+ background-color: var(--openapi-input-background);
+
+ width: 100%;
+ border-radius: 4px;
+ padding: var(--ifm-pre-padding);
+ font-size: var(--ifm-code-font-size);
+
+ &:hover {
+ border: 2px dashed var(--ifm-color-primary);
+ background: linear-gradient(
+ var(--openapi-dropzone-hover-shim),
+ var(--openapi-dropzone-hover-shim)
+ ),
+ linear-gradient(var(--ifm-color-primary), var(--ifm-color-primary));
+
+ .openapi-explorer__dropzone-content {
+ color: var(--ifm-pre-color);
+ }
+ }
+}
+
+.openapi-explorer__dropzone-hover {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ border: 2px dashed var(--openapi-monaco-border-color);
+ background-color: var(--openapi-input-background);
+ width: 100%;
+ border-radius: 4px;
+ padding: var(--ifm-pre-padding);
+ font-size: var(--ifm-code-font-size);
+ border: 2px dashed var(--ifm-color-primary);
+
+ background: linear-gradient(
+ var(--openapi-dropzone-hover-shim),
+ var(--openapi-dropzone-hover-shim)
+ ),
+ linear-gradient(var(--ifm-color-primary), var(--ifm-color-primary));
+
+ .openapi-explorer__dropzone-content {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-wrap: wrap;
+ margin: var(--ifm-pre-padding) 0;
+ color: var(--ifm-pre-color);
+ }
+
+ .openapi-explorer__file-name {
+ margin: 0 calc(var(--ifm-pre-padding) * 1.5);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ flex: 1;
+ }
+}
+
+.openapi-explorer__dropzone-content {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-wrap: wrap;
+ margin: var(--ifm-pre-padding) 0;
+ color: var(--openapi-dropzone-color);
+}
diff --git a/docs/src/theme/ApiExplorer/FormFileUpload/index.tsx b/docs/src/theme/ApiExplorer/FormFileUpload/index.tsx
new file mode 100644
index 00000000..5d5831c4
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/FormFileUpload/index.tsx
@@ -0,0 +1,112 @@
+import React, { useState } from "react";
+
+import FloatingButton from "@theme/ApiExplorer/FloatingButton";
+import MagicDropzone from "react-magic-dropzone";
+
+type PreviewFile = { preview: string } & File;
+
+interface RenderPreviewProps {
+ file: PreviewFile;
+}
+
+function RenderPreview({ file }: RenderPreviewProps) {
+ switch (file.type) {
+ case "image/png":
+ case "image/jpeg":
+ case "image/jpg":
+ case "image/svg+xml":
+ return (
+
+ );
+ default:
+ return (
+
+
+ {file.name}
+
+ );
+ }
+}
+
+export interface Props {
+ placeholder: string;
+ onChange?(file?: File): any;
+}
+
+function FormFileUpload({ placeholder, onChange }: Props) {
+ const [hover, setHover] = useState(false);
+ const [file, setFile] = useState();
+
+ function setAndNotifyFile(file?: PreviewFile) {
+ setFile(file);
+ onChange?.(file);
+ }
+
+ function handleDrop(accepted: PreviewFile[]) {
+ const [file] = accepted;
+ setAndNotifyFile(file);
+ setHover(false);
+ }
+
+ return (
+
+ setHover(true)}
+ onDragLeave={() => setHover(false)}
+ multiple={false}
+ style={{ marginTop: "calc(var(--ifm-pre-padding) / 2)" }}
+ >
+ {file ? (
+ <>
+
+
+ >
+ ) : (
+
+ {placeholder}
+
+ )}
+
+
+ );
+}
+
+export default FormFileUpload;
diff --git a/docs/src/theme/ApiExplorer/FormItem/_FormItem.scss b/docs/src/theme/ApiExplorer/FormItem/_FormItem.scss
new file mode 100644
index 00000000..25b59053
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/FormItem/_FormItem.scss
@@ -0,0 +1,21 @@
+.openapi-explorer__form-item {
+ padding: var(--openapi-explorer-padding-input);
+ font-size: var(--openapi-explorer-font-size-input);
+
+ &:first-child {
+ margin-top: 0;
+ }
+
+ .required {
+ color: var(--openapi-required);
+ }
+}
+
+.openapi-explorer__form-item-body-container {
+ padding: 0;
+}
+
+.openapi-explorer__form-item-label {
+ font-family: var(--ifm-font-family-monospace);
+ font-weight: bold;
+}
diff --git a/docs/src/theme/ApiExplorer/FormItem/index.tsx b/docs/src/theme/ApiExplorer/FormItem/index.tsx
new file mode 100644
index 00000000..992f6c60
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/FormItem/index.tsx
@@ -0,0 +1,26 @@
+import React from "react";
+
+import clsx from "clsx";
+
+export interface Props {
+ label?: string;
+ type?: string;
+ required?: boolean | undefined;
+ children?: React.ReactNode;
+ className?: string;
+}
+
+function FormItem({ label, type, required, children, className }: Props) {
+ return (
+
+ {label && (
+
+ )}
+ {type && — {type}}
+ {required && required}
+ {children}
+
+ );
+}
+
+export default FormItem;
diff --git a/docs/src/theme/ApiExplorer/FormMultiSelect/_FormMultiSelect.scss b/docs/src/theme/ApiExplorer/FormMultiSelect/_FormMultiSelect.scss
new file mode 100644
index 00000000..da855e0e
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/FormMultiSelect/_FormMultiSelect.scss
@@ -0,0 +1,30 @@
+.openapi-explorer__multi-select-input {
+ width: 100%;
+ margin-top: calc(var(--ifm-pre-padding) / 2);
+ padding: 1rem;
+ border-radius: 4px;
+ border: 1px solid transparent;
+ background-color: var(--openapi-input-background);
+ outline: none;
+ font-size: var(--openapi-explorer-font-size-input);
+ color: var(--ifm-pre-color);
+ -moz-appearance: none;
+ -webkit-appearance: none;
+ appearance: none;
+
+ &:focus {
+ border: 1px solid var(--openapi-input-border);
+ }
+
+ &.error {
+ border: 1px solid var(--ifm-color-danger);
+ }
+
+ option {
+ border-radius: 0.25rem;
+ color: var(--ifm-menu-color);
+ margin: 0.25rem 0;
+ padding: var(--ifm-menu-link-padding-vertical)
+ var(--ifm-menu-link-padding-horizontal);
+ }
+}
diff --git a/docs/src/theme/ApiExplorer/FormMultiSelect/index.tsx b/docs/src/theme/ApiExplorer/FormMultiSelect/index.tsx
new file mode 100644
index 00000000..7678ab79
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/FormMultiSelect/index.tsx
@@ -0,0 +1,50 @@
+import React from "react";
+
+import clsx from "clsx";
+
+export interface Props {
+ value?: string;
+ options: string[];
+ onChange?: React.ChangeEventHandler;
+ showErrors?: boolean;
+}
+
+function FormMultiSelect({ value, options, onChange, showErrors }: Props) {
+ if (options.length === 0) {
+ return null;
+ }
+
+ let height;
+ if (options.length < 6) {
+ const selectPadding = 12 * 2;
+ const rawHeight = options.length * 29;
+ const innerMargins = 4 * options.length - 1;
+ const outerMargins = 4 * 2;
+ const mysteryScroll = 1;
+ height =
+ rawHeight + innerMargins + outerMargins + selectPadding + mysteryScroll;
+ }
+
+ return (
+
+ );
+}
+
+export default FormMultiSelect;
diff --git a/docs/src/theme/ApiExplorer/FormSelect/_FormSelect.scss b/docs/src/theme/ApiExplorer/FormSelect/_FormSelect.scss
new file mode 100644
index 00000000..86872613
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/FormSelect/_FormSelect.scss
@@ -0,0 +1,43 @@
+html[data-theme="dark"] .openapi-explorer__select-input {
+ margin-top: calc(var(--ifm-pre-padding) / 2);
+ background-color: var(--openapi-input-background);
+ border: none;
+ outline: none;
+ width: 100%;
+ color: var(--ifm-pre-color);
+
+ border-radius: 4px;
+ -moz-appearance: none;
+ -webkit-appearance: none;
+ appearance: none;
+
+ background-image: url('data:image/svg+xml;charset=US-ASCII,');
+ background-repeat: no-repeat;
+ background-position: right var(--ifm-pre-padding) top 50%;
+ background-size: auto auto;
+}
+
+.openapi-explorer__select-input {
+ width: 100%;
+ margin-top: calc(var(--ifm-pre-padding) / 2);
+ padding: var(--openapi-explorer-padding-input);
+ border: none;
+ outline: none;
+ border-radius: 4px;
+ background-color: var(--openapi-input-background);
+ font-size: var(--openapi-explorer-font-size-input);
+ font-family: var(--ifm-font-family-monospace);
+ color: var(--ifm-pre-color);
+ -moz-appearance: none;
+ -webkit-appearance: none;
+ appearance: none;
+
+ background-image: url('data:image/svg+xml;charset=US-ASCII,');
+ background-repeat: no-repeat;
+ background-position: right var(--ifm-pre-padding) top 50%;
+ background-size: auto auto;
+
+ &:focus {
+ box-shadow: inset 0px 0px 0px 2px var(--openapi-input-border);
+ }
+}
diff --git a/docs/src/theme/ApiExplorer/FormSelect/index.tsx b/docs/src/theme/ApiExplorer/FormSelect/index.tsx
new file mode 100644
index 00000000..f1933674
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/FormSelect/index.tsx
@@ -0,0 +1,31 @@
+import React from "react";
+
+export interface Props {
+ value?: string;
+ options?: string[];
+ onChange?: React.ChangeEventHandler;
+}
+
+function FormSelect({ value, options, onChange }: Props) {
+ if (!Array.isArray(options) || options.length === 0) {
+ return null;
+ }
+
+ return (
+
+ );
+}
+
+export default FormSelect;
diff --git a/docs/src/theme/ApiExplorer/FormTextInput/_FormTextInput.scss b/docs/src/theme/ApiExplorer/FormTextInput/_FormTextInput.scss
new file mode 100644
index 00000000..8c02e92a
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/FormTextInput/_FormTextInput.scss
@@ -0,0 +1,34 @@
+.openapi-explorer__form-item-input {
+ margin-top: calc(var(--ifm-pre-padding) / 2);
+ background-color: var(--openapi-input-background);
+ border: 1px solid transparent;
+ outline: none;
+ width: 100%;
+ color: var(--ifm-pre-color);
+ padding: var(--openapi-explorer-padding-input);
+ border-radius: 4px;
+
+ &:hover {
+ border: 1px solid var(--ifm-toc-border-color);
+ }
+
+ &:focus {
+ border: 1px solid var(--ifm-color-primary);
+ box-shadow: none;
+ }
+
+ &.error {
+ border: 1px solid var(--openapi-required);
+ }
+}
+
+.openapi-explorer__input-error {
+ font-size: var(--openapi-explorer-font-size-input);
+ color: var(--openapi-required);
+ padding-top: var(--openapi-explorer-padding-input);
+
+ &::before {
+ display: inline;
+ content: "⚠ ";
+ }
+}
diff --git a/docs/src/theme/ApiExplorer/FormTextInput/index.tsx b/docs/src/theme/ApiExplorer/FormTextInput/index.tsx
new file mode 100644
index 00000000..e70736e0
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/FormTextInput/index.tsx
@@ -0,0 +1,73 @@
+// @ts-nocheck
+import React from "react";
+
+import { ErrorMessage } from "@hookform/error-message";
+import clsx from "clsx";
+import { useFormContext } from "react-hook-form";
+
+export interface Props {
+ value?: string;
+ placeholder?: string;
+ password?: boolean;
+ onChange?: React.ChangeEventHandler;
+}
+
+function FormTextInput({
+ isRequired,
+ value,
+ placeholder,
+ password,
+ onChange,
+ paramName,
+}: Props) {
+ placeholder = placeholder?.split("\n")[0];
+
+ const {
+ register,
+ formState: { errors },
+ } = useFormContext();
+
+ const showErrorMessage = errors?.[paramName]?.message;
+
+ return (
+ <>
+ {paramName ? (
+
+ ) : (
+
+ )}
+ {showErrorMessage && (
+ (
+ {message}
+ )}
+ />
+ )}
+ >
+ );
+}
+
+export default FormTextInput;
diff --git a/docs/src/theme/ApiExplorer/LiveEditor/_LiveEditor.scss b/docs/src/theme/ApiExplorer/LiveEditor/_LiveEditor.scss
new file mode 100644
index 00000000..9dd7393c
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/LiveEditor/_LiveEditor.scss
@@ -0,0 +1,15 @@
+.openapi-explorer__playground-container {
+ margin-top: 1rem;
+ margin-bottom: var(--ifm-leading);
+ border-radius: var(--ifm-global-radius);
+ box-shadow: var(--ifm-global-shadow-lw);
+ overflow: auto;
+ max-height: 500px;
+}
+
+.openapi-explorer__playground-editor {
+ font: var(--ifm-code-font-size) / var(--ifm-pre-line-height)
+ var(--ifm-font-family-monospace) !important;
+ /* rtl:ignore */
+ direction: ltr;
+}
diff --git a/docs/src/theme/ApiExplorer/LiveEditor/index.tsx b/docs/src/theme/ApiExplorer/LiveEditor/index.tsx
new file mode 100644
index 00000000..c3667566
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/LiveEditor/index.tsx
@@ -0,0 +1,107 @@
+import React, { type JSX, useEffect, useState } from "react";
+
+import { usePrismTheme } from "@docusaurus/theme-common";
+import useIsBrowser from "@docusaurus/useIsBrowser";
+import { ErrorMessage } from "@hookform/error-message";
+import { setStringRawBody } from "@theme/ApiExplorer/Body/slice";
+import clsx from "clsx";
+import { Controller, useFormContext } from "react-hook-form";
+import { LiveProvider, LiveEditor, withLive } from "react-live";
+
+function Live({ onEdit, showErrors }: any) {
+ const isBrowser = useIsBrowser();
+ const [editorDisabled, setEditorDisabled] = useState(false);
+
+ return (
+ setEditorDisabled(false)}
+ onBlur={() => setEditorDisabled(true)}
+ >
+
+
+ );
+}
+
+const LiveComponent = withLive(Live);
+
+function App({
+ children,
+ transformCode,
+ value,
+ language,
+ action,
+ required: isRequired,
+ ...props
+}: any): JSX.Element {
+ const prismTheme = usePrismTheme();
+ const [code, setCode] = React.useState(children.replace(/\n$/, ""));
+
+ useEffect(() => {
+ action(setStringRawBody(code));
+ }, [action, code]);
+
+ const {
+ control,
+ formState: { errors },
+ } = useFormContext();
+
+ const showErrorMessage = errors?.requestBody;
+
+ const handleChange = (snippet: string, onChange: any) => {
+ setCode(snippet);
+ onChange(snippet);
+ };
+
+ return (
+
+ `${code};`)}
+ theme={prismTheme}
+ language={language}
+ {...props}
+ >
+ (
+ handleChange(e, onChange)}
+ name={name}
+ showErrors={showErrorMessage}
+ />
+ )}
+ />
+ {showErrorMessage && (
+ (
+ {message}
+ )}
+ />
+ )}
+
+
+ );
+}
+
+const LiveApp = withLive(App);
+export default LiveApp;
diff --git a/docs/src/theme/ApiExplorer/MethodEndpoint/_MethodEndpoint.scss b/docs/src/theme/ApiExplorer/MethodEndpoint/_MethodEndpoint.scss
new file mode 100644
index 00000000..9d22e85f
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/MethodEndpoint/_MethodEndpoint.scss
@@ -0,0 +1,22 @@
+.openapi__method-endpoint {
+ display: flex;
+ align-items: center;
+ max-width: 100%;
+ width: fit-content;
+ padding: 0.65rem;
+ border: 1px solid var(--ifm-toc-border-color);
+}
+
+.openapi__method-endpoint-path {
+ margin-bottom: 0;
+ margin-left: 0.5rem;
+ font-size: 12px;
+ font-weight: normal;
+ font-family: var(--ifm-font-family-monospace);
+}
+
+.openapi__divider {
+ width: 100%;
+ margin: 1.5rem 0;
+ border-bottom: 1px solid var(--ifm-toc-border-color);
+}
diff --git a/docs/src/theme/ApiExplorer/MethodEndpoint/index.tsx b/docs/src/theme/ApiExplorer/MethodEndpoint/index.tsx
new file mode 100644
index 00000000..e4098eb5
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/MethodEndpoint/index.tsx
@@ -0,0 +1,84 @@
+import React from "react";
+
+import BrowserOnly from "@docusaurus/BrowserOnly";
+import { useTypedSelector } from "@theme/ApiItem/hooks";
+
+function colorForMethod(method: string) {
+ switch (method.toLowerCase()) {
+ case "get":
+ return "primary";
+ case "post":
+ return "success";
+ case "delete":
+ return "danger";
+ case "put":
+ return "info";
+ case "patch":
+ return "warning";
+ case "head":
+ return "secondary";
+ case "event":
+ return "secondary";
+ default:
+ return undefined;
+ }
+}
+
+export interface Props {
+ method: string;
+ path: string;
+ context?: "endpoint" | "callback";
+}
+
+function MethodEndpoint({ method, path, context }: Props) {
+ let serverValue = useTypedSelector((state: any) => state.server.value);
+ let serverUrlWithVariables = "";
+
+ const renderServerUrl = () => {
+ if (context === "callback") {
+ return "";
+ }
+
+ if (serverValue && serverValue.variables) {
+ serverUrlWithVariables = serverValue.url.replace(/\/$/, "");
+
+ Object.keys(serverValue.variables).forEach((variable) => {
+ serverUrlWithVariables = serverUrlWithVariables.replace(
+ `{${variable}}`,
+ serverValue.variables?.[variable].default ?? "",
+ );
+ });
+ }
+
+ return (
+
+ {() => {
+ if (serverUrlWithVariables.length) {
+ return serverUrlWithVariables;
+ } else if (serverValue && serverValue.url) {
+ return serverValue.url;
+ }
+ }}
+
+ );
+ };
+
+ return (
+ <>
+
+
+ {method === "event" ? "Webhook" : method.toUpperCase()}
+ {" "}
+ {method !== "event" && (
+
+ {renderServerUrl()}
+ {`${path.replace(/{([a-z0-9-_]+)}/gi, ":$1")}`}
+
+ )}
+
+
+ >
+ );
+}
+
+export default MethodEndpoint;
diff --git a/docs/src/theme/ApiExplorer/ParamOptions/ParamFormItems/ParamArrayFormItem.tsx b/docs/src/theme/ApiExplorer/ParamOptions/ParamFormItems/ParamArrayFormItem.tsx
new file mode 100644
index 00000000..b6076a4b
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/ParamOptions/ParamFormItems/ParamArrayFormItem.tsx
@@ -0,0 +1,168 @@
+import React, { useEffect, useState } from "react";
+
+import { ErrorMessage } from "@hookform/error-message";
+import { nanoid } from "@reduxjs/toolkit";
+import FormSelect from "@theme/ApiExplorer/FormSelect";
+import FormTextInput from "@theme/ApiExplorer/FormTextInput";
+import { Param, setParam } from "@theme/ApiExplorer/ParamOptions/slice";
+import { useTypedDispatch } from "@theme/ApiItem/hooks";
+import { Controller, useFormContext } from "react-hook-form";
+
+export interface ParamProps {
+ param: Param;
+}
+
+function ArrayItem({
+ param,
+ onChange,
+ initialValue,
+}: ParamProps & { onChange(value?: string): any; initialValue?: string }) {
+ const [value, setValue] = useState(initialValue || "");
+
+ if (param.schema?.items?.type === "boolean") {
+ return (
+ ) => {
+ const val = e.target.value;
+ onChange(val === "---" ? undefined : val);
+ }}
+ />
+ );
+ }
+
+ return (
+ ) => {
+ setValue(e.target.value);
+ onChange(e.target.value);
+ }}
+ />
+ );
+}
+
+export default function ParamArrayFormItem({ param }: ParamProps) {
+ const [items, setItems] = useState<{ id: string; value?: string }[]>([]);
+ const dispatch = useTypedDispatch();
+
+ const {
+ control,
+ formState: { errors },
+ } = useFormContext();
+
+ const showErrorMessage = errors?.paramArray?.message;
+
+ function handleAddItem(e: any) {
+ e.preventDefault(); // prevent form from submitting
+ setItems((i) => [
+ ...i,
+ {
+ id: nanoid(),
+ },
+ ]);
+ }
+
+ useEffect(() => {
+ const values = items
+ .map((item) => item.value)
+ .filter((item): item is string => !!item);
+
+ dispatch(
+ setParam({
+ ...param,
+ value: values.length > 0 ? values : undefined,
+ }),
+ );
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [items]);
+
+ useEffect(() => {
+ if (param.schema?.example?.length > 0) {
+ const examplesWithIds = param.schema.example.map((item: any) => ({
+ id: nanoid(),
+ value: item.toString(),
+ }));
+
+ setItems(examplesWithIds);
+ }
+ }, [param.schema.example, param.schema.length]);
+
+ function handleDeleteItem(itemToDelete: { id: string }) {
+ return () => {
+ const newItems = items.filter((i) => i.id !== itemToDelete.id);
+ setItems(newItems);
+ };
+ }
+
+ function handleChangeItem(itemToUpdate: { id: string }, onChange: any) {
+ return (value: string) => {
+ const newItems = items.map((i) => {
+ if (i.id === itemToUpdate.id) {
+ return { ...i, value: value };
+ }
+ return i;
+ });
+ setItems(newItems);
+ onChange(newItems);
+ };
+ }
+
+ return (
+ <>
+ (
+ <>
+ {items.map((item) => (
+
+
+
+
+ ))}
+
+ >
+ )}
+ />
+ {showErrorMessage && (
+ (
+ {message}
+ )}
+ />
+ )}
+ >
+ );
+}
diff --git a/docs/src/theme/ApiExplorer/ParamOptions/ParamFormItems/ParamBooleanFormItem.tsx b/docs/src/theme/ApiExplorer/ParamOptions/ParamFormItems/ParamBooleanFormItem.tsx
new file mode 100644
index 00000000..2fb3c315
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/ParamOptions/ParamFormItems/ParamBooleanFormItem.tsx
@@ -0,0 +1,57 @@
+import React from "react";
+
+import { ErrorMessage } from "@hookform/error-message";
+import FormSelect from "@theme/ApiExplorer/FormSelect";
+import { Param, setParam } from "@theme/ApiExplorer/ParamOptions/slice";
+import { useTypedDispatch } from "@theme/ApiItem/hooks";
+import { Controller, useFormContext } from "react-hook-form";
+
+export interface ParamProps {
+ param: Param;
+}
+
+export default function ParamBooleanFormItem({ param }: ParamProps) {
+ const dispatch = useTypedDispatch();
+
+ const {
+ control,
+ formState: { errors },
+ } = useFormContext();
+
+ const showErrorMessage = errors?.paramBoolean;
+
+ return (
+ <>
+ (
+ ) => {
+ const val = e.target.value;
+ dispatch(
+ setParam({
+ ...param,
+ value: val === "---" ? undefined : val,
+ }),
+ );
+ onChange(val);
+ }}
+ />
+ )}
+ />
+ {showErrorMessage && (
+ (
+ {message}
+ )}
+ />
+ )}
+ >
+ );
+}
diff --git a/docs/src/theme/ApiExplorer/ParamOptions/ParamFormItems/ParamMultiSelectFormItem.tsx b/docs/src/theme/ApiExplorer/ParamOptions/ParamFormItems/ParamMultiSelectFormItem.tsx
new file mode 100644
index 00000000..6c59f98d
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/ParamOptions/ParamFormItems/ParamMultiSelectFormItem.tsx
@@ -0,0 +1,79 @@
+import React from "react";
+
+import { ErrorMessage } from "@hookform/error-message";
+import FormMultiSelect from "@theme/ApiExplorer/FormMultiSelect";
+import { Param, setParam } from "@theme/ApiExplorer/ParamOptions/slice";
+import { useTypedDispatch, useTypedSelector } from "@theme/ApiItem/hooks";
+import { Controller, useFormContext } from "react-hook-form";
+
+export interface ParamProps {
+ param: Param;
+}
+
+export default function ParamMultiSelectFormItem({ param }: ParamProps) {
+ const {
+ control,
+ formState: { errors },
+ } = useFormContext();
+
+ const showErrorMessage = errors?.paramMultiSelect;
+
+ const dispatch = useTypedDispatch();
+
+ const options = param.schema?.items?.enum ?? [];
+
+ const pathParams = useTypedSelector((state: any) => state.params.path);
+ const queryParams = useTypedSelector((state: any) => state.params.query);
+ const cookieParams = useTypedSelector((state: any) => state.params.cookie);
+ const headerParams = useTypedSelector((state: any) => state.params.header);
+
+ const paramTypeToWatch = pathParams.length
+ ? pathParams
+ : queryParams.length
+ ? queryParams
+ : cookieParams.length
+ ? cookieParams
+ : headerParams;
+
+ const handleChange = (e: any, onChange: any) => {
+ const values = Array.prototype.filter
+ .call(e.target.options, (o) => o.selected)
+ .map((o) => o.value);
+
+ dispatch(
+ setParam({
+ ...param,
+ value: values.length > 0 ? values : undefined,
+ }),
+ );
+
+ onChange(paramTypeToWatch);
+ };
+
+ return (
+ <>
+ (
+ handleChange(e, onChange)}
+ showErrors={showErrorMessage}
+ />
+ )}
+ />
+ {showErrorMessage && (
+ (
+ {message}
+ )}
+ />
+ )}
+ >
+ );
+}
diff --git a/docs/src/theme/ApiExplorer/ParamOptions/ParamFormItems/ParamSelectFormItem.tsx b/docs/src/theme/ApiExplorer/ParamOptions/ParamFormItems/ParamSelectFormItem.tsx
new file mode 100644
index 00000000..b86a4634
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/ParamOptions/ParamFormItems/ParamSelectFormItem.tsx
@@ -0,0 +1,58 @@
+import React from "react";
+
+import { ErrorMessage } from "@hookform/error-message";
+import FormSelect from "@theme/ApiExplorer/FormSelect";
+import { Param, setParam } from "@theme/ApiExplorer/ParamOptions/slice";
+import { useTypedDispatch } from "@theme/ApiItem/hooks";
+import { Controller, useFormContext } from "react-hook-form";
+
+export interface ParamProps {
+ param: Param;
+}
+
+export default function ParamSelectFormItem({ param }: ParamProps) {
+ const {
+ control,
+ formState: { errors },
+ } = useFormContext();
+
+ const showErrorMessage = errors?.paramSelect;
+
+ const dispatch = useTypedDispatch();
+
+ const options = param.schema?.enum ?? [];
+
+ return (
+ <>
+ (
+ ) => {
+ const val = e.target.value;
+ dispatch(
+ setParam({
+ ...param,
+ value: val === "---" ? undefined : val,
+ }),
+ );
+ onChange(val);
+ }}
+ />
+ )}
+ />
+ {showErrorMessage && (
+ (
+ {message}
+ )}
+ />
+ )}
+ >
+ );
+}
diff --git a/docs/src/theme/ApiExplorer/ParamOptions/ParamFormItems/ParamTextFormItem.tsx b/docs/src/theme/ApiExplorer/ParamOptions/ParamFormItems/ParamTextFormItem.tsx
new file mode 100644
index 00000000..8309b56e
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/ParamOptions/ParamFormItems/ParamTextFormItem.tsx
@@ -0,0 +1,31 @@
+import React from "react";
+
+import FormTextInput from "@theme/ApiExplorer/FormTextInput";
+import { Param, setParam } from "@theme/ApiExplorer/ParamOptions/slice";
+import { useTypedDispatch } from "@theme/ApiItem/hooks";
+
+export interface ParamProps {
+ param: Param;
+}
+
+export default function ParamTextFormItem({ param }: ParamProps) {
+ const dispatch = useTypedDispatch();
+ return (
+ ) =>
+ dispatch(
+ setParam({
+ ...param,
+ value:
+ param.in === "path" || param.in === "query"
+ ? e.target.value.replace(/\s/g, "%20")
+ : e.target.value,
+ }),
+ )
+ }
+ />
+ );
+}
diff --git a/docs/src/theme/ApiExplorer/ParamOptions/_ParamOptions.scss b/docs/src/theme/ApiExplorer/ParamOptions/_ParamOptions.scss
new file mode 100644
index 00000000..e90d5af2
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/ParamOptions/_ParamOptions.scss
@@ -0,0 +1,123 @@
+.openapi-explorer__plus-btn--expanded {
+ transition: transform 0.2s ease;
+ display: inline-block;
+ transform: rotate(0deg);
+ transform-origin: center;
+ margin-right: 6px;
+ transform: rotate(45deg);
+}
+
+.openapi-explorer__show-more-btn {
+ width: 100%;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ margin-left: 0.25rem;
+ margin-bottom: 0.5rem;
+ padding: 0;
+ cursor: pointer;
+ font-size: var(--openapi-explorer-font-size-input);
+ user-select: none;
+ white-space: nowrap;
+ border: 0px solid transparent;
+ display: block;
+ background-color: transparent;
+ color: var(--ifm-color-primary);
+ text-align: left;
+
+ &:hover {
+ color: var(--ifm-color-primary-hover);
+ }
+
+ &:first-child {
+ margin-top: 0;
+ }
+}
+
+.openapi-explorer__delete-btn {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+
+ cursor: pointer;
+ font-size: calc(0.875rem * var(--ifm-button-size-multiplier));
+ font-weight: normal;
+ line-height: 1.5;
+
+ transition-property: color, background, border-color, box-shadow;
+ transition-duration: 100ms, 100ms, 100ms,
+ var(--ifm-button-transition-duration);
+ transition-timing-function: cubic-bezier(0.08, 0.52, 0.52, 1);
+
+ -webkit-user-select: none;
+ user-select: none;
+ white-space: nowrap;
+
+ display: flex;
+
+ align-items: center;
+ justify-content: center;
+
+ padding: 0 12px;
+
+ margin-top: calc(var(--ifm-pre-padding) / 2);
+ background-color: var(--openapi-input-background);
+ border: none;
+ outline: none;
+ color: var(--ifm-pre-color);
+ border-radius: 4px;
+ margin-left: 4px;
+
+ &:focus {
+ outline: 0;
+ }
+
+ &:active {
+ box-shadow: inset 0px 0px 0px 2px var(--openapi-input-border);
+ }
+}
+
+.openapi-explorer__thin-btn {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ cursor: pointer;
+ font-weight: bold;
+ padding: 0.5rem 1rem;
+ font-size: 12px;
+ transition-property: color, background, border-color, box-shadow;
+ transition-duration: 100ms, 100ms, 100ms,
+ var(--ifm-button-transition-duration);
+ transition-timing-function: cubic-bezier(0.08, 0.52, 0.52, 1);
+ user-select: none;
+ white-space: nowrap;
+ background-color: transparent;
+ color: var(--openapi-input-border);
+ border: 1px solid var(--openapi-input-border);
+ border-radius: var(--ifm-pre-border-radius);
+ margin-top: calc(var(--ifm-pre-padding) / 2);
+ text-transform: uppercase;
+
+ &:hover {
+ color: var(--openapi-inverse-color);
+ background-color: var(--openapi-input-border);
+ }
+
+ &:focus {
+ outline: 0;
+ }
+
+ &:active {
+ box-shadow:
+ inset 0 0 0 1px var(--openapi-input-border),
+ inset 0 0 0 2px var(--openapi-inverse-color);
+ }
+}
+
+.openapi-explorer__show-options {
+ visibility: visible;
+}
+
+.openapi-explorer__hide-options {
+ display: none;
+ visibility: hidden;
+}
diff --git a/docs/src/theme/ApiExplorer/ParamOptions/index.tsx b/docs/src/theme/ApiExplorer/ParamOptions/index.tsx
new file mode 100644
index 00000000..d0780769
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/ParamOptions/index.tsx
@@ -0,0 +1,139 @@
+import React, { useState } from "react";
+
+import FormItem from "@theme/ApiExplorer/FormItem";
+import ParamArrayFormItem from "@theme/ApiExplorer/ParamOptions/ParamFormItems/ParamArrayFormItem";
+import ParamBooleanFormItem from "@theme/ApiExplorer/ParamOptions/ParamFormItems/ParamBooleanFormItem";
+import ParamMultiSelectFormItem from "@theme/ApiExplorer/ParamOptions/ParamFormItems/ParamMultiSelectFormItem";
+import ParamSelectFormItem from "@theme/ApiExplorer/ParamOptions/ParamFormItems/ParamSelectFormItem";
+import ParamTextFormItem from "@theme/ApiExplorer/ParamOptions/ParamFormItems/ParamTextFormItem";
+import { useTypedSelector } from "@theme/ApiItem/hooks";
+
+import { Param } from "./slice";
+
+export interface ParamProps {
+ param: Param;
+}
+
+function ParamOption({ param }: ParamProps) {
+ if (param.schema?.type === "array" && param.schema.items?.enum) {
+ return ;
+ }
+
+ if (param.schema?.type === "array") {
+ return ;
+ }
+
+ if (param.schema?.enum) {
+ return ;
+ }
+
+ if (param.schema?.type === "boolean") {
+ return ;
+ }
+
+ // integer, number, string, int32, int64, float, double, object, byte, binary,
+ // date-time, date, password
+ return ;
+}
+
+function ParamOptionWrapper({ param }: ParamProps) {
+ return (
+
+
+
+ );
+}
+
+function ParamOptions() {
+ const [showOptional, setShowOptional] = useState(false);
+
+ const pathParams = useTypedSelector((state: any) => state.params.path);
+ const queryParams = useTypedSelector((state: any) => state.params.query);
+ const cookieParams = useTypedSelector((state: any) => state.params.cookie);
+ const headerParams = useTypedSelector((state: any) => state.params.header);
+
+ const allParams = [
+ ...pathParams,
+ ...queryParams,
+ ...cookieParams,
+ ...headerParams,
+ ];
+
+ const requiredParams = allParams.filter((p) => p.required);
+ const optionalParams = allParams.filter((p) => !p.required);
+
+ return (
+ <>
+ {/* Required Parameters */}
+ {requiredParams.map((param) => (
+
+ ))}
+
+ {/* Optional Parameters */}
+ {optionalParams.length > 0 && (
+ <>
+
+
+
+ {optionalParams.map((param) => (
+
+ ))}
+
+ >
+ )}
+ >
+ );
+}
+
+export default ParamOptions;
diff --git a/docs/src/theme/ApiExplorer/ParamOptions/slice.ts b/docs/src/theme/ApiExplorer/ParamOptions/slice.ts
new file mode 100644
index 00000000..52c8a72e
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/ParamOptions/slice.ts
@@ -0,0 +1,30 @@
+import { createSlice, PayloadAction } from "@reduxjs/toolkit";
+import { ParameterObject } from "docusaurus-plugin-openapi-docs/src/openapi/types";
+
+export type Param = ParameterObject & { value?: string[] | string };
+
+export interface State {
+ path: Param[];
+ query: Param[];
+ header: Param[];
+ cookie: Param[];
+}
+
+const initialState: State = {} as any;
+
+export const slice = createSlice({
+ name: "params",
+ initialState,
+ reducers: {
+ setParam: (state, action: PayloadAction) => {
+ const newParam = action.payload;
+ const paramGroup = state[action.payload.in];
+ const index = paramGroup.findIndex((p) => p.name === newParam.name);
+ paramGroup[index] = newParam;
+ },
+ },
+});
+
+export const { setParam } = slice.actions;
+
+export default slice.reducer;
diff --git a/docs/src/theme/ApiExplorer/Request/_Request.scss b/docs/src/theme/ApiExplorer/Request/_Request.scss
new file mode 100644
index 00000000..8b1d135d
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/Request/_Request.scss
@@ -0,0 +1,129 @@
+.openapi-explorer__request-form {
+ background-color: var(--ifm-pre-background);
+ border-radius: var(--openapi-card-border-radius);
+ border: 1px solid var(--openapi-explorer-border-color);
+ box-shadow:
+ 0 2px 3px hsla(222, 8%, 43%, 0.1),
+ 0 8px 16px -10px hsla(222, 8%, 43%, 0.2);
+ color: var(--ifm-pre-color);
+ line-height: var(--ifm-pre-line-height);
+ margin-bottom: var(--ifm-spacing-vertical);
+ margin-top: 0;
+ overflow: auto;
+ transition: 300ms;
+
+ /* hack for view calculation when monaco is hidden */
+ position: relative;
+
+ &:empty {
+ display: none;
+ }
+
+ &:hover {
+ box-shadow:
+ 0 0 0 2px rgba(38, 53, 61, 0.15),
+ 0 2px 3px hsla(222, 8%, 43%, 0.15),
+ 0 16px 16px -10px hsla(222, 8%, 43%, 0.2);
+ }
+
+ .required {
+ font-size: var(--ifm-code-font-size);
+ color: var(--openapi-required);
+
+ &.request-body {
+ padding-left: 0.25rem;
+ }
+ }
+}
+
+.openapi-explorer__request-header-container {
+ display: flex;
+ justify-content: space-between;
+ border-bottom: 1px solid var(--openapi-explorer-border-color);
+ margin: 0;
+ padding: 0.75rem var(--ifm-pre-padding);
+ text-transform: uppercase;
+ font-size: 12px;
+ font-weight: bold;
+}
+
+.openapi-explorer__expand-details-btn {
+ &:hover {
+ cursor: pointer;
+ }
+}
+
+.openapi-explorer__details-outer-container {
+ padding: 1rem;
+}
+
+.openapi-explorer__details-container[open] {
+ .openapi-explorer__details-summary::before {
+ transform: rotate(180deg);
+ margin-top: 0.25rem;
+ }
+}
+
+.openapi-explorer__details-summary {
+ display: inline-flex;
+ align-items: center;
+ padding: 0.35rem 0;
+ font-size: 14px;
+ list-style: none;
+
+ &:hover {
+ cursor: pointer;
+ }
+
+ &::-webkit-details-marker {
+ display: none;
+ }
+
+ &::before {
+ margin-right: 0.25rem;
+ margin-bottom: 0.25rem;
+ margin-top: 0.25rem;
+ background-image: var(--openapi-explorer-caret-bg);
+ border: none !important;
+ transform: rotate(90deg);
+ content: "";
+ height: 1rem;
+ width: 1rem;
+ }
+}
+
+.openapi-explorer__request-btn {
+ border: none;
+ border-radius: var(--ifm-global-radius);
+ padding: 0.5rem 1rem;
+ margin-top: 1rem;
+ background-color: var(--ifm-color-primary-light);
+ text-transform: uppercase;
+ font-weight: bold;
+ font-size: 12px;
+ color: white;
+ cursor: pointer;
+ transition: 300ms;
+
+ &:hover {
+ background-color: var(--ifm-color-primary-lightest);
+ }
+
+ &:active {
+ background-color: var(--ifm-color-primary-light);
+ }
+}
+
+.openapi-security__summary-container {
+ background: var(--ifm-pre-background);
+ border-radius: var(--ifm-pre-border-radius);
+}
+
+// Prevent auto zoom on mobile iOS devices when focusing on input elmenents
+@media screen and (-webkit-min-device-pixel-ratio: 0) and (max-device-width: 1024px) {
+ .prism-code,
+ select,
+ input {
+ font-size: 1rem;
+ }
+}
diff --git a/docs/src/theme/ApiExplorer/Request/index.tsx b/docs/src/theme/ApiExplorer/Request/index.tsx
new file mode 100644
index 00000000..1e971325
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/Request/index.tsx
@@ -0,0 +1,279 @@
+// @ts-nocheck
+import React, { useState } from "react";
+
+import { useDoc } from "@docusaurus/plugin-content-docs/client";
+import Accept from "@theme/ApiExplorer/Accept";
+import Authorization from "@theme/ApiExplorer/Authorization";
+import Body from "@theme/ApiExplorer/Body";
+import buildPostmanRequest from "@theme/ApiExplorer/buildPostmanRequest";
+import ContentType from "@theme/ApiExplorer/ContentType";
+import ParamOptions from "@theme/ApiExplorer/ParamOptions";
+import {
+ setResponse,
+ setCode,
+ clearCode,
+ setHeaders,
+ clearHeaders,
+} from "@theme/ApiExplorer/Response/slice";
+import Server from "@theme/ApiExplorer/Server";
+import { useTypedDispatch, useTypedSelector } from "@theme/ApiItem/hooks";
+import { ParameterObject } from "docusaurus-plugin-openapi-docs/src/openapi/types";
+import { ApiItem } from "docusaurus-plugin-openapi-docs/src/types";
+import sdk from "postman-collection";
+import { FormProvider, useForm } from "react-hook-form";
+
+import makeRequest from "./makeRequest";
+
+function Request({ item }: { item: ApiItem }) {
+ const postman = new sdk.Request(item.postman);
+ const metadata = useDoc();
+ const { proxy, hide_send_button: hideSendButton } = metadata.frontMatter;
+
+ const pathParams = useTypedSelector((state: any) => state.params.path);
+ const queryParams = useTypedSelector((state: any) => state.params.query);
+ const cookieParams = useTypedSelector((state: any) => state.params.cookie);
+ const contentType = useTypedSelector((state: any) => state.contentType.value);
+ const headerParams = useTypedSelector((state: any) => state.params.header);
+ const body = useTypedSelector((state: any) => state.body);
+ const accept = useTypedSelector((state: any) => state.accept.value);
+ const acceptOptions = useTypedDispatch((state: any) => state.accept.options);
+ const authSelected = useTypedSelector((state: any) => state.auth.selected);
+ const server = useTypedSelector((state: any) => state.server.value);
+ const serverOptions = useTypedSelector((state: any) => state.server.options);
+ const auth = useTypedSelector((state: any) => state.auth);
+ const dispatch = useTypedDispatch();
+
+ const [expandAccept, setExpandAccept] = useState(true);
+ const [expandAuth, setExpandAuth] = useState(true);
+ const [expandBody, setExpandBody] = useState(true);
+ const [expandParams, setExpandParams] = useState(true);
+ const [expandServer, setExpandServer] = useState(true);
+
+ const allParams = [
+ ...pathParams,
+ ...queryParams,
+ ...cookieParams,
+ ...headerParams,
+ ];
+
+ const postmanRequest = buildPostmanRequest(postman, {
+ queryParams,
+ pathParams,
+ cookieParams,
+ contentType,
+ accept,
+ headerParams,
+ body,
+ server,
+ auth,
+ });
+
+ const delay = (ms: number) =>
+ new Promise((resolve) => setTimeout(resolve, ms));
+
+ const paramsObject = {
+ path: [] as ParameterObject[],
+ query: [] as ParameterObject[],
+ header: [] as ParameterObject[],
+ cookie: [] as ParameterObject[],
+ };
+
+ item.parameters?.forEach(
+ (param: { in: "path" | "query" | "header" | "cookie" }) => {
+ const paramType = param.in;
+ const paramsArray: ParameterObject[] = paramsObject[paramType];
+ paramsArray.push(param as ParameterObject);
+ },
+ );
+
+ const methods = useForm({ shouldFocusError: false });
+
+ const onSubmit = async (data) => {
+ dispatch(setResponse("Fetching..."));
+ try {
+ await delay(1200);
+ const res = await makeRequest(postmanRequest, proxy, body);
+ dispatch(setResponse(await res.text()));
+ dispatch(setCode(res.status));
+ res.headers && dispatch(setHeaders(Object.fromEntries(res.headers)));
+ } catch (e: any) {
+ console.log(e);
+ dispatch(setResponse("Connection failed"));
+ dispatch(clearCode());
+ dispatch(clearHeaders());
+ }
+ };
+
+ const showServerOptions = serverOptions.length > 0;
+ const showAcceptOptions = acceptOptions.length > 1;
+ const showRequestBody = contentType !== undefined;
+ const showRequestButton = item.servers && !hideSendButton;
+ const showAuth = authSelected !== undefined;
+ const showParams = allParams.length > 0;
+ const requestBodyRequired = item.requestBody?.required;
+
+ if (
+ !showAcceptOptions &&
+ !showAuth &&
+ !showParams &&
+ !showRequestBody &&
+ !showServerOptions
+ ) {
+ return null;
+ }
+
+ const expandAllDetails = () => {
+ setExpandAccept(true);
+ setExpandAuth(true);
+ setExpandBody(true);
+ setExpandParams(true);
+ setExpandServer(true);
+ };
+
+ const collapseAllDetails = () => {
+ setExpandAccept(false);
+ setExpandAuth(false);
+ setExpandBody(false);
+ setExpandParams(false);
+ setExpandServer(false);
+ };
+
+ const allDetailsExpanded =
+ expandParams && expandBody && expandServer && expandAuth && expandAccept;
+
+ return (
+
+
+
+ );
+}
+
+export default Request;
diff --git a/docs/src/theme/ApiExplorer/Request/makeRequest.ts b/docs/src/theme/ApiExplorer/Request/makeRequest.ts
new file mode 100644
index 00000000..fec289e3
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/Request/makeRequest.ts
@@ -0,0 +1,247 @@
+import { Body } from "@theme/ApiExplorer/Body/slice";
+import sdk from "postman-collection";
+
+function fetchWithtimeout(
+ url: string,
+ options: RequestInit,
+ timeout = 5000,
+): any {
+ return Promise.race([
+ fetch(url, options),
+ new Promise((_, reject) =>
+ setTimeout(() => reject(new Error("Request timed out")), timeout),
+ ),
+ ]);
+}
+
+async function loadImage(content: Blob): Promise {
+ return new Promise((accept, reject) => {
+ const reader = new FileReader();
+
+ reader.onabort = () => {
+ console.log("file reading was aborted");
+ reject();
+ };
+
+ reader.onerror = () => {
+ console.log("file reading has failed");
+ reject();
+ };
+
+ reader.onload = () => {
+ // Do whatever you want with the file contents
+ const binaryStr = reader.result;
+ accept(binaryStr);
+ };
+ reader.readAsArrayBuffer(content);
+ });
+}
+
+async function makeRequest(
+ request: sdk.Request,
+ proxy: string | undefined,
+ _body: Body,
+) {
+ const headers = request.toJSON().header;
+
+ let myHeaders = new Headers();
+ if (headers) {
+ headers.forEach((header: any) => {
+ if (header.key && header.value) {
+ myHeaders.append(header.key, header.value);
+ }
+ });
+ }
+
+ // The following code handles multiple files in the same formdata param.
+ // It removes the form data params where the src property is an array of filepath strings
+ // Splits that array into different form data params with src set as a single filepath string
+ // TODO:
+ // if (request.body && request.body.mode === 'formdata') {
+ // let formdata = request.body.formdata,
+ // formdataArray = [];
+ // formdata.members.forEach((param) => {
+ // let key = param.key,
+ // type = param.type,
+ // disabled = param.disabled,
+ // contentType = param.contentType;
+ // // check if type is file or text
+ // if (type === 'file') {
+ // // if src is not of type string we check for array(multiple files)
+ // if (typeof param.src !== 'string') {
+ // // if src is an array(not empty), iterate over it and add files as separate form fields
+ // if (Array.isArray(param.src) && param.src.length) {
+ // param.src.forEach((filePath) => {
+ // addFormParam(
+ // formdataArray,
+ // key,
+ // param.type,
+ // filePath,
+ // disabled,
+ // contentType
+ // );
+ // });
+ // }
+ // // if src is not an array or string, or is an empty array, add a placeholder for file path(no files case)
+ // else {
+ // addFormParam(
+ // formdataArray,
+ // key,
+ // param.type,
+ // '/path/to/file',
+ // disabled,
+ // contentType
+ // );
+ // }
+ // }
+ // // if src is string, directly add the param with src as filepath
+ // else {
+ // addFormParam(
+ // formdataArray,
+ // key,
+ // param.type,
+ // param.src,
+ // disabled,
+ // contentType
+ // );
+ // }
+ // }
+ // // if type is text, directly add it to formdata array
+ // else {
+ // addFormParam(
+ // formdataArray,
+ // key,
+ // param.type,
+ // param.value,
+ // disabled,
+ // contentType
+ // );
+ // }
+ // });
+ // request.body.update({
+ // mode: 'formdata',
+ // formdata: formdataArray,
+ // });
+ // }
+
+ const body = request.body?.toJSON();
+
+ let myBody: RequestInit["body"] = undefined;
+ if (body !== undefined && Object.keys(body).length > 0) {
+ switch (body.mode) {
+ case "urlencoded": {
+ myBody = new URLSearchParams();
+ if (Array.isArray(body.urlencoded)) {
+ for (const data of body.urlencoded) {
+ if (data.key && data.value) {
+ myBody.append(data.key, data.value);
+ }
+ }
+ }
+ break;
+ }
+ case "raw": {
+ myBody = (body.raw ?? "").toString();
+ break;
+ }
+ case "formdata": {
+ // The Content-Type header will be set automatically based on the type of body.
+ myHeaders.delete("Content-Type");
+
+ myBody = new FormData();
+ if (Array.isArray(request.body.formdata.members)) {
+ for (const data of request.body.formdata.members) {
+ if (data.key && data.value.content) {
+ myBody.append(data.key, data.value.content);
+ }
+ // handle generic key-value payload
+ if (data.key && typeof data.value === "string") {
+ myBody.append(data.key, data.value);
+ }
+ }
+ }
+ break;
+ }
+ case "file": {
+ if (_body.type === "raw" && _body.content?.type === "file") {
+ myBody = await loadImage(_body.content.value.content);
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ }
+
+ const requestOptions: RequestInit = {
+ method: request.method,
+ headers: myHeaders,
+ body: myBody,
+ };
+
+ let finalUrl = request.url.toString();
+ if (proxy) {
+ // Ensure the proxy ends with a slash.
+ let normalizedProxy = proxy.replace(/\/$/, "") + "/";
+ finalUrl = normalizedProxy + request.url.toString();
+ }
+
+ return fetchWithtimeout(finalUrl, requestOptions).then((response: any) => {
+ const contentType = response.headers.get("content-type");
+ let fileExtension = "";
+
+ if (contentType) {
+ if (contentType.includes("application/pdf")) {
+ fileExtension = ".pdf";
+ } else if (contentType.includes("image/jpeg")) {
+ fileExtension = ".jpg";
+ } else if (contentType.includes("image/png")) {
+ fileExtension = ".png";
+ } else if (contentType.includes("image/gif")) {
+ fileExtension = ".gif";
+ } else if (contentType.includes("image/webp")) {
+ fileExtension = ".webp";
+ } else if (contentType.includes("video/mpeg")) {
+ fileExtension = ".mpeg";
+ } else if (contentType.includes("video/mp4")) {
+ fileExtension = ".mp4";
+ } else if (contentType.includes("audio/mpeg")) {
+ fileExtension = ".mp3";
+ } else if (contentType.includes("audio/ogg")) {
+ fileExtension = ".ogg";
+ } else if (contentType.includes("application/octet-stream")) {
+ fileExtension = ".bin";
+ } else if (contentType.includes("application/zip")) {
+ fileExtension = ".zip";
+ }
+
+ if (fileExtension) {
+ return response.blob().then((blob: any) => {
+ const url = window.URL.createObjectURL(blob);
+
+ const link = document.createElement("a");
+ link.href = url;
+ // Now the file name includes the extension
+ link.setAttribute("download", `file${fileExtension}`);
+
+ // These two lines are necessary to make the link click in Firefox
+ link.style.display = "none";
+ document.body.appendChild(link);
+
+ link.click();
+
+ // After link is clicked, it's safe to remove it.
+ setTimeout(() => document.body.removeChild(link), 0);
+
+ return response;
+ });
+ } else {
+ return response;
+ }
+ }
+
+ return response;
+ });
+}
+
+export default makeRequest;
diff --git a/docs/src/theme/ApiExplorer/Response/_Response.scss b/docs/src/theme/ApiExplorer/Response/_Response.scss
new file mode 100644
index 00000000..70b3ec01
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/Response/_Response.scss
@@ -0,0 +1,120 @@
+.openapi-explorer__response-container {
+ background-color: var(--ifm-pre-background);
+ border-radius: var(--openapi-card-border-radius);
+ border: 1px solid var(--openapi-explorer-border-color);
+ box-shadow:
+ 0 2px 3px hsla(222, 8%, 43%, 0.1),
+ 0 8px 16px -10px hsla(222, 8%, 43%, 0.2);
+ color: var(--ifm-pre-color);
+ line-height: var(--ifm-pre-line-height);
+ margin-bottom: var(--ifm-spacing-vertical);
+ margin-top: 0;
+ overflow: auto;
+ transition: 300ms;
+
+ &:hover {
+ box-shadow:
+ 0 0 0 2px rgba(38, 53, 61, 0.15),
+ 0 2px 3px hsla(222, 8%, 43%, 0.15),
+ 0 16px 16px -10px hsla(222, 8%, 43%, 0.2);
+ }
+
+ .openapi-explorer__code-block code {
+ padding-top: 0;
+ }
+}
+
+.openapi-explorer__response-title-container {
+ display: flex;
+ justify-content: space-between;
+ border-bottom: 1px solid var(--openapi-explorer-border-color);
+ margin: 0;
+ padding: 0.75rem var(--ifm-pre-padding);
+ text-transform: uppercase;
+ font-size: 12px;
+ font-weight: bold;
+}
+
+.openapi-explorer__response-placeholder-message {
+ font-size: 12px;
+ padding: 1.25rem;
+ margin-bottom: 0;
+ text-align: center;
+}
+
+.openapi-explorer__response-clear-btn {
+ &:hover {
+ cursor: pointer;
+ }
+}
+
+.openapi-explorer__loading-container {
+ width: 100%;
+ display: flex;
+ justify-content: center;
+}
+
+.openapi-response__dot::before {
+ margin-right: 0.2rem;
+ margin-bottom: 0.15rem;
+ content: "⬤";
+ color: var(--ifm-color-primary);
+ font-size: 8px;
+}
+
+.openapi-response__dot--danger::before {
+ color: var(--ifm-color-danger);
+}
+
+.openapi-response__dot--success::before {
+ color: var(--ifm-color-success);
+}
+
+.openapi-response__dot--info::before {
+ color: var(--ifm-color-info);
+}
+
+.openapi-response__status-code {
+ margin-left: -1rem;
+}
+
+.openapi-response__status-headers {
+ margin-left: -1rem;
+}
+
+.openapi-response__lds-ring {
+ display: inline-block;
+ position: relative;
+ width: 80px;
+ height: 80px;
+}
+.openapi-response__lds-ring div {
+ box-sizing: border-box;
+ display: block;
+ position: absolute;
+ width: 64px;
+ height: 64px;
+ margin: 8px;
+ border: 5px solid #dfc;
+ border-radius: 50%;
+ animation: openapi-response__lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1)
+ infinite;
+ border-color: var(--ifm-color-primary) transparent transparent transparent;
+}
+.openapi-response__lds-ring div:nth-child(1) {
+ animation-delay: -0.45s;
+}
+.openapi-response__lds-ring div:nth-child(2) {
+ animation-delay: -0.3s;
+}
+.openapi-response__lds-ring div:nth-child(3) {
+ animation-delay: -0.15s;
+}
+@keyframes openapi-response__lds-ring {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+}
diff --git a/docs/src/theme/ApiExplorer/Response/index.tsx b/docs/src/theme/ApiExplorer/Response/index.tsx
new file mode 100644
index 00000000..cf031c83
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/Response/index.tsx
@@ -0,0 +1,150 @@
+import React from "react";
+
+import { useDoc } from "@docusaurus/plugin-content-docs/client";
+import { usePrismTheme } from "@docusaurus/theme-common";
+import ApiCodeBlock from "@theme/ApiExplorer/ApiCodeBlock";
+import { useTypedDispatch, useTypedSelector } from "@theme/ApiItem/hooks";
+import SchemaTabs from "@theme/SchemaTabs";
+import TabItem from "@theme/TabItem";
+import clsx from "clsx";
+import { ApiItem } from "docusaurus-plugin-openapi-docs/src/types";
+
+import { clearResponse, clearCode, clearHeaders } from "./slice";
+
+// TODO: We probably shouldn't attempt to format XML...
+function formatXml(xml: string) {
+ const tab = " ";
+ let formatted = "";
+ let indent = "";
+
+ xml.split(/>\s*).forEach((node) => {
+ if (node.match(/^\/\w/)) {
+ // decrease indent by one 'tab'
+ indent = indent.substring(tab.length);
+ }
+ formatted += indent + "<" + node + ">\r\n";
+ if (node.match(/^\w[^>]*[^/]$/)) {
+ // increase indent
+ indent += tab;
+ }
+ });
+ return formatted.substring(1, formatted.length - 3);
+}
+
+function Response({ item }: { item: ApiItem }) {
+ const metadata = useDoc();
+ const hideSendButton = metadata.frontMatter.hide_send_button;
+ const prismTheme = usePrismTheme();
+ const code = useTypedSelector((state: any) => state.response.code);
+ const headers = useTypedSelector((state: any) => state.response.headers);
+ const response = useTypedSelector((state: any) => state.response.value);
+ const dispatch = useTypedDispatch();
+ const responseStatusClass =
+ code &&
+ "openapi-response__dot " +
+ (parseInt(code) >= 400
+ ? "openapi-response__dot--danger"
+ : parseInt(code) >= 200 && parseInt(code) < 300
+ ? "openapi-response__dot--success"
+ : "openapi-response__dot--info");
+
+ if (!item.servers || hideSendButton) {
+ return null;
+ }
+
+ let prettyResponse: string = response;
+
+ if (prettyResponse) {
+ try {
+ prettyResponse = JSON.stringify(JSON.parse(response), null, 2);
+ } catch {
+ if (response.startsWith("<")) {
+ prettyResponse = formatXml(response);
+ }
+ }
+ }
+
+ return (
+
+
+ Response
+ {
+ dispatch(clearResponse());
+ dispatch(clearCode());
+ dispatch(clearHeaders());
+ }}
+ >
+ Clear
+
+
+
+ {code && prettyResponse !== "Fetching..." ? (
+
+ {/* @ts-ignore */}
+
+ {/* @ts-ignore */}
+
+ {prettyResponse || (
+
+ Click the Send API Request button above and see
+ the response here!
+
+ )}
+
+
+ {/* @ts-ignore */}
+
+ {/* @ts-ignore */}
+
+ {JSON.stringify(headers, undefined, 2)}
+
+
+
+ ) : prettyResponse === "Fetching..." ? (
+
+
+
+
+
+
+
+
+ ) : (
+
+ Click the Send API Request button above and see the
+ response here!
+
+ )}
+
+
+ );
+}
+
+export default Response;
diff --git a/docs/src/theme/ApiExplorer/Response/slice.ts b/docs/src/theme/ApiExplorer/Response/slice.ts
new file mode 100644
index 00000000..d58f0b05
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/Response/slice.ts
@@ -0,0 +1,45 @@
+import { createSlice, PayloadAction } from "@reduxjs/toolkit";
+
+export interface State {
+ value?: string;
+ code?: string;
+ headers?: string;
+}
+
+const initialState: State = {} as any;
+
+export const slice = createSlice({
+ name: "response",
+ initialState,
+ reducers: {
+ setResponse: (state, action: PayloadAction) => {
+ state.value = action.payload;
+ },
+ setCode: (state, action: PayloadAction) => {
+ state.code = action.payload;
+ },
+ setHeaders: (state, action: PayloadAction) => {
+ state.headers = action.payload;
+ },
+ clearResponse: (state) => {
+ state.value = undefined;
+ },
+ clearCode: (state) => {
+ state.code = undefined;
+ },
+ clearHeaders: (state) => {
+ state.headers = undefined;
+ },
+ },
+});
+
+export const {
+ setResponse,
+ clearResponse,
+ setCode,
+ clearCode,
+ setHeaders,
+ clearHeaders,
+} = slice.actions;
+
+export default slice.reducer;
diff --git a/docs/src/theme/ApiExplorer/SecuritySchemes/index.tsx b/docs/src/theme/ApiExplorer/SecuritySchemes/index.tsx
new file mode 100644
index 00000000..2e03a0c6
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/SecuritySchemes/index.tsx
@@ -0,0 +1,280 @@
+import React from "react";
+
+import Link from "@docusaurus/Link";
+import { useTypedSelector } from "@theme/ApiItem/hooks";
+
+function SecuritySchemes(props: any) {
+ const options = useTypedSelector((state: any) => state.auth.options);
+ const selected = useTypedSelector((state: any) => state.auth.selected);
+ const infoAuthPath = `/${props.infoPath}#authentication`;
+
+ if (selected === undefined) return null;
+
+ if (options[selected]?.[0]?.type === undefined) {
+ return null;
+ }
+
+ const selectedAuth = options[selected];
+ return (
+
+
+
+ Authorization: {selectedAuth[0].name ?? selectedAuth[0].type}
+
+
+ {selectedAuth.map((auth: any) => {
+ const isHttp = auth.type === "http";
+ const isApiKey = auth.type === "apiKey";
+ const isOauth2 = auth.type === "oauth2";
+ const isOpenId = auth.type === "openIdConnect";
+
+ if (isHttp) {
+ if (auth.scheme === "bearer") {
+ const { name, key, type, scopes, ...rest } = auth;
+ return (
+
+
+
+ name:{" "}
+ {name ?? key}
+
+
+ type:
+ {type}
+
+ {scopes && scopes.length > 0 && (
+
+ scopes:
+
+ {auth.scopes.length > 0 ? auth.scopes.toString() : "[]"}
+
+
+ )}
+ {Object.keys(rest).map((k, i) => {
+ return (
+
+ {k}:
+ {typeof rest[k] === "object"
+ ? JSON.stringify(rest[k], null, 2)
+ : String(rest[k])}
+
+ );
+ })}
+
+
+ );
+ }
+ if (auth.scheme === "basic") {
+ const { name, key, type, scopes, ...rest } = auth;
+ return (
+
+
+
+ name:{" "}
+ {name ?? key}
+
+
+ type:
+ {type}
+
+ {scopes && scopes.length > 0 && (
+
+ scopes:
+
+ {auth.scopes.length > 0 ? auth.scopes.toString() : "[]"}
+
+
+ )}
+ {Object.keys(rest).map((k, i) => {
+ return (
+
+ {k}:
+ {typeof rest[k] === "object"
+ ? JSON.stringify(rest[k], null, 2)
+ : String(rest[k])}
+
+ );
+ })}
+
+
+ );
+ }
+ return (
+
+
+
+ name:{" "}
+ {auth.name ?? auth.key}
+
+
+ type:
+ {auth.type}
+
+
+ in:
+ {auth.in}
+
+
+
+ );
+ }
+
+ if (isApiKey) {
+ const { name, key, type, scopes, ...rest } = auth;
+ return (
+
+
+
+ name:{" "}
+ {name ?? key}
+
+
+ type:
+ {type}
+
+ {scopes && scopes.length > 0 && (
+
+ scopes:
+
+ {auth.scopes.length > 0 ? auth.scopes.toString() : "[]"}
+
+
+ )}
+ {Object.keys(rest).map((k, i) => {
+ return (
+
+ {k}:
+ {typeof rest[k] === "object"
+ ? JSON.stringify(rest[k], null, 2)
+ : String(rest[k])}
+
+ );
+ })}
+
+
+ );
+ }
+
+ if (isOauth2) {
+ const { name, key, type, scopes, flows, ...rest } = auth;
+ return (
+
+
+
+ name:{" "}
+ {name ?? key}
+
+
+ type:
+ {type}
+
+ {scopes && scopes.length > 0 && (
+
+ scopes:
+
+ {auth.scopes.length > 0 ? auth.scopes.toString() : "[]"}
+
+
+ )}
+ {Object.keys(rest).map((k, i) => {
+ return (
+
+ {k}:
+ {typeof rest[k] === "object"
+ ? JSON.stringify(rest[k], null, 2)
+ : String(rest[k])}
+
+ );
+ })}
+ {flows && (
+
+
+ flows:
+ {JSON.stringify(flows, null, 2)}
+
+
+ )}
+
+
+ );
+ }
+
+ if (isOpenId) {
+ const { name, key, scopes, type, ...rest } = auth;
+ return (
+
+
+
+ name:{" "}
+ {name ?? key}
+
+
+ type:
+ {type}
+
+ {scopes && scopes.length > 0 && (
+
+ scopes:
+
+ {auth.scopes.length > 0 ? auth.scopes.toString() : "[]"}
+
+
+ )}
+ {Object.keys(rest).map((k, i) => {
+ return (
+
+ {k}:
+ {typeof rest[k] === "object"
+ ? JSON.stringify(rest[k], null, 2)
+ : String(rest[k])}
+
+ );
+ })}
+
+
+ );
+ }
+
+ return undefined;
+ })}
+
+ );
+}
+
+export default SecuritySchemes;
diff --git a/docs/src/theme/ApiExplorer/Server/_Server.scss b/docs/src/theme/ApiExplorer/Server/_Server.scss
new file mode 100644
index 00000000..c6bbb7d7
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/Server/_Server.scss
@@ -0,0 +1,26 @@
+.openapi-explorer__server-container {
+ background: var(--openapi-card-background-color);
+ border-radius: var(--openapi-card-border-radius);
+ color: var(--ifm-pre-color);
+ line-height: var(--ifm-pre-line-height);
+ margin-bottom: var(--ifm-spacing-vertical);
+ margin-top: 0;
+ overflow: auto;
+
+ /* hack for view calculation when monaco is hidden */
+ position: relative;
+
+ &:empty {
+ display: none;
+ }
+}
+
+.openapi-explorer__server-url {
+ font-size: var(--openapi-explorer-font-size-input);
+ font-family: var(--ifm-font-family-monospace);
+}
+
+.openapi-explorer__server-description {
+ padding-left: 0.5rem;
+ font-weight: var(--ifm-font-weight-bold);
+}
diff --git a/docs/src/theme/ApiExplorer/Server/index.tsx b/docs/src/theme/ApiExplorer/Server/index.tsx
new file mode 100644
index 00000000..bd0140e7
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/Server/index.tsx
@@ -0,0 +1,123 @@
+import React, { useState } from "react";
+
+import FloatingButton from "@theme/ApiExplorer/FloatingButton";
+import FormItem from "@theme/ApiExplorer/FormItem";
+import FormSelect from "@theme/ApiExplorer/FormSelect";
+import FormTextInput from "@theme/ApiExplorer/FormTextInput";
+import { useTypedDispatch, useTypedSelector } from "@theme/ApiItem/hooks";
+
+import { setServer, setServerVariable } from "./slice";
+
+function Server() {
+ const [isEditing, setIsEditing] = useState(false);
+ const value = useTypedSelector((state: any) => state.server.value);
+ const options = useTypedSelector((state: any) => state.server.options);
+ const dispatch = useTypedDispatch();
+
+ if (options.length <= 0) {
+ return null;
+ }
+
+ if (options.length < 1 && value?.variables === undefined) {
+ return null;
+ }
+
+ if (!value) {
+ const defaultOption = options[0];
+ dispatch(setServer(JSON.stringify(defaultOption)));
+ }
+
+ // Default to first option when existing server state is mismatched
+ if (value) {
+ const urlExists = options.find((s: any) => s.url === value.url);
+ if (!urlExists) {
+ const defaultOption = options[0];
+ dispatch(setServer(JSON.stringify(defaultOption)));
+ }
+ }
+
+ if (!isEditing) {
+ let url = "";
+ if (value) {
+ url = value.url.replace(/\/$/, "");
+ if (value.variables) {
+ Object.keys(value.variables).forEach((variable) => {
+ url = url.replace(
+ `{${variable}}`,
+ value.variables?.[variable].default ?? "",
+ );
+ });
+ }
+ }
+ return (
+ setIsEditing(true)} label="Edit">
+
+
+ {url}
+
+
+
+ );
+ }
+ return (
+
+ setIsEditing(false)} label="Hide">
+
+ s.url)}
+ onChange={(e: React.ChangeEvent) => {
+ dispatch(
+ setServer(
+ JSON.stringify(
+ options.filter((s: any) => s.url === e.target.value)[0],
+ ),
+ ),
+ );
+ }}
+ value={value?.url}
+ />
+
+ {value?.description}
+
+
+ {value?.variables &&
+ Object.keys(value.variables).map((key) => {
+ if (value.variables?.[key].enum !== undefined) {
+ return (
+
+ ) => {
+ dispatch(
+ setServerVariable(
+ JSON.stringify({ key, value: e.target.value }),
+ ),
+ );
+ }}
+ value={value?.variables[key].default}
+ />
+
+ );
+ }
+ return (
+
+ ) => {
+ dispatch(
+ setServerVariable(
+ JSON.stringify({ key, value: e.target.value }),
+ ),
+ );
+ }}
+ value={value?.variables?.[key].default}
+ />
+
+ );
+ })}
+
+
+ );
+}
+
+export default Server;
diff --git a/docs/src/theme/ApiExplorer/Server/slice.ts b/docs/src/theme/ApiExplorer/Server/slice.ts
new file mode 100644
index 00000000..ffb2b4fc
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/Server/slice.ts
@@ -0,0 +1,32 @@
+import { createSlice, PayloadAction } from "@reduxjs/toolkit";
+import { ServerObject } from "docusaurus-plugin-openapi-docs/src/openapi/types";
+// TODO: we might want to export this
+
+export interface State {
+ value?: ServerObject;
+ options: ServerObject[];
+}
+
+const initialState: State = {} as any;
+
+export const slice = createSlice({
+ name: "server",
+ initialState,
+ reducers: {
+ setServer: (state, action: PayloadAction) => {
+ state.value = state.options.find(
+ (s) => s.url === JSON.parse(action.payload).url,
+ );
+ },
+ setServerVariable: (state, action: PayloadAction) => {
+ if (state.value?.variables) {
+ const parsedPayload = JSON.parse(action.payload);
+ state.value.variables[parsedPayload.key].default = parsedPayload.value;
+ }
+ },
+ },
+});
+
+export const { setServer, setServerVariable } = slice.actions;
+
+export default slice.reducer;
diff --git a/docs/src/theme/ApiExplorer/buildPostmanRequest.ts b/docs/src/theme/ApiExplorer/buildPostmanRequest.ts
new file mode 100644
index 00000000..f26ae7f2
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/buildPostmanRequest.ts
@@ -0,0 +1,525 @@
+import { AuthState, Scheme } from "@theme/ApiExplorer/Authorization/slice";
+import { Body, Content } from "@theme/ApiExplorer/Body/slice";
+import {
+ ParameterObject,
+ ServerObject,
+} from "docusaurus-plugin-openapi-docs/src/openapi/types";
+import cloneDeep from "lodash/cloneDeep";
+import sdk from "postman-collection";
+
+type Param = {
+ value?: string | string[];
+} & ParameterObject;
+
+function setQueryParams(postman: sdk.Request, queryParams: Param[]) {
+ postman.url.query.clear();
+
+ const qp = queryParams
+ .map((param) => {
+ if (!param.value) {
+ return undefined;
+ }
+
+ // Handle array values
+ if (Array.isArray(param.value)) {
+ if (param.style === "spaceDelimited") {
+ return new sdk.QueryParam({
+ key: param.name,
+ value: param.value.join(" "),
+ });
+ } else if (param.style === "pipeDelimited") {
+ return new sdk.QueryParam({
+ key: param.name,
+ value: param.value.join("|"),
+ });
+ } else if (param.explode) {
+ return param.value.map(
+ (val) =>
+ new sdk.QueryParam({
+ key: param.name,
+ value: val,
+ }),
+ );
+ } else {
+ return new sdk.QueryParam({
+ key: param.name,
+ value: param.value.join(","),
+ });
+ }
+ }
+
+ const decodedValue = decodeURI(param.value);
+ const tryJson = () => {
+ try {
+ return JSON.parse(decodedValue);
+ } catch (e) {
+ return false;
+ }
+ };
+
+ const jsonResult = tryJson();
+
+ // Handle object values
+ if (jsonResult && typeof jsonResult === "object") {
+ if (param.style === "deepObject") {
+ return Object.entries(jsonResult).map(
+ ([key, val]) =>
+ new sdk.QueryParam({
+ key: `${param.name}[${key}]`,
+ value: val,
+ }),
+ );
+ } else if (param.explode) {
+ return Object.entries(jsonResult).map(
+ ([key, val]) =>
+ new sdk.QueryParam({
+ key: key,
+ value: val,
+ }),
+ );
+ } else {
+ return new sdk.QueryParam({
+ key: param.name,
+ value: Object.entries(jsonResult)
+ .map(([key, val]) => `${key},${val}`)
+ .join(","),
+ });
+ }
+ }
+
+ // Handle boolean values
+ if (typeof decodedValue === "boolean") {
+ return new sdk.QueryParam({
+ key: param.name,
+ value: decodedValue ? "true" : "false",
+ });
+ }
+
+ // Parameter allows empty value: "/hello?extended"
+ if (param.allowEmptyValue) {
+ if (decodedValue === "true") {
+ return new sdk.QueryParam({
+ key: param.name,
+ value: null,
+ });
+ }
+ return undefined;
+ }
+
+ return new sdk.QueryParam({
+ key: param.name,
+ value: param.value,
+ });
+ })
+ .flat() // Flatten the array in case of nested arrays from map
+ .filter((item) => item !== undefined);
+
+ if (qp.length > 0) {
+ postman.addQueryParams(qp);
+ }
+}
+
+function setPathParams(postman: sdk.Request, pathParams: Param[]) {
+ // Map through the path parameters
+ const source = pathParams.map((param) => {
+ if (!param.value) {
+ return undefined;
+ }
+
+ let serializedValue;
+
+ // Handle different styles
+ if (Array.isArray(param.value)) {
+ if (param.style === "label") {
+ serializedValue = `.${param.value.join(".")}`;
+ } else if (param.style === "matrix") {
+ serializedValue = `;${param.name}=${param.value.join(";")}`;
+ } else {
+ serializedValue = param.value.join(",");
+ }
+ return new sdk.Variable({
+ key: param.name,
+ value: serializedValue,
+ });
+ }
+
+ const decodedValue = decodeURI(param.value);
+ const tryJson = () => {
+ try {
+ return JSON.parse(decodedValue);
+ } catch (e) {
+ return false;
+ }
+ };
+
+ const jsonResult = tryJson();
+
+ if (typeof jsonResult === "object") {
+ if (param.style === "matrix") {
+ serializedValue = Object.entries(jsonResult)
+ .map(([key, val]) => `;${key}=${val}`)
+ .join("");
+ } else {
+ serializedValue = Object.entries(jsonResult)
+ .map(([key, val]) => `${key}=${val}`)
+ .join(",");
+ }
+ } else {
+ serializedValue = decodedValue || `:${param.name}`;
+ }
+
+ return new sdk.Variable({
+ key: param.name,
+ value: serializedValue,
+ });
+ });
+
+ postman.url.variables.assimilate(source, false);
+}
+
+function buildCookie(cookieParams: Param[]) {
+ const cookies = cookieParams
+ .map((param) => {
+ if (param.value) {
+ const decodedValue = decodeURI(param.value as string);
+ const tryJson = () => {
+ try {
+ return JSON.parse(decodedValue);
+ } catch (e) {
+ return false;
+ }
+ };
+
+ const jsonResult = tryJson();
+ if (typeof jsonResult === "object") {
+ if (param.style === "form") {
+ // Handle form style
+ if (param.explode) {
+ // Serialize each key-value pair as a separate cookie
+ return Object.entries(jsonResult).map(
+ ([key, val]) =>
+ new sdk.Cookie({
+ key: key,
+ value: val,
+ }),
+ );
+ } else {
+ // Serialize the object as a single cookie with key-value pairs joined by commas
+ return new sdk.Cookie({
+ key: param.name,
+ value: Object.entries(jsonResult)
+ .map(([key, val]) => `${key},${val}`)
+ .join(","),
+ });
+ }
+ }
+ } else {
+ // Handle scalar values
+ return new sdk.Cookie({
+ key: param.name,
+ value: param.value,
+ });
+ }
+ }
+ return undefined;
+ })
+ .flat() // Flatten the array in case of nested arrays from map
+ .filter((item) => item !== undefined);
+
+ const list = new sdk.CookieList(null, cookies);
+ return list.toString();
+}
+
+function setHeaders(
+ postman: sdk.Request,
+ contentType: string,
+ accept: string,
+ cookie: string,
+ headerParams: Param[],
+ other: { key: string; value: string }[],
+) {
+ postman.headers.clear();
+
+ if (contentType) {
+ postman.addHeader({ key: "Content-Type", value: contentType });
+ }
+
+ if (accept) {
+ postman.addHeader({ key: "Accept", value: accept });
+ }
+
+ headerParams.forEach((param) => {
+ if (param.value) {
+ const decodedValue = decodeURI(param.value as string);
+ const tryJson = () => {
+ try {
+ return JSON.parse(decodedValue);
+ } catch (e) {
+ return false;
+ }
+ };
+
+ const jsonResult = tryJson();
+ if (Array.isArray(param.value)) {
+ if (param.style === "simple") {
+ if (param.explode) {
+ // Each item in the array is a separate header
+ jsonResult.forEach((val: any) => {
+ postman.addHeader({ key: param.name, value: val });
+ });
+ } else {
+ // Array values are joined by commas
+ postman.addHeader({
+ key: param.name,
+ value: param.value.join(","),
+ });
+ }
+ }
+ } else if (typeof jsonResult === "object") {
+ if (param.style === "simple") {
+ if (param.explode) {
+ // Each key-value pair in the object is a separate header
+ Object.entries(jsonResult).forEach(([key, val]) => {
+ postman.addHeader({ key: param.name, value: `${key}=${val}` });
+ });
+ } else {
+ // Object is serialized as a single header with key-value pairs joined by commas
+ postman.addHeader({
+ key: param.name,
+ value: Object.entries(jsonResult)
+ .map(([key, val]) => `${key},${val}`)
+ .join(","),
+ });
+ }
+ }
+ } else {
+ // Handle scalar values
+ postman.addHeader({ key: param.name, value: param.value });
+ }
+ }
+ });
+
+ other.forEach((header) => {
+ postman.addHeader(header);
+ });
+
+ if (cookie) {
+ postman.addHeader({ key: "Cookie", value: cookie });
+ }
+}
+
+// TODO: this is all a bit hacky
+function setBody(clonedPostman: sdk.Request, body: Body) {
+ if (clonedPostman.body === undefined) {
+ return;
+ }
+
+ if (body.type === "empty") {
+ clonedPostman.body = undefined;
+ return;
+ }
+
+ if (body.type === "raw" && body.content?.type === "file") {
+ // treat it like file.
+ clonedPostman.body.mode = "file";
+ clonedPostman.body.file = { src: body.content.value.src };
+ return;
+ }
+
+ 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") {
+ clonedPostman.body = undefined;
+ return;
+ }
+ clonedPostman.body.raw = body.content?.value ?? "";
+ return;
+ }
+ case "formdata": {
+ clonedPostman.body.formdata?.clear();
+ if (body.type !== "form") {
+ // treat it like raw.
+ clonedPostman.body.mode = "raw";
+ clonedPostman.body.raw = `${body.content?.value}`;
+ return;
+ }
+ const params = Object.entries(body.content)
+ .filter((entry): entry is [string, NonNullable] => !!entry[1])
+ .map(([key, content]) => {
+ if (content.type === "file") {
+ return new sdk.FormParam({ key: key, ...content });
+ }
+ return new sdk.FormParam({ key: key, value: content.value });
+ });
+ clonedPostman.body.formdata?.assimilate(params, false);
+ return;
+ }
+ case "urlencoded": {
+ clonedPostman.body.urlencoded?.clear();
+ if (body.type !== "form") {
+ // treat it like raw.
+ clonedPostman.body.mode = "raw";
+ clonedPostman.body.raw = `${body.content?.value}`;
+ return;
+ }
+ const params = Object.entries(body.content)
+ .filter((entry): entry is [string, NonNullable] => !!entry[1])
+ .map(([key, content]) => {
+ if (content.type !== "file" && content.value) {
+ return new sdk.QueryParam({ key: key, value: content.value });
+ }
+ return undefined;
+ })
+ .filter((item): item is sdk.QueryParam => item !== undefined);
+ clonedPostman.body.urlencoded?.assimilate(params, false);
+ return;
+ }
+ default:
+ return;
+ }
+}
+
+// TODO: finish these types
+interface Options {
+ server?: ServerObject;
+ queryParams: Param[];
+ pathParams: Param[];
+ cookieParams: Param[];
+ headerParams: Param[];
+ contentType: string;
+ accept: string;
+ body: Body;
+ auth: AuthState;
+}
+
+function buildPostmanRequest(
+ postman: sdk.Request,
+ {
+ queryParams,
+ pathParams,
+ cookieParams,
+ contentType,
+ accept,
+ headerParams,
+ body,
+ server,
+ auth,
+ }: Options,
+) {
+ const clonedPostman = cloneDeep(postman);
+
+ clonedPostman.url.protocol = undefined;
+ clonedPostman.url.host = [window.location.origin];
+
+ if (server) {
+ let url = server.url.replace(/\/$/, "");
+ const variables = server.variables;
+ if (variables) {
+ Object.keys(variables).forEach((variable) => {
+ url = url.replace(`{${variable}}`, variables[variable].default);
+ });
+ }
+ clonedPostman.url.host = [url];
+ }
+
+ setQueryParams(clonedPostman, queryParams);
+ setPathParams(
+ clonedPostman,
+ pathParams.map((path) => ({
+ ...path,
+ value: path.value || path.schema.example,
+ })),
+ );
+
+ const cookie = buildCookie(cookieParams);
+ let otherHeaders = [];
+
+ let selectedAuth: Scheme[] = [];
+ if (auth.selected !== undefined) {
+ selectedAuth = auth.options[auth.selected];
+ }
+
+ for (const a of selectedAuth) {
+ // Bearer Auth
+ if (a.type === "http" && a.scheme === "bearer") {
+ const { token } = auth.data[a.key];
+ if (token === undefined) {
+ otherHeaders.push({
+ key: "Authorization",
+ value: "Bearer ",
+ });
+ continue;
+ }
+ otherHeaders.push({
+ key: "Authorization",
+ value: `Bearer ${token}`,
+ });
+ continue;
+ }
+
+ if (a.type === "oauth2") {
+ let token;
+ if (auth.data[a.key]) {
+ token = auth.data[a.key].token;
+ }
+ if (token === undefined) {
+ otherHeaders.push({
+ key: "Authorization",
+ value: "Bearer ",
+ });
+ continue;
+ }
+ otherHeaders.push({
+ key: "Authorization",
+ value: `Bearer ${token}`,
+ });
+ continue;
+ }
+
+ // Basic Auth
+ if (a.type === "http" && a.scheme === "basic") {
+ const { username, password } = auth.data[a.key];
+ if (username === undefined || password === undefined) {
+ continue;
+ }
+ otherHeaders.push({
+ key: "Authorization",
+ value: `Basic ${window.btoa(`${username}:${password}`)}`,
+ });
+ continue;
+ }
+
+ // API Key
+ if (a.type === "apiKey" && a.in === "header") {
+ const { apiKey } = auth.data[a.key];
+ if (apiKey === undefined) {
+ otherHeaders.push({
+ key: a.name,
+ value: `<${a.name ?? a.type}>`,
+ });
+ continue;
+ }
+ otherHeaders.push({
+ key: a.name,
+ value: apiKey,
+ });
+ continue;
+ }
+ }
+
+ setHeaders(
+ clonedPostman,
+ contentType,
+ accept,
+ cookie,
+ headerParams,
+ otherHeaders,
+ );
+
+ setBody(clonedPostman, body);
+
+ return clonedPostman;
+}
+
+export default buildPostmanRequest;
diff --git a/docs/src/theme/ApiExplorer/index.tsx b/docs/src/theme/ApiExplorer/index.tsx
new file mode 100644
index 00000000..0de525af
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/index.tsx
@@ -0,0 +1,34 @@
+import React from "react";
+
+import CodeSnippets from "@theme/ApiExplorer/CodeSnippets";
+import Request from "@theme/ApiExplorer/Request";
+import Response from "@theme/ApiExplorer/Response";
+import SecuritySchemes from "@theme/ApiExplorer/SecuritySchemes";
+import { ApiItem } from "docusaurus-plugin-openapi-docs/src/types";
+import sdk from "postman-collection";
+
+function ApiExplorer({
+ item,
+ infoPath,
+}: {
+ item: NonNullable;
+ infoPath: string;
+}) {
+ const postman = new sdk.Request(item.postman);
+
+ return (
+ <>
+
+
+
+ {item.method !== "event" && (
+
+ )}
+ >
+ );
+}
+
+export default ApiExplorer;
diff --git a/docs/src/theme/ApiExplorer/persistanceMiddleware.ts b/docs/src/theme/ApiExplorer/persistanceMiddleware.ts
new file mode 100644
index 00000000..3b1bde35
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/persistanceMiddleware.ts
@@ -0,0 +1,64 @@
+import { Middleware } from "@reduxjs/toolkit";
+import {
+ setAuthData,
+ setSelectedAuth,
+} from "@theme/ApiExplorer/Authorization/slice";
+import { AppDispatch, RootState } from "@theme/ApiItem/store";
+/* eslint-disable import/no-extraneous-dependencies*/
+import { ThemeConfig } from "docusaurus-theme-openapi-docs/src/types";
+
+import { createStorage, hashArray } from "./storage-utils";
+
+export function createPersistanceMiddleware(options: ThemeConfig["api"]) {
+ const persistanceMiddleware: Middleware<{}, RootState, AppDispatch> =
+ (storeAPI) => (next) => (action) => {
+ const result = next(action);
+
+ const state = storeAPI.getState();
+
+ const storage = createStorage("sessionStorage");
+
+ if (action.type === setAuthData.type) {
+ for (const [key, value] of Object.entries(state.auth.data)) {
+ if (Object.values(value as any).filter(Boolean).length > 0) {
+ storage.setItem(key, JSON.stringify(value));
+ } else {
+ storage.removeItem(key);
+ }
+ }
+ }
+
+ if (action.type === setSelectedAuth.type) {
+ if (state.auth.selected) {
+ storage.setItem(
+ hashArray(Object.keys(state.auth.options)),
+ state.auth.selected,
+ );
+ }
+ }
+
+ // TODO: determine way to rehydrate without flashing
+ if (action.type === "contentType/setContentType") {
+ storage.setItem("contentType", action.payload);
+ }
+
+ if (action.type === "accept/setAccept") {
+ storage.setItem("accept", action.payload);
+ }
+
+ if (action.type === "server/setServer") {
+ storage.setItem("server", action.payload);
+ }
+
+ if (action.type === "server/setServerVariable") {
+ const server = storage.getItem("server") ?? "{}";
+ const variables = JSON.parse(action.payload);
+ let serverObject = JSON.parse(server);
+ serverObject.variables[variables.key].default = variables.value;
+ storage.setItem("server", JSON.stringify(serverObject));
+ }
+
+ return result;
+ };
+ return persistanceMiddleware;
+}
diff --git a/docs/src/theme/ApiExplorer/postman-collection.d.ts b/docs/src/theme/ApiExplorer/postman-collection.d.ts
new file mode 100644
index 00000000..9a49b232
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/postman-collection.d.ts
@@ -0,0 +1,3 @@
+declare module "postman-collection" {
+ export = Request.sdk;
+}
diff --git a/docs/src/theme/ApiExplorer/react-modal.d.ts b/docs/src/theme/ApiExplorer/react-modal.d.ts
new file mode 100644
index 00000000..2ba6abf7
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/react-modal.d.ts
@@ -0,0 +1 @@
+declare module "react-modal";
diff --git a/docs/src/theme/ApiExplorer/storage-utils.ts b/docs/src/theme/ApiExplorer/storage-utils.ts
new file mode 100644
index 00000000..298a5938
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/storage-utils.ts
@@ -0,0 +1,32 @@
+import crypto from "crypto-js";
+
+export function hashArray(arr: string[]) {
+ function hash(message: string) {
+ return crypto.SHA1(message).toString();
+ }
+ const hashed = arr.map((item) => hash(item));
+ hashed.sort();
+ const res = hashed.join();
+ return hash(res);
+}
+
+type Persistance = false | "localStorage" | "sessionStorage" | undefined;
+
+export function createStorage(persistance: Persistance): Storage {
+ if (persistance === false) {
+ return {
+ getItem: () => null,
+ setItem: () => {},
+ clear: () => {},
+ key: () => null,
+ removeItem: () => {},
+ length: 0,
+ };
+ }
+
+ if (persistance === "sessionStorage") {
+ return sessionStorage;
+ }
+
+ return localStorage;
+}