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
This commit is contained in:
@@ -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",
|
||||
|
||||
16
www/reference/src/assets/clipboard.svg
Normal file
16
www/reference/src/assets/clipboard.svg
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<svg height="10" width="10" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
|
||||
<title>Clipboard</title>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M435.2,51.2H384v102.4H128V51.2H76.8c-15.36,0-25.6,10.24-25.6,25.6v409.6c0,15.36,10.24,25.6,25.6,25.6h358.4
|
||||
c15.36,0,25.6-10.24,25.6-25.6V76.8C460.8,61.44,450.56,51.2,435.2,51.2z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M307.2,0H204.8c-15.36,0-25.6,10.24-25.6,25.6v76.8h153.6V25.6C332.8,10.24,322.56,0,307.2,0z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 695 B |
76
www/reference/src/components/CopyToClipboard/index.js
Normal file
76
www/reference/src/components/CopyToClipboard/index.js
Normal file
@@ -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(
|
||||
<StyledTooltip
|
||||
place="top"
|
||||
backgroundColor='#FFF'
|
||||
textColor='black'
|
||||
effect="solid"
|
||||
id={id}
|
||||
sx={{ boxShadow: `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`
|
||||
}}
|
||||
>
|
||||
<Text>{text}</Text>
|
||||
</StyledTooltip>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<Box
|
||||
mr={1}
|
||||
onMouseLeave={() => {setCopied(false)}}
|
||||
onClick={() => {
|
||||
setCopied(true)
|
||||
onCopyClicked()
|
||||
}}
|
||||
data-for={id}
|
||||
data-tip={forceTooltipRemount}
|
||||
key={forceTooltipRemount}
|
||||
sx={{cursor: 'pointer'}}
|
||||
>
|
||||
{
|
||||
text && (
|
||||
<Text variant="small" mr={2} sx={{ fontWeight: "300" }}>
|
||||
{text.toUpperCase()}
|
||||
</Text>)
|
||||
}
|
||||
<Clipboard/>
|
||||
{copied ?
|
||||
<StyledTooltip id={id} text={"Copied!"} />
|
||||
:
|
||||
<StyledTooltip id={id} text={tooltipText} />
|
||||
}
|
||||
</Box>)
|
||||
}
|
||||
|
||||
export default CopyToClipboard
|
||||
@@ -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 (
|
||||
<Box
|
||||
sx={{
|
||||
@@ -23,9 +24,25 @@ const CodeBox = ({ header, children }) => {
|
||||
borderRadius: "8px 8px 0 0",
|
||||
}}
|
||||
>
|
||||
<Text variant="small" sx={{ fontWeight: "400" }}>
|
||||
{header}
|
||||
</Text>
|
||||
<Flex
|
||||
sx={{
|
||||
height: "100%",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "baseline",
|
||||
}}
|
||||
>
|
||||
<Text variant="small" sx={{ fontWeight: "400" }}>
|
||||
{header.toUpperCase()}
|
||||
</Text>
|
||||
{allowCopy ? (
|
||||
<CopyToClipboard
|
||||
copyText={content}
|
||||
tooltipText={"Copy to clipboard"}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</Flex>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
|
||||
@@ -22,7 +22,7 @@ const Content = ({ data, api }) => {
|
||||
>
|
||||
<main className="DocSearch-content">
|
||||
{data.sections.map((s, i) => {
|
||||
return <Section key={i} data={s} />
|
||||
return <Section key={i} data={s} api={api} />
|
||||
})}
|
||||
</main>
|
||||
</Box>
|
||||
|
||||
@@ -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 (
|
||||
<Box ref={jsonRef} sx={{ position: "sticky", top: "20px" }}>
|
||||
<CodeBox header={header}>
|
||||
<CodeBox
|
||||
allowCopy={allowCopy}
|
||||
content={json}
|
||||
shell={language === "shell"}
|
||||
header={header}
|
||||
>
|
||||
<pre>
|
||||
<code className={"language-json"}>{json}</code>
|
||||
<code className={codeClass}>{json}</code>
|
||||
</pre>
|
||||
</CodeBox>
|
||||
</Box>
|
||||
|
||||
@@ -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 <ACCESS TOKEN>" ${
|
||||
data.method.toUpperCase() === "POST" && requestBody.properties?.length > 0
|
||||
? `\\
|
||||
--header "content-type: application/json" \\
|
||||
--data '${body}'`
|
||||
: ""
|
||||
}`
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex
|
||||
py={"5vw"}
|
||||
@@ -89,18 +213,27 @@ const Method = ({ data, section, pathname }) => {
|
||||
<Markdown>{description}</Markdown>
|
||||
</Text>
|
||||
</Description>
|
||||
<Box mt={4}>
|
||||
<Parameters
|
||||
params={formatMethodParams({ parameters, requestBody })}
|
||||
/>
|
||||
<Box mt={2}>
|
||||
<Parameters params={formattedParameters} type={"Parameters"} />
|
||||
</Box>
|
||||
</Flex>
|
||||
<Box className="code">
|
||||
<JsonContainer
|
||||
json={jsonResponse}
|
||||
header={"RESPONSE"}
|
||||
method={convertToKebabCase(summary)}
|
||||
/>
|
||||
<Box>
|
||||
<JsonContainer
|
||||
json={getCurlCommand(requestBody)}
|
||||
header={"cURL Example"}
|
||||
language={"shell"}
|
||||
allowCopy={true}
|
||||
method={convertToKebabCase(summary)}
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<JsonContainer
|
||||
json={jsonResponse}
|
||||
header={"RESPONSE"}
|
||||
method={convertToKebabCase(summary)}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</ResponsiveContainer>
|
||||
</Flex>
|
||||
|
||||
@@ -5,62 +5,77 @@ import NestedCollapsible from "./collapsible"
|
||||
import Description from "./description"
|
||||
|
||||
const Parameters = ({ params, type }) => {
|
||||
const getDescriptions = (title, items) => {
|
||||
return (
|
||||
<>
|
||||
{items?.length > 0 && (
|
||||
<>
|
||||
<Text
|
||||
sx={{ borderLeft: "2px solid gray", alignItems: "center" }}
|
||||
my={3}
|
||||
pl={2}
|
||||
py={1}
|
||||
>
|
||||
{title === "attr" ? "Attributes" : title}
|
||||
</Text>
|
||||
{items.map((prop, i) => {
|
||||
const nested = prop.nestedModel || prop.items?.properties || null
|
||||
return (
|
||||
<Box
|
||||
py={2}
|
||||
pl={2}
|
||||
sx={{
|
||||
borderTop: "hairline",
|
||||
fontFamily: "monospace",
|
||||
fontSize: "0",
|
||||
}}
|
||||
key={i}
|
||||
>
|
||||
<Flex sx={{ alignItems: "baseline", fontSize: "0" }}>
|
||||
<Text mr={2}>{prop.property || prop.name}</Text>
|
||||
<Text color={"gray"}>
|
||||
{prop.type || prop.schema?.type || nested?.title}
|
||||
</Text>
|
||||
{prop.required ? (
|
||||
<Text ml={1} variant="labels.required">
|
||||
required
|
||||
</Text>
|
||||
) : null}
|
||||
</Flex>
|
||||
<Description>
|
||||
<Text
|
||||
sx={{
|
||||
fontSize: "0",
|
||||
lineHeight: "26px",
|
||||
fontFamily: "body",
|
||||
}}
|
||||
>
|
||||
<Markdown>{prop.description}</Markdown>
|
||||
</Text>
|
||||
</Description>
|
||||
{nested?.properties && (
|
||||
<NestedCollapsible
|
||||
properties={nested.properties}
|
||||
title={nested.title}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex
|
||||
sx={{
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<Text pb="2">{type === "attr" ? "Attributes" : "Parameters"}</Text>
|
||||
{params.properties.length > 0 ? (
|
||||
params.properties.map((prop, i) => {
|
||||
const nested = prop.nestedModel || prop.items?.properties || null
|
||||
return (
|
||||
<Box
|
||||
py={2}
|
||||
sx={{
|
||||
borderTop: "hairline",
|
||||
fontFamily: "monospace",
|
||||
fontSize: "0",
|
||||
}}
|
||||
key={i}
|
||||
>
|
||||
<Flex sx={{ alignItems: "baseline", fontSize: "0" }}>
|
||||
<Text mr={2}>{prop.property || prop.name}</Text>
|
||||
<Text color={"gray"}>
|
||||
{prop.type || prop.schema?.type || nested?.title}
|
||||
</Text>
|
||||
{prop.required ? (
|
||||
<Text ml={1} variant="labels.required">
|
||||
required
|
||||
</Text>
|
||||
) : null}
|
||||
</Flex>
|
||||
<Description>
|
||||
<Text
|
||||
sx={{
|
||||
fontSize: "0",
|
||||
lineHeight: "26px",
|
||||
fontFamily: "body",
|
||||
}}
|
||||
>
|
||||
<Markdown>{prop.description}</Markdown>
|
||||
</Text>
|
||||
</Description>
|
||||
{nested?.properties && (
|
||||
<NestedCollapsible
|
||||
properties={nested.properties}
|
||||
title={nested.title}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<Text sx={{ fontSize: "0", py: "3", fontFamily: "monospace" }}>
|
||||
No parameters
|
||||
</Text>
|
||||
)}
|
||||
{getDescriptions(type, params.properties)}
|
||||
{getDescriptions("Request body", params.body)}
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<Method
|
||||
api={api}
|
||||
key={i}
|
||||
data={m}
|
||||
section={convertToKebabCase(section.section_name)}
|
||||
|
||||
19
www/reference/src/components/icons/clipboard.js
Normal file
19
www/reference/src/components/icons/clipboard.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Image } from "@theme-ui/components"
|
||||
import React from "react"
|
||||
|
||||
import Logo from "../../assets/clipboard.svg"
|
||||
|
||||
const Clipboard = () => {
|
||||
return (
|
||||
<Image
|
||||
src={Logo}
|
||||
sx={{
|
||||
height: "100%",
|
||||
fill: "#000",
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default Clipboard
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user