docs: add copy subscriber button (#12405)
* docs: add copy subscriber button * re-generate * fixes + update copy button
This commit is contained in:
@@ -57,8 +57,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/is-prop-valid": "^1.3.1",
|
||||
"@medusajs/icons": "2.6.0",
|
||||
"@medusajs/ui": "4.0.6",
|
||||
"@medusajs/icons": "2.7.1",
|
||||
"@medusajs/ui": "4.0.9",
|
||||
"@next/third-parties": "15.3.1",
|
||||
"@octokit/request": "^8.1.1",
|
||||
"@react-hook/resize-observer": "^1.2.6",
|
||||
|
||||
@@ -5,6 +5,10 @@ import clsx from "clsx"
|
||||
import { Tooltip } from "@/components"
|
||||
import { useCopy } from "../../hooks"
|
||||
|
||||
export type CopyButtonChildFn = (props: {
|
||||
isCopied: boolean
|
||||
}) => React.ReactNode
|
||||
|
||||
export type CopyButtonProps = {
|
||||
text: string
|
||||
buttonClassName?: string
|
||||
@@ -17,7 +21,8 @@ export type CopyButtonProps = {
|
||||
| React.TouchEvent<HTMLSpanElement>
|
||||
) => void
|
||||
handleTouch?: boolean
|
||||
} & Omit<React.HTMLAttributes<HTMLDivElement>, "onCopy">
|
||||
children?: React.ReactNode | CopyButtonChildFn
|
||||
} & Omit<React.HTMLAttributes<HTMLDivElement>, "onCopy" | "children">
|
||||
|
||||
export const CopyButton = ({
|
||||
text,
|
||||
@@ -64,7 +69,7 @@ export const CopyButton = ({
|
||||
}
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
{typeof children === "function" ? children({ isCopied }) : children}
|
||||
</span>
|
||||
</Tooltip>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
"use client"
|
||||
|
||||
import React from "react"
|
||||
import { CopyButton, useGenerateSnippet, UseGenerateSnippet } from "../.."
|
||||
import { SquareTwoStack, CheckCircle } from "@medusajs/icons"
|
||||
|
||||
export type CopyGeneratedSnippetButtonProps = UseGenerateSnippet & {
|
||||
tooltipText?: string
|
||||
}
|
||||
|
||||
export const CopyGeneratedSnippetButton = ({
|
||||
tooltipText,
|
||||
...props
|
||||
}: CopyGeneratedSnippetButtonProps) => {
|
||||
const { snippet } = useGenerateSnippet(props)
|
||||
|
||||
return (
|
||||
<CopyButton
|
||||
text={snippet}
|
||||
tooltipText={tooltipText}
|
||||
className="inline-block w-fit"
|
||||
>
|
||||
{({ isCopied }) => {
|
||||
if (isCopied) {
|
||||
return <CheckCircle />
|
||||
}
|
||||
return <SquareTwoStack />
|
||||
}}
|
||||
</CopyButton>
|
||||
)
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import clsx from "clsx"
|
||||
import React from "react"
|
||||
import { LlmDropdown } from "../../LlmDropdown"
|
||||
|
||||
type H1Props = React.HTMLAttributes<HTMLHeadingElement> & {
|
||||
export type H1Props = React.HTMLAttributes<HTMLHeadingElement> & {
|
||||
id?: string
|
||||
hideLlmDropdown?: boolean
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import React from "react"
|
||||
import { CopyButton, Link } from "@/components"
|
||||
import { useHeadingUrl, useLayout } from "../../.."
|
||||
|
||||
type H2Props = React.HTMLAttributes<HTMLHeadingElement> & {
|
||||
export type H2Props = React.HTMLAttributes<HTMLHeadingElement> & {
|
||||
id?: string
|
||||
passRef?: React.RefObject<HTMLHeadingElement | null>
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import React from "react"
|
||||
import { CopyButton, Link } from "@/components"
|
||||
import { useHeadingUrl, useLayout } from "../../.."
|
||||
|
||||
type H3Props = React.HTMLAttributes<HTMLHeadingElement> & {
|
||||
export type H3Props = React.HTMLAttributes<HTMLHeadingElement> & {
|
||||
id?: string
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ export const H4 = ({
|
||||
}: React.HTMLAttributes<HTMLHeadingElement>) => {
|
||||
return (
|
||||
<h4
|
||||
className={clsx("mb-docs_0.5 text-medusa-fg-base text-h4", className)}
|
||||
className={clsx("mb-docs_1 text-medusa-fg-base text-h4", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
import React, { useRef, useState } from "react"
|
||||
import { useAiAssistant, useSiteConfig } from "../../providers"
|
||||
import { usePathname } from "next/navigation"
|
||||
import { Button } from "../Button"
|
||||
import { AiAssistent, Book } from "@medusajs/icons"
|
||||
import { Menu } from "../Menu"
|
||||
import { DropdownMenu, Menu } from "../Menu"
|
||||
import { MarkdownIcon } from "../Icons/Markdown"
|
||||
import { useAiAssistantChat } from "../../providers/AiAssistant/Chat"
|
||||
import clsx from "clsx"
|
||||
@@ -30,43 +29,41 @@ export const LlmDropdown = () => {
|
||||
const pageUrl = `${baseUrl}${basePath}${pathname}`
|
||||
|
||||
return (
|
||||
<div className="relative hidden md:block">
|
||||
<Button
|
||||
variant="transparent"
|
||||
onClick={() => setOpen(!open)}
|
||||
className="!p-[6px] text-medusa-fg-subtle"
|
||||
buttonRef={ref}
|
||||
>
|
||||
<Book />
|
||||
</Button>
|
||||
<Menu
|
||||
items={[
|
||||
{
|
||||
type: "link",
|
||||
title: "View as Markdown",
|
||||
link: `${pageUrl}/index.html.md`,
|
||||
icon: <MarkdownIcon width={19} height={19} />,
|
||||
openInNewTab: true,
|
||||
},
|
||||
{
|
||||
type: "action",
|
||||
title: "Ask AI Assistant",
|
||||
action: () => {
|
||||
if (loading) {
|
||||
return
|
||||
}
|
||||
setQuestion(`Explain the page ${pageUrl}`)
|
||||
setChatOpened(true)
|
||||
setOpen(false)
|
||||
<DropdownMenu
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
dropdownButtonContent={<Book />}
|
||||
menuComponent={
|
||||
<Menu
|
||||
items={[
|
||||
{
|
||||
type: "link",
|
||||
title: "View as Markdown",
|
||||
link: `${pageUrl}/index.html.md`,
|
||||
icon: <MarkdownIcon width={19} height={19} />,
|
||||
openInNewTab: true,
|
||||
},
|
||||
icon: <AiAssistent />,
|
||||
},
|
||||
]}
|
||||
className={clsx(
|
||||
"absolute right-0 top-[calc(100%+8px)] w-max",
|
||||
!open && "hidden"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
type: "action",
|
||||
title: "Ask AI Assistant",
|
||||
action: () => {
|
||||
if (loading) {
|
||||
return
|
||||
}
|
||||
setQuestion(`Explain the page ${pageUrl}`)
|
||||
setChatOpened(true)
|
||||
setOpen(false)
|
||||
},
|
||||
icon: <AiAssistent />,
|
||||
},
|
||||
]}
|
||||
className={clsx(
|
||||
"absolute right-0 top-[calc(100%+8px)] w-max",
|
||||
!open && "hidden"
|
||||
)}
|
||||
/>
|
||||
}
|
||||
className="hidden md:block"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
"use client"
|
||||
|
||||
import React from "react"
|
||||
import { useRef, useState } from "react"
|
||||
import { Button, Menu, useClickOutside } from "../../.."
|
||||
import { MenuItem } from "types"
|
||||
import clsx from "clsx"
|
||||
|
||||
type DropdownMenuProps = {
|
||||
dropdownButtonContent: React.ReactNode
|
||||
dropdownButtonClassName?: string
|
||||
menuComponent?: React.ReactNode
|
||||
menuItems?: MenuItem[]
|
||||
menuClassName?: string
|
||||
className?: string
|
||||
open?: boolean
|
||||
setOpen?: (open: boolean) => void
|
||||
}
|
||||
|
||||
export const DropdownMenu = ({
|
||||
dropdownButtonContent,
|
||||
dropdownButtonClassName,
|
||||
menuComponent,
|
||||
menuItems,
|
||||
menuClassName,
|
||||
className,
|
||||
open: externalOpen = false,
|
||||
setOpen: externalSetOpen,
|
||||
}: DropdownMenuProps) => {
|
||||
const [open, setOpen] = useState(externalOpen)
|
||||
const ref = useRef<HTMLButtonElement | null>(null)
|
||||
function changeOpenState(newOpenState: boolean) {
|
||||
if (externalSetOpen) {
|
||||
externalSetOpen(newOpenState)
|
||||
} else {
|
||||
setOpen(newOpenState)
|
||||
}
|
||||
}
|
||||
useClickOutside({
|
||||
elmRef: ref,
|
||||
onClickOutside: () => {
|
||||
changeOpenState(false)
|
||||
},
|
||||
})
|
||||
|
||||
if (!menuComponent && !menuItems) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={clsx("relative", className)}>
|
||||
<Button
|
||||
variant="transparent"
|
||||
onClick={() => changeOpenState(!open)}
|
||||
className={clsx(
|
||||
"!p-[6px] text-medusa-fg-subtle",
|
||||
dropdownButtonClassName
|
||||
)}
|
||||
buttonRef={ref}
|
||||
>
|
||||
{dropdownButtonContent}
|
||||
</Button>
|
||||
{menuComponent}
|
||||
{!menuComponent && menuItems && (
|
||||
<Menu
|
||||
items={menuItems}
|
||||
className={clsx(
|
||||
"absolute right-0 top-[calc(100%+8px)] w-max",
|
||||
!open && "hidden",
|
||||
menuClassName
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -39,3 +39,5 @@ export const Menu = ({ items, className, itemsOnClick }: MenuProps) => {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export * from "./Dropdown"
|
||||
|
||||
@@ -15,6 +15,7 @@ export * from "./CodeMdx"
|
||||
export * from "./CodeTabs"
|
||||
export * from "./CodeTabs/Item"
|
||||
export * from "./CopyButton"
|
||||
export * from "./CopyGeneratedSnippetButton"
|
||||
export * from "./Details"
|
||||
export * from "./Details/Summary"
|
||||
export * from "./DetailsList"
|
||||
|
||||
@@ -5,6 +5,7 @@ export * from "./use-click-outside"
|
||||
export * from "./use-collapsible"
|
||||
export * from "./use-collapsible-code-lines"
|
||||
export * from "./use-copy"
|
||||
export * from "./use-generate-snippet"
|
||||
export * from "./use-heading-url"
|
||||
export * from "./use-current-learning-path"
|
||||
export * from "./use-is-external-link"
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import {
|
||||
subscriberSnippetGenerator,
|
||||
SubscriberSnippetGeneratorOptions,
|
||||
} from "./snippet-generators/subscriber"
|
||||
|
||||
export type UseGenerateSnippet = {
|
||||
type: "subscriber"
|
||||
options: SubscriberSnippetGeneratorOptions
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const generators: Record<string, (options: any) => string> = {
|
||||
subscriber: subscriberSnippetGenerator,
|
||||
}
|
||||
|
||||
export const useGenerateSnippet = ({ type, options }: UseGenerateSnippet) => {
|
||||
const snippet = generators[type](options)
|
||||
|
||||
return {
|
||||
snippet,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { parseEventPayload } from "../../../utils"
|
||||
|
||||
export type SubscriberSnippetGeneratorOptions = {
|
||||
event: string
|
||||
payload: Record<string, unknown> | string
|
||||
}
|
||||
|
||||
export const subscriberSnippetGenerator = ({
|
||||
event,
|
||||
payload: initialPayload,
|
||||
}: SubscriberSnippetGeneratorOptions) => {
|
||||
const payload =
|
||||
typeof initialPayload === "string"
|
||||
? parseEventPayload(initialPayload).payload_for_snippet
|
||||
: initialPayload
|
||||
// format subscriber name
|
||||
const subscriberName =
|
||||
event
|
||||
.split(".")
|
||||
.map((word) =>
|
||||
word.replace(/-./g, (match) => match.charAt(1).toUpperCase())
|
||||
)
|
||||
.map((word, index) => {
|
||||
if (index === 0) {
|
||||
return word.charAt(0).toLowerCase() + word.slice(1)
|
||||
}
|
||||
|
||||
return word.charAt(0).toUpperCase() + word.slice(1)
|
||||
})
|
||||
.join("") + "Handler"
|
||||
// format payload
|
||||
const payloadType: Record<string, unknown> = {}
|
||||
Object.keys(payload).forEach((key) => {
|
||||
const value = payload[key]
|
||||
if (Array.isArray(value)) {
|
||||
payloadType[key] = `${typeof value[0]}[]`
|
||||
} else {
|
||||
payloadType[key] = typeof value
|
||||
}
|
||||
})
|
||||
|
||||
const payloadString = JSON.stringify(payloadType, null, 2).replaceAll(
|
||||
/"/g,
|
||||
""
|
||||
)
|
||||
|
||||
// return the snippet
|
||||
return subscriberSnippet
|
||||
.replace("{{subscriberName}}", subscriberName)
|
||||
.replace("{{event}}", event)
|
||||
.replace("{{payload}}", payloadString)
|
||||
}
|
||||
|
||||
const subscriberSnippet = `import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework"
|
||||
|
||||
export default async function {{subscriberName}}({
|
||||
event: { data },
|
||||
container,
|
||||
}: SubscriberArgs<{{payload}}>) {
|
||||
// TODO handle event
|
||||
}
|
||||
|
||||
export const config: SubscriberConfig = {
|
||||
event: "{{event}}",
|
||||
}`
|
||||
@@ -0,0 +1,54 @@
|
||||
import { OpenAPI } from "types"
|
||||
|
||||
export function parseEventPayload(payloadStr: string) {
|
||||
const payloadParams = payloadStr.matchAll(/([\w_]+),? \/\/ (\(\w*\) )*(.*)/g)
|
||||
const payloadForSnippet: Record<string, unknown> = {}
|
||||
const payload = Array.from(payloadParams).map((match) => {
|
||||
const name = match[1]
|
||||
const type = (match[2]?.replace(/\(|\)/g, "") || "string").trim()
|
||||
const description = match[3]
|
||||
|
||||
if (type === "string") {
|
||||
payloadForSnippet[name] = "test"
|
||||
} else if (type === "number") {
|
||||
payloadForSnippet[name] = 1
|
||||
} else if (type === "boolean") {
|
||||
payloadForSnippet[name] = true
|
||||
} else if (type === "object") {
|
||||
payloadForSnippet[name] = {}
|
||||
} else if (type === "array") {
|
||||
payloadForSnippet[name] = [{}]
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
type,
|
||||
description,
|
||||
}
|
||||
})
|
||||
return {
|
||||
parsed_payload: {
|
||||
type: "object",
|
||||
required: ["payload"],
|
||||
properties: {
|
||||
payload: {
|
||||
type: "object",
|
||||
description: "The payload emitted with the event",
|
||||
required: [...payload.map((param) => param.name)],
|
||||
properties: payload.reduce(
|
||||
(acc, curr) => {
|
||||
acc[curr.name] = {
|
||||
type: curr.type as OpenAPI.OpenAPIV3.NonArraySchemaObjectType,
|
||||
description: curr.description,
|
||||
properties: {},
|
||||
}
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, OpenAPI.SchemaObject>
|
||||
),
|
||||
},
|
||||
},
|
||||
} as OpenAPI.SchemaObject,
|
||||
payload_for_snippet: payloadForSnippet,
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ export * from "./capitalize"
|
||||
export * from "./check-sidebar-item-visibility"
|
||||
export * from "./decode-str"
|
||||
export * from "./dom-utils"
|
||||
export * from "./event-parser"
|
||||
export * from "./get-link-with-base-path"
|
||||
export * from "./get-local-search"
|
||||
export * from "./get-navbar-items"
|
||||
|
||||
@@ -5,6 +5,8 @@ import {
|
||||
Expression,
|
||||
ExpressionJsVar,
|
||||
ExpressionJsVarLiteral,
|
||||
JSXElementExpression,
|
||||
JSXTextExpression,
|
||||
LiteralExpression,
|
||||
ObjectExpression,
|
||||
} from "types"
|
||||
@@ -64,7 +66,23 @@ function expressionToJs(
|
||||
data: (expression as LiteralExpression).value,
|
||||
} as ExpressionJsVarLiteral
|
||||
case "JSXElement":
|
||||
// ignore JSXElements
|
||||
return
|
||||
case "JSXFragment":
|
||||
// Only take text children
|
||||
let text = ""
|
||||
;(expression as JSXElementExpression).children.forEach((child) => {
|
||||
if (child.type !== "JSXText") {
|
||||
return
|
||||
}
|
||||
|
||||
text += (child as JSXTextExpression).value
|
||||
})
|
||||
return {
|
||||
original: {
|
||||
type: "Literal",
|
||||
value: text,
|
||||
raw: `"${text}"`,
|
||||
},
|
||||
data: text,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
parseComponentExample,
|
||||
parseComponentReference,
|
||||
parseDetails,
|
||||
parseEventHeader,
|
||||
parseHookValues,
|
||||
parseIconSearch,
|
||||
parseNote,
|
||||
@@ -48,6 +49,7 @@ const parsers: Record<string, ComponentParser> = {
|
||||
HookValues: parseHookValues,
|
||||
Colors: parseColors,
|
||||
SplitList: parseSplitList,
|
||||
EventHeader: parseEventHeader,
|
||||
}
|
||||
|
||||
const isComponentAllowed = (nodeName: string): boolean => {
|
||||
|
||||
@@ -951,6 +951,54 @@ export const parseSplitList: ComponentParser = (
|
||||
return [SKIP, index]
|
||||
}
|
||||
|
||||
export const parseEventHeader: ComponentParser = (
|
||||
node: UnistNodeWithData,
|
||||
index: number,
|
||||
parent: UnistTree
|
||||
): VisitorResult => {
|
||||
const headerContent = node.attributes?.find(
|
||||
(attr) => attr.name === "headerProps"
|
||||
)
|
||||
const headerLvl = node.attributes?.find((attr) => attr.name === "headerLvl")
|
||||
|
||||
if (
|
||||
!headerContent ||
|
||||
typeof headerContent.value === "string" ||
|
||||
!headerContent.value.data?.estree ||
|
||||
!headerLvl ||
|
||||
typeof headerLvl.value !== "string" ||
|
||||
!headerLvl.value
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const headerPropsJsVar = estreeToJs(headerContent.value.data.estree)
|
||||
|
||||
if (
|
||||
!isExpressionJsVarObj(headerPropsJsVar) ||
|
||||
!("children" in headerPropsJsVar) ||
|
||||
!isExpressionJsVarLiteral(headerPropsJsVar.children)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const headerLevel = parseInt(headerLvl.value, 10)
|
||||
const headerChildren = headerPropsJsVar.children.data as string
|
||||
|
||||
const headerChildrenNode = {
|
||||
type: "text",
|
||||
value: headerChildren,
|
||||
}
|
||||
const headerNode = {
|
||||
type: "heading",
|
||||
depth: headerLevel,
|
||||
children: [headerChildrenNode],
|
||||
}
|
||||
|
||||
parent?.children.splice(index, 1, headerNode)
|
||||
return [SKIP, index]
|
||||
}
|
||||
|
||||
/**
|
||||
* Helpers
|
||||
*/
|
||||
@@ -964,11 +1012,13 @@ const getTextNode = (node: UnistNode): UnistNode | undefined => {
|
||||
textNode = child
|
||||
} else if (child.type === "paragraph") {
|
||||
textNode = getTextNode(child)
|
||||
} else if (child.type === "link") {
|
||||
textNode = getTextNode(child)
|
||||
} else if (child.value) {
|
||||
textNode = child
|
||||
}
|
||||
|
||||
return false
|
||||
return textNode !== undefined
|
||||
})
|
||||
|
||||
return textNode
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"watch": "tsc --watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@medusajs/icons": "2.6.0",
|
||||
"@medusajs/icons": "2.7.1",
|
||||
"@types/node": "^20.11.20",
|
||||
"rimraf": "^5.0.5",
|
||||
"tsconfig": "*",
|
||||
|
||||
@@ -37,6 +37,17 @@ export type LiteralExpression = {
|
||||
raw: string
|
||||
}
|
||||
|
||||
export type JSXElementExpression = {
|
||||
type: "JSXElement" | "JSXFragment"
|
||||
children: Expression[]
|
||||
}
|
||||
|
||||
export type JSXTextExpression = {
|
||||
type: "JSXText"
|
||||
value: string
|
||||
raw: string
|
||||
}
|
||||
|
||||
export type Expression =
|
||||
| {
|
||||
type: string
|
||||
@@ -44,6 +55,8 @@ export type Expression =
|
||||
| ArrayExpression
|
||||
| ObjectExpression
|
||||
| LiteralExpression
|
||||
| JSXElementExpression
|
||||
| JSXTextExpression
|
||||
|
||||
export interface Estree {
|
||||
body?: {
|
||||
|
||||
Reference in New Issue
Block a user