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