docs: add copy subscriber button (#12405)

* docs: add copy subscriber button

* re-generate

* fixes + update copy button
This commit is contained in:
Shahed Nasser
2025-05-08 12:48:44 +03:00
committed by GitHub
parent f929185021
commit f81eb51b67
38 changed files with 20525 additions and 18707 deletions
+2 -2
View File
@@ -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"
+1
View File
@@ -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,
}
}
+1
View File
@@ -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"
+20 -2
View File
@@ -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 => {
+51 -1
View File
@@ -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
+1 -1
View File
@@ -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": "*",
+13
View File
@@ -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?: {