From f162b4a2a1ef611474e8fb4006f7d28f3c223451 Mon Sep 17 00:00:00 2001 From: Philip Korsholm <88927411+pKorsholm@users.noreply.github.com> Date: Fri, 19 Nov 2021 10:18:16 +0100 Subject: [PATCH] Feat/api reference curl examples (#720) * curl reference initial * add tooltips to curl command * refactor copy component * move copy to copy component * formatting * always include required fields * add example values * format svg * fix property extraction * explainer comment --- www/reference/package.json | 2 + www/reference/src/assets/clipboard.svg | 16 ++ .../src/components/CopyToClipboard/index.js | 76 +++++++++ .../src/components/content/code-box.js | 25 ++- www/reference/src/components/content/index.js | 2 +- .../src/components/content/json-container.js | 17 +- .../src/components/content/method.js | 155 ++++++++++++++++-- .../src/components/content/parameters.js | 115 +++++++------ .../src/components/content/section.js | 7 +- .../src/components/icons/clipboard.js | 19 +++ www/reference/src/utils/format-parameters.js | 13 +- 11 files changed, 369 insertions(+), 78 deletions(-) create mode 100644 www/reference/src/assets/clipboard.svg create mode 100644 www/reference/src/components/CopyToClipboard/index.js create mode 100644 www/reference/src/components/icons/clipboard.js diff --git a/www/reference/package.json b/www/reference/package.json index 862da9e6e3..e5c4b3c1f0 100644 --- a/www/reference/package.json +++ b/www/reference/package.json @@ -38,10 +38,12 @@ "preact-render-to-string": "^5.1.19", "prismjs": "^1.24.1", "react": "^16.12.0", + "copy-to-clipboard": "^3.3.1", "react-collapsible": "^2.8.1", "react-dom": "^16.12.0", "react-helmet": "^6.1.0", "react-highlight.js": "^1.0.7", + "react-tooltip": "^4.2.10", "react-intersection-observer": "^8.29.0", "react-markdown": "^5.0.3", "react-virtualized": "^9.22.3", diff --git a/www/reference/src/assets/clipboard.svg b/www/reference/src/assets/clipboard.svg new file mode 100644 index 0000000000..d370442575 --- /dev/null +++ b/www/reference/src/assets/clipboard.svg @@ -0,0 +1,16 @@ + + + Clipboard + + + + + + + + + + + diff --git a/www/reference/src/components/CopyToClipboard/index.js b/www/reference/src/components/CopyToClipboard/index.js new file mode 100644 index 0000000000..44299513f6 --- /dev/null +++ b/www/reference/src/components/CopyToClipboard/index.js @@ -0,0 +1,76 @@ +import React, { useState } from "react" +import { Flex, Box, Text } from "theme-ui" +import Clipboard from '../icons/clipboard' +import ReactTooltip from "react-tooltip" +import styled from "@emotion/styled" +import copy from 'copy-to-clipboard' + +const StyledTooltip = ({id, text}) => { + const StyledTooltip = styled(ReactTooltip)` + box-shadow: 0px 5px 15px 0px rgba(0, 0, 0, 0.08), + 0px 0px 0px 1px rgba(136, 152, 170, 0.1), + 0px 4px 4px 0px rgba(136, 152, 170, 0.1) !important; + padding: 8px 12px; + &:after { + margin-right: 4px; + } + &.show { + opacity: 1; + } + ` + return( + + {text} + + ) +} + +const CopyToClipboard = ({text, copyText, tooltipText}) => { + const [copied, setCopied] = useState(false) + const id = (Math.random()*1000000).toString() + const forceTooltipRemount = copied ? "content-1" : "content-2" + + const onCopyClicked = () => { + copy(copyText || tooltipText, {format: 'text/plain'}) + } + + return ( + {setCopied(false)}} + onClick={() => { + setCopied(true) + onCopyClicked() + }} + data-for={id} + data-tip={forceTooltipRemount} + key={forceTooltipRemount} + sx={{cursor: 'pointer'}} + > + { + text && ( + + {text.toUpperCase()} + ) + } + + {copied ? + + : + + } + ) +} + +export default CopyToClipboard \ No newline at end of file diff --git a/www/reference/src/components/content/code-box.js b/www/reference/src/components/content/code-box.js index 2b959be221..5647752f14 100644 --- a/www/reference/src/components/content/code-box.js +++ b/www/reference/src/components/content/code-box.js @@ -1,7 +1,8 @@ import React from "react" import { Flex, Box, Text } from "theme-ui" +import CopyToClipboard from "../CopyToClipboard" -const CodeBox = ({ header, children }) => { +const CodeBox = ({ header, children, shell, content, allowCopy }) => { return ( { borderRadius: "8px 8px 0 0", }} > - - {header} - + + + {header.toUpperCase()} + + {allowCopy ? ( + + ) : ( + <> + )} + { >
{data.sections.map((s, i) => { - return
+ return
})}
diff --git a/www/reference/src/components/content/json-container.js b/www/reference/src/components/content/json-container.js index 5dc9e2ed20..1714a121b1 100644 --- a/www/reference/src/components/content/json-container.js +++ b/www/reference/src/components/content/json-container.js @@ -5,9 +5,15 @@ import Prism from "prismjs" import "prismjs/components/prism-json" import CodeBox from "./code-box" -const JsonContainer = ({ json, header }) => { +const JsonContainer = ({ json, header, language, allowCopy }) => { const jsonRef = useRef() + const codeClass = language + ? language === "shell" + ? "language-shell" + : "language-json" + : "language-json" + //INVESTIGATE: @theme-ui/prism might be a better solution useEffect(() => { if (jsonRef.current) { @@ -19,9 +25,14 @@ const JsonContainer = ({ json, header }) => { return ( - +
-          {json}
+          {json}
         
diff --git a/www/reference/src/components/content/method.js b/www/reference/src/components/content/method.js index 7c8703dc28..e9efcd3626 100644 --- a/www/reference/src/components/content/method.js +++ b/www/reference/src/components/content/method.js @@ -10,18 +10,19 @@ import ResponsiveContainer from "./responsive-container" import { formatMethodParams } from "../../utils/format-parameters" import useInView from "../../hooks/use-in-view" import NavigationContext from "../../context/navigation-context" +import { formatRoute } from "../../utils/format-route" -const Method = ({ data, section, pathname }) => { +const Method = ({ data, section, pathname, api }) => { const { parameters, requestBody, description, method, summary } = data const jsonResponse = data.responses[0].content?.[0].json const { updateHash, updateMetadata } = useContext(NavigationContext) const methodRef = useRef(null) - const [containerRef, isInView] = useInView({ root: null, rootMargin: "0px 0px -80% 0px", threshold: 0, }) + const formattedParameters = formatMethodParams({ parameters, requestBody }) useEffect(() => { if (isInView) { @@ -42,6 +43,129 @@ const Method = ({ data, section, pathname }) => { } } + const getExampleValues = (type, defaultExample) => { + switch (type) { + case "integer": + return 1000 + case "boolean": + return false + case "object": + return {} + default: + return defaultExample + } + } + + // extract required properties or a non-required property from a json object + // based on the extraction method "getPropertyFromObject" + const getPropertiesFromObject = ( + requiredProperties, + properties, + obj, + res, + getPropertyFromObject + ) => { + for (const element of requiredProperties) { + try { + res[element.property] = getPropertyFromObject(obj, element.property) + } catch (err) {} + } + + // if (Object.keys(res) === requiredProperties.map((p) => p.property)) { + // return res + // } + + for (const element of properties) { + try { + res[element.property] = getPropertyFromObject(obj, element.property) + break + } catch (err) {} + } + + return res + } + + const getCurlJson = (properties, prefix, bodyParameters) => { + if (!properties[0]) { + return + } + const jsonObject = JSON.parse(jsonResponse) + const pathParts = pathname.split("/") + const requiredProperties = bodyParameters.filter((p) => p.required) + + let res = {} + + // if the endpoint is for a relation i.e. /orders/:id/shipment drill down into the properties of the json object + if (pathParts.length > 3) { + const propertyIndex = pathParts[2].match(/{[A-Za-z_]+}/) ? 3 : 2 + + try { + const obj = + jsonObject[pathParts[propertyIndex].replace("-", "_")] || + jsonObject[Object.keys(jsonObject)[0]][ + pathParts[propertyIndex].replace("-", "_") + ] + + res = getPropertiesFromObject( + requiredProperties, + properties, + obj, + res, + (obj, property) => + Array.isArray(obj) + ? obj.find((o) => o[property])[property] + : obj[property] + ) + } catch (err) {} + } + + // if nothing was found drilling down look at the top level properties + if (JSON.stringify(res) === "{}") { + res = getPropertiesFromObject( + requiredProperties, + properties, + jsonObject, + res, + (jsonObject, property) => + jsonObject[property] || + jsonObject[Object.keys(jsonObject)[0]][property] + ) + } + + // Last resort, set the first property to an example + if (JSON.stringify(res) === "{}") { + res[properties[0].property] = getExampleValues(properties[0].type, `${prefix}_${properties[0].property}`) + } + + // Add values to 'undefined' properties before returning due to JSON.stringify removing 'undefined' but not 'null' + return requiredProperties.reduce((prev, curr) => { + if(prev[curr.property] === undefined){ + prev[curr.property] = getExampleValues(curr.type, `${prefix}_${curr.property}`) + } + return prev + }, res) + } + + const getCurlCommand = (requestBody) => { + const body = JSON.stringify( + getCurlJson( + requestBody.properties, + `example_${section}`, + formattedParameters.body + ) + ) + return `curl -X ${data.method.toUpperCase()} https://medusa-url.com/${api}${formatRoute( + pathname + )} \\ + --header "Authorization: Bearer " ${ + data.method.toUpperCase() === "POST" && requestBody.properties?.length > 0 + ? `\\ + --header "content-type: application/json" \\ + --data '${body}'` + : "" + }` + } + return ( { {description} - - + + - + + + + + + diff --git a/www/reference/src/components/content/parameters.js b/www/reference/src/components/content/parameters.js index 61c7120526..207e4dd855 100644 --- a/www/reference/src/components/content/parameters.js +++ b/www/reference/src/components/content/parameters.js @@ -5,62 +5,77 @@ import NestedCollapsible from "./collapsible" import Description from "./description" const Parameters = ({ params, type }) => { + const getDescriptions = (title, items) => { + return ( + <> + {items?.length > 0 && ( + <> + + {title === "attr" ? "Attributes" : title} + + {items.map((prop, i) => { + const nested = prop.nestedModel || prop.items?.properties || null + return ( + + + {prop.property || prop.name} + + {prop.type || prop.schema?.type || nested?.title} + + {prop.required ? ( + + required + + ) : null} + + + + {prop.description} + + + {nested?.properties && ( + + )} + + ) + })} + + )} + + ) + } + return ( - {type === "attr" ? "Attributes" : "Parameters"} - {params.properties.length > 0 ? ( - params.properties.map((prop, i) => { - const nested = prop.nestedModel || prop.items?.properties || null - return ( - - - {prop.property || prop.name} - - {prop.type || prop.schema?.type || nested?.title} - - {prop.required ? ( - - required - - ) : null} - - - - {prop.description} - - - {nested?.properties && ( - - )} - - ) - }) - ) : ( - - No parameters - - )} + {getDescriptions(type, params.properties)} + {getDescriptions("Request body", params.body)} ) } diff --git a/www/reference/src/components/content/section.js b/www/reference/src/components/content/section.js index 770300f97a..3b9386c3cf 100644 --- a/www/reference/src/components/content/section.js +++ b/www/reference/src/components/content/section.js @@ -12,7 +12,7 @@ import NavigationContext from "../../context/navigation-context" import ChevronDown from "../icons/chevron-down" import useInView from "../../hooks/use-in-view" -const Section = ({ data }) => { +const Section = ({ data, api }) => { const { section } = data const [isExpanded, setIsExpanded] = useState(false) const { openSections, updateSection, updateMetadata } = useContext( @@ -20,11 +20,11 @@ const Section = ({ data }) => { ) const endpoints = section.paths - .map(p => { + .map((p) => { let path = p.name let ep = [] - p.methods.forEach(m => { + p.methods.forEach((m) => { ep.push({ method: m.method, endpoint: path }) }) @@ -184,6 +184,7 @@ const Section = ({ data }) => { {p.methods.map((m, i) => { return ( { + return ( + + ) +} + +export default Clipboard diff --git a/www/reference/src/utils/format-parameters.js b/www/reference/src/utils/format-parameters.js index 1dcbb21f0b..23a7c349da 100644 --- a/www/reference/src/utils/format-parameters.js +++ b/www/reference/src/utils/format-parameters.js @@ -1,9 +1,10 @@ -export const formatMethodParams = method => { +export const formatMethodParams = (method) => { const { parameters, requestBody } = method const params = [] + const body = [] if (parameters && parameters.length > 0) { - parameters.map(p => { + parameters.map((p) => { return params.push({ property: p.name, description: p.description, @@ -14,16 +15,16 @@ export const formatMethodParams = method => { } if (requestBody) { const { required, properties } = requestBody - properties.map(p => { - return params.push({ + properties.map((p) => { + return body.push({ property: p.property, description: p.description, - required: required ? required.some(req => req === p.property) : false, + required: required ? required.some((req) => req === p.property) : false, type: p.type, nestedModel: p.nestedModel, }) }) } - return { properties: params } + return { properties: params, body } }