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
@@ -3,18 +3,23 @@
import { import {
Badge, Badge,
DetailsSummary, DetailsSummary,
DropdownMenu,
Link, Link,
MarkdownContent, MarkdownContent,
parseEventPayload,
Tabs, Tabs,
TabsContent, TabsContent,
TabsContentWrapper, TabsContentWrapper,
TabsList, TabsList,
TabsTrigger, TabsTrigger,
Tooltip, Tooltip,
useCopy,
useGenerateSnippet,
} from "docs-ui" } from "docs-ui"
import { useMemo } from "react" import { useMemo } from "react"
import type { OpenAPI } from "types" import type { OpenAPI } from "types"
import TagOperationParameters from "../../Parameters" import TagOperationParameters from "../../Parameters"
import { Brackets, CheckCircle, SquareTwoStack, Tag } from "@medusajs/icons"
export type TagsOperationDescriptionSectionEventsProps = { export type TagsOperationDescriptionSectionEventsProps = {
events: OpenAPI.OasEvents[] events: OpenAPI.OasEvents[]
@@ -68,62 +73,73 @@ const TagsOperationDescriptionSectionEvent = ({
}: { }: {
event: OpenAPI.OasEvents event: OpenAPI.OasEvents
}) => { }) => {
const parsedPayload: OpenAPI.SchemaObject = useMemo(() => { const {
const payloadParams = event.payload.matchAll( parsed_payload: parsedPayload,
/([\w_]+),? \/\/ (\(\w*\) )*(.*)/g payload_for_snippet: payloadForSnippet,
) } = useMemo(() => {
const payload = Array.from(payloadParams).map((match) => { return parseEventPayload(event.payload)
return {
name: match[1],
type: match[2]?.replace(/\(|\)/g, "") || "string",
description: match[3],
}
})
return {
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>
),
},
},
}
}, [event.payload]) }, [event.payload])
const { snippet } = useGenerateSnippet({
type: "subscriber",
options: {
event: event.name,
payload: payloadForSnippet,
},
})
const { handleCopy: handleEventNameCopy, isCopied: eventNameCopied } =
useCopy(event.name)
const { handleCopy: handleSnippetCopy, isCopied: snippetCopied } =
useCopy(snippet)
return ( return (
<TabsContent value={event.name}> <TabsContent value={event.name}>
<div className="my-1 flex flex-wrap gap-1"> <div className="my-1 flex flex-wrap justify-between items-center mb-1">
<MarkdownContent <div className="flex flex-wrap items-center gap-0.5">
allowedElements={["code", "p", "a"]} <MarkdownContent
className={"[&_p:last-child]:!mb-1"} allowedElements={["code", "p", "a"]}
> className={"[&_p:last-child]:!mb-0"}
{`\`${event.name}\`: ${event.description}`} >
</MarkdownContent> {event.description}
{event.deprecated && </MarkdownContent>
(event.deprecated_message ? ( {event.deprecated &&
<Tooltip text={event.deprecated_message}> (event.deprecated_message ? (
<Tooltip text={event.deprecated_message}>
<Badge variant="orange">Deprecated</Badge>
</Tooltip>
) : (
<Badge variant="orange">Deprecated</Badge> <Badge variant="orange">Deprecated</Badge>
))}
{event.version && (
<Tooltip text={`This event is emitted since v${event.version}`}>
<Badge variant="blue">v{event.version}</Badge>
</Tooltip> </Tooltip>
) : ( )}
<Badge variant="orange">Deprecated</Badge> </div>
))} <DropdownMenu
{event.version && ( dropdownButtonContent={
<Tooltip text={`This event is emitted since v${event.version}`}> <>
<Badge variant="blue">v{event.version}</Badge> {eventNameCopied || snippetCopied ? (
</Tooltip> <CheckCircle />
)} ) : (
<SquareTwoStack />
)}
</>
}
menuItems={[
{
type: "action",
title: "Copy event name",
action: () => handleEventNameCopy(),
icon: <Tag />,
},
{
type: "action",
title: "Copy subscriber for event",
action: () => handleSnippetCopy(),
icon: <Brackets />,
},
]}
menuClassName="z-10"
/>
</div> </div>
<TagOperationParameters <TagOperationParameters
schemaObject={parsedPayload} schemaObject={parsedPayload}
+2 -2
View File
@@ -16,8 +16,8 @@
"dependencies": { "dependencies": {
"@mdx-js/loader": "^3.1.0", "@mdx-js/loader": "^3.1.0",
"@mdx-js/react": "^3.1.0", "@mdx-js/react": "^3.1.0",
"@medusajs/icons": "2.6.0", "@medusajs/icons": "2.7.1",
"@medusajs/ui": "4.0.6", "@medusajs/ui": "4.0.9",
"@next/mdx": "15.3.1", "@next/mdx": "15.3.1",
"@react-hook/resize-observer": "^2.0.2", "@react-hook/resize-observer": "^2.0.2",
"@readme/openapi-parser": "^2.5.0", "@readme/openapi-parser": "^2.5.0",
+1 -1
View File
@@ -16,7 +16,7 @@
"dependencies": { "dependencies": {
"@mdx-js/loader": "^3.1.0", "@mdx-js/loader": "^3.1.0",
"@mdx-js/react": "^3.1.0", "@mdx-js/react": "^3.1.0",
"@medusajs/icons": "~2.5.1", "@medusajs/icons": "~2.7.1",
"@next/mdx": "15.3.1", "@next/mdx": "15.3.1",
"clsx": "^2.1.0", "clsx": "^2.1.0",
"docs-ui": "*", "docs-ui": "*",
File diff suppressed because it is too large Load Diff
+11 -1
View File
@@ -66,7 +66,7 @@ async function main() {
"commerce-modules" "commerce-modules"
), ),
allowedFilesPatterns: [ allowedFilesPatterns: [
/^(?!.*\/(workflows|js-sdk|extend|events|admin-widget-zones)\/).*$/, /^(?!.*\/(workflows|js-sdk|extend|admin-widget-zones)\/).*$/,
], ],
}, },
{ {
@@ -101,6 +101,16 @@ async function main() {
}, },
}, },
}, },
{
dir: path.join(
process.cwd(),
"..",
"resources",
"references",
"modules",
"events"
),
},
{ {
dir: path.join(process.cwd(), "..", "resources", "app", "medusa-cli"), dir: path.join(process.cwd(), "..", "resources", "app", "medusa-cli"),
}, },
@@ -197,7 +197,7 @@ const priceSet = await pricingModuleService.createPriceSets({
amount: 200, amount: 200,
currency_code: "EUR", currency_code: "EUR",
min_quantity: 100, min_quantity: 100,
} },
], ],
}) })
``` ```
@@ -101,10 +101,10 @@ const { result } = await createProductsWorkflow(container)
}, },
], ],
// ... // ...
}] }],
}], }],
// ... // ...
} },
}) })
``` ```
@@ -217,8 +217,8 @@ To create the Bundled Product Module's service, create the file `src/modules/bun
```ts title="src/modules/bundled-product/service.ts" ```ts title="src/modules/bundled-product/service.ts"
import { MedusaService } from "@medusajs/framework/utils" import { MedusaService } from "@medusajs/framework/utils"
import { Bundle } from "./models/bundle"; import { Bundle } from "./models/bundle"
import { BundleItem } from "./models/bundle-item"; import { BundleItem } from "./models/bundle-item"
export default class BundledProductModuleService extends MedusaService({ export default class BundledProductModuleService extends MedusaService({
Bundle, Bundle,
@@ -338,9 +338,9 @@ You can define links between data models in a TypeScript or JavaScript file unde
So, to define the link between a bundle and a product, create the file `src/links/bundle-product.ts` with the following content: So, to define the link between a bundle and a product, create the file `src/links/bundle-product.ts` with the following content:
```ts title="src/links/bundle-product.ts" ```ts title="src/links/bundle-product.ts"
import { defineLink } from "@medusajs/framework/utils"; import { defineLink } from "@medusajs/framework/utils"
import ProductModule from "@medusajs/medusa/product"; import ProductModule from "@medusajs/medusa/product"
import BundledProductsModule from "../modules/bundled-product"; import BundledProductsModule from "../modules/bundled-product"
export default defineLink( export default defineLink(
BundledProductsModule.linkable.bundle, BundledProductsModule.linkable.bundle,
@@ -360,9 +360,9 @@ You'll later learn how to query and manage the linked records.
Next, you'll define the link between the `BundleItem` data model and the `Product` data model. Create the file `src/links/bundle-item-product.ts` with the following content: Next, you'll define the link between the `BundleItem` data model and the `Product` data model. Create the file `src/links/bundle-item-product.ts` with the following content:
```ts title="src/links/bundle-item-product.ts" ```ts title="src/links/bundle-item-product.ts"
import { defineLink } from "@medusajs/framework/utils"; import { defineLink } from "@medusajs/framework/utils"
import ProductModule from "@medusajs/medusa/product"; import ProductModule from "@medusajs/medusa/product"
import BundledProductsModule from "../modules/bundled-product"; import BundledProductsModule from "../modules/bundled-product"
export default defineLink( export default defineLink(
{ {
@@ -579,13 +579,13 @@ export const createBundleItemsStep = createStep(
container.resolve(BUNDLED_PRODUCT_MODULE) container.resolve(BUNDLED_PRODUCT_MODULE)
const bundleItems = await bundledProductModuleService.createBundleItems( const bundleItems = await bundledProductModuleService.createBundleItems(
items.map(item => ({ items.map((item) => ({
bundle_id, bundle_id,
quantity: item.quantity, quantity: item.quantity,
})) }))
) )
return new StepResponse(bundleItems, bundleItems.map(item => item.id)) return new StepResponse(bundleItems, bundleItems.map((item) => item.id))
}, },
async (itemIds, { container }) => { async (itemIds, { container }) => {
if (!itemIds?.length) { if (!itemIds?.length) {
@@ -657,7 +657,7 @@ export const createBundledProductWorkflow = createWorkflow(
const bundleProduct = createProductsWorkflow.runAsStep({ const bundleProduct = createProductsWorkflow.runAsStep({
input: { input: {
products: [bundleData.product], products: [bundleData.product],
} },
}) })
createRemoteLinkStep([{ createRemoteLinkStep([{
@@ -671,7 +671,7 @@ export const createBundledProductWorkflow = createWorkflow(
const bundleProducttemLinks = transform({ const bundleProducttemLinks = transform({
bundleData, bundleData,
bundleItems bundleItems,
}, (data) => { }, (data) => {
return data.bundleItems.map((item, index) => ({ return data.bundleItems.map((item, index) => ({
[BUNDLED_PRODUCT_MODULE]: { [BUNDLED_PRODUCT_MODULE]: {
@@ -758,16 +758,16 @@ export const bundledProductsRouteHighlights = [
```ts title="src/api/admin/bundled-products/route.ts" highlights={bundledProductsRouteHighlights} ```ts title="src/api/admin/bundled-products/route.ts" highlights={bundledProductsRouteHighlights}
import { import {
AuthenticatedMedusaRequest, AuthenticatedMedusaRequest,
MedusaResponse MedusaResponse,
} from "@medusajs/framework/http"; } from "@medusajs/framework/http"
import { z } from "zod"; import { z } from "zod"
import { import {
AdminCreateProduct AdminCreateProduct,
} from "@medusajs/medusa/api/admin/products/validators" } from "@medusajs/medusa/api/admin/products/validators"
import { import {
createBundledProductWorkflow, createBundledProductWorkflow,
CreateBundledProductWorkflowInput CreateBundledProductWorkflowInput,
} from "../../../workflows/create-bundled-product"; } from "../../../workflows/create-bundled-product"
export const PostBundledProductsSchema = z.object({ export const PostBundledProductsSchema = z.object({
title: z.string(), title: z.string(),
@@ -785,12 +785,12 @@ export async function POST(
res: MedusaResponse res: MedusaResponse
) { ) {
const { const {
result: bundledProduct result: bundledProduct,
} = await createBundledProductWorkflow(req.scope) } = await createBundledProductWorkflow(req.scope)
.run({ .run({
input: { input: {
bundle: req.validatedBody, bundle: req.validatedBody,
} as CreateBundledProductWorkflowInput } as CreateBundledProductWorkflowInput,
}) })
res.json({ res.json({
@@ -843,9 +843,9 @@ export const middlewaresHighlights = [
```ts title="src/api/middlewares.ts" highlights={middlewaresHighlights} ```ts title="src/api/middlewares.ts" highlights={middlewaresHighlights}
import { import {
defineMiddlewares, defineMiddlewares,
validateAndTransformBody validateAndTransformBody,
} from "@medusajs/framework/http"; } from "@medusajs/framework/http"
import { PostBundledProductsSchema } from "./admin/bundled-products/route"; import { PostBundledProductsSchema } from "./admin/bundled-products/route"
export default defineMiddlewares({ export default defineMiddlewares({
routes: [ routes: [
@@ -856,7 +856,7 @@ export default defineMiddlewares({
validateAndTransformBody(PostBundledProductsSchema), validateAndTransformBody(PostBundledProductsSchema),
], ],
}, },
] ],
}) })
``` ```
@@ -893,10 +893,10 @@ export async function GET(
const { const {
data: bundledProducts, data: bundledProducts,
metadata: { count, take, skip } = {} metadata: { count, take, skip } = {},
} = await query.graph({ } = await query.graph({
entity: "bundle", entity: "bundle",
...req.queryConfig ...req.queryConfig,
}) })
res.json({ res.json({
@@ -932,8 +932,8 @@ export const getBundledProductsMiddlewareHighlights = [
```ts title="src/api/middlewares.ts" highlights={getBundledProductsMiddlewareHighlights} ```ts title="src/api/middlewares.ts" highlights={getBundledProductsMiddlewareHighlights}
// other imports... // other imports...
import { validateAndTransformQuery } from "@medusajs/framework/http"; import { validateAndTransformQuery } from "@medusajs/framework/http"
import { createFindParams } from "@medusajs/medusa/api/utils/validators"; import { createFindParams } from "@medusajs/medusa/api/utils/validators"
export default defineMiddlewares({ export default defineMiddlewares({
routes: [ routes: [
@@ -955,7 +955,7 @@ export default defineMiddlewares({
}), }),
], ],
}, },
] ],
}) })
``` ```
@@ -1253,7 +1253,7 @@ import {
Input, Input,
Label, Label,
Select, Select,
toast toast,
} from "@medusajs/ui" } from "@medusajs/ui"
import { useState, useRef, useCallback, useMemo } from "react" import { useState, useRef, useCallback, useMemo } from "react"
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query" import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
@@ -1270,7 +1270,7 @@ const CreateBundledProduct = () => {
{ {
product_id: undefined, product_id: undefined,
quantity: 1, quantity: 1,
} },
]) ])
// TODO fetch products // TODO fetch products
} }
@@ -1303,8 +1303,9 @@ const [products, setProducts] = useState<HttpTypes.AdminProduct[]>([])
const productsLimit = 15 const productsLimit = 15
const [currnetProductPage, setCurrentProductPage] = useState(0) const [currnetProductPage, setCurrentProductPage] = useState(0)
const [productsCount, setProductsCount] = useState(0) const [productsCount, setProductsCount] = useState(0)
const hasNextPage = useMemo(() => const hasNextPage = useMemo(() => {
productsCount ? productsCount > productsLimit : true, return productsCount ? productsCount > productsLimit : true
},
[productsCount, productsLimit]) [productsCount, productsLimit])
const queryClient = useQueryClient() const queryClient = useQueryClient()
useQuery({ useQuery({
@@ -1353,14 +1354,14 @@ export const createBundledProductComponentHighlights3 = [
```tsx title="src/admin/components/create-bundled-product.tsx" highlights={createBundledProductComponentHighlights3} ```tsx title="src/admin/components/create-bundled-product.tsx" highlights={createBundledProductComponentHighlights3}
const { const {
mutateAsync: createBundledProduct, mutateAsync: createBundledProduct,
isPending: isCreating isPending: isCreating,
} = useMutation({ } = useMutation({
mutationFn: async (data: Record<string, any>) => { mutationFn: async (data: Record<string, any>) => {
await sdk.client.fetch("/admin/bundled-products", { await sdk.client.fetch("/admin/bundled-products", {
method: "POST", method: "POST",
body: data body: data,
}) })
} },
}) })
const handleCreate = async () => { const handleCreate = async () => {
@@ -1372,8 +1373,8 @@ const handleCreate = async () => {
options: [ options: [
{ {
title: "Default", title: "Default",
values: ["default"] values: ["default"],
} },
], ],
status: "published", status: "published",
variants: [ variants: [
@@ -1382,21 +1383,21 @@ const handleCreate = async () => {
// You can set prices in the product's page // You can set prices in the product's page
prices: [], prices: [],
options: { options: {
Default: "default" Default: "default",
}, },
manage_inventory: false manage_inventory: false,
} },
] ],
}, },
items: items.map((item) => ({ items: items.map((item) => ({
product_id: item.product_id, product_id: item.product_id,
quantity: item.quantity, quantity: item.quantity,
})) })),
}) })
setOpen(false) setOpen(false)
toast.success("Bundled product created successfully") toast.success("Bundled product created successfully")
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: ["bundled-products"] queryKey: ["bundled-products"],
}) })
setTitle("") setTitle("")
setItems([{ product_id: undefined, quantity: 1 }]) setItems([{ product_id: undefined, quantity: 1 }])
@@ -1452,7 +1453,7 @@ const BundledProductItem = ({
setItems, setItems,
products, products,
fetchMoreProducts, fetchMoreProducts,
hasNextPage hasNextPage,
}: BundledProductItemProps) => { }: BundledProductItemProps) => {
const observer = useRef( const observer = useRef(
new IntersectionObserver( new IntersectionObserver(
@@ -1491,14 +1492,14 @@ const BundledProductItem = ({
value={item.product_id} value={item.product_id}
onValueChange={(value) => onValueChange={(value) =>
setItems((items) => setItems((items) =>
items.map((item, i) => items.map((item, i) => {
i === index return i === index
? { ? {
...item, ...item,
product_id: value, product_id: value,
} }
: item : item
) })
) )
} }
> >
@@ -1530,11 +1531,11 @@ const BundledProductItem = ({
value={item.quantity} value={item.quantity}
onChange={(e) => onChange={(e) =>
setItems((items) => setItems((items) =>
items.map((item, i) => items.map((item, i) => {
i === index return i === index
? { ...item, quantity: parseInt(e.target.value) } ? { ...item, quantity: parseInt(e.target.value) }
: item : item
) })
) )
} }
/> />
@@ -1838,8 +1839,8 @@ export const prepareBundleCartDataStep = createStep(
quantity: item.quantity * quantity, quantity: item.quantity * quantity,
metadata: { metadata: {
bundle_id: bundle.id, bundle_id: bundle.id,
quantity: quantity quantity: quantity,
} },
} }
}) })
@@ -1869,8 +1870,8 @@ return {
unit_price: 100, unit_price: 100,
metadata: { metadata: {
bundle_id: bundle.id, bundle_id: bundle.id,
quantity: quantity quantity: quantity,
} },
} }
``` ```
@@ -1895,15 +1896,15 @@ export const addBundleToCartWorkflowHighlights = [
import { import {
createWorkflow, createWorkflow,
transform, transform,
WorkflowResponse WorkflowResponse,
} from "@medusajs/framework/workflows-sdk" } from "@medusajs/framework/workflows-sdk"
import { import {
addToCartWorkflow, addToCartWorkflow,
useQueryGraphStep useQueryGraphStep,
} from "@medusajs/medusa/core-flows" } from "@medusajs/medusa/core-flows"
import { import {
prepareBundleCartDataStep, prepareBundleCartDataStep,
PrepareBundleCartDataStepInput PrepareBundleCartDataStepInput,
} from "./steps/prepare-bundle-cart-data" } from "./steps/prepare-bundle-cart-data"
type AddBundleToCartWorkflowInput = { type AddBundleToCartWorkflowInput = {
@@ -1926,27 +1927,27 @@ export const addBundleToCartWorkflow = createWorkflow(
"id", "id",
"items.*", "items.*",
"items.product.*", "items.product.*",
"items.product.variants.*" "items.product.variants.*",
], ],
filters: { filters: {
id: bundle_id id: bundle_id,
}, },
options: { options: {
throwIfKeyNotFound: true throwIfKeyNotFound: true,
} },
}) })
const itemsToAdd = prepareBundleCartDataStep({ const itemsToAdd = prepareBundleCartDataStep({
bundle: data[0], bundle: data[0],
quantity, quantity,
items items,
} as unknown as PrepareBundleCartDataStepInput) } as unknown as PrepareBundleCartDataStepInput)
addToCartWorkflow.runAsStep({ addToCartWorkflow.runAsStep({
input: { input: {
cart_id, cart_id,
items: itemsToAdd items: itemsToAdd,
} },
}) })
// @ts-ignore // @ts-ignore
@@ -1979,19 +1980,19 @@ You'll now create the API route that exposes the workflow's functionalities to s
To create the API route, create the file `src/api/store/carts/[id]/line-item-bundles/route.ts` with the following content: To create the API route, create the file `src/api/store/carts/[id]/line-item-bundles/route.ts` with the following content:
```ts title="src/api/store/carts/[id]/line-item-bundles/route.ts" collapsibleLines="1-6" expandButtonLabel="Show Imports" ```ts title="src/api/store/carts/[id]/line-item-bundles/route.ts" collapsibleLines="1-6" expandButtonLabel="Show Imports"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"; import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { z } from "zod"; import { z } from "zod"
import { import {
addBundleToCartWorkflow addBundleToCartWorkflow,
} from "../../../../../workflows/add-bundle-to-cart"; } from "../../../../../workflows/add-bundle-to-cart"
export const PostCartsBundledLineItemsSchema = z.object({ export const PostCartsBundledLineItemsSchema = z.object({
bundle_id: z.string(), bundle_id: z.string(),
quantity: z.number().default(1), quantity: z.number().default(1),
items: z.array(z.object({ items: z.array(z.object({
item_id: z.string(), item_id: z.string(),
variant_id: z.string() variant_id: z.string(),
})) })),
}) })
type PostCartsBundledLineItemsSchema = z.infer< type PostCartsBundledLineItemsSchema = z.infer<
@@ -2008,12 +2009,12 @@ export async function POST(
cart_id: req.params.id, cart_id: req.params.id,
bundle_id: req.validatedBody.bundle_id, bundle_id: req.validatedBody.bundle_id,
quantity: req.validatedBody.quantity || 1, quantity: req.validatedBody.quantity || 1,
items: req.validatedBody.items items: req.validatedBody.items,
} },
}) })
res.json({ res.json({
cart cart,
}) })
} }
``` ```
@@ -2039,8 +2040,8 @@ In `src/api/middlewares.ts`, add a new middleware object to the `routes` array:
```ts title="src/api/middlewares.ts" ```ts title="src/api/middlewares.ts"
// other imports... // other imports...
import { import {
PostCartsBundledLineItemsSchema PostCartsBundledLineItemsSchema,
} from "./store/carts/[id]/line-item-bundles/route"; } from "./store/carts/[id]/line-item-bundles/route"
export default defineMiddlewares({ export default defineMiddlewares({
routes: [ routes: [
@@ -2049,10 +2050,10 @@ export default defineMiddlewares({
matcher: "/store/carts/:id/line-item-bundles", matcher: "/store/carts/:id/line-item-bundles",
methods: ["POST"], methods: ["POST"],
middlewares: [ middlewares: [
validateAndTransformBody(PostCartsBundledLineItemsSchema) validateAndTransformBody(PostCartsBundledLineItemsSchema),
], ],
} },
] ],
}) })
``` ```
@@ -2076,8 +2077,8 @@ export const bundleProductsRouteHighlights = [
] ]
```ts title="src/api/store/bundle-products/[id]/route.ts" highlights={bundleProductsRouteHighlights} ```ts title="src/api/store/bundle-products/[id]/route.ts" highlights={bundleProductsRouteHighlights}
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"; import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { QueryContext } from "@medusajs/framework/utils"; import { QueryContext } from "@medusajs/framework/utils"
export async function GET( export async function GET(
req: MedusaRequest, req: MedusaRequest,
@@ -2100,7 +2101,7 @@ export async function GET(
"items.product.variants.options.*", "items.product.variants.options.*",
], ],
filters: { filters: {
id id,
}, },
context: { context: {
items: { items: {
@@ -2111,16 +2112,16 @@ export async function GET(
currency_code, currency_code,
}), }),
}, },
} },
} },
}, },
}, { }, {
throwIfKeyNotFound: true throwIfKeyNotFound: true,
}) })
res.json({ res.json({
bundle_product: data[0] bundle_product: data[0],
}) })
} }
``` ```
@@ -2283,7 +2284,7 @@ const pricedProduct = await listProducts({
countryCode: params.countryCode, countryCode: params.countryCode,
queryParams: { queryParams: {
handle: params.handle, handle: params.handle,
fields: "*bundle" fields: "*bundle",
}, },
}).then(({ response }) => response.products[0]) }).then(({ response }) => response.products[0])
``` ```
@@ -2313,7 +2314,7 @@ export async function addBundleToCart({
bundleId, bundleId,
quantity, quantity,
countryCode, countryCode,
items items,
}: { }: {
bundleId: string bundleId: string
quantity: number quantity: number
@@ -2344,7 +2345,7 @@ export async function addBundleToCart({
body: { body: {
bundle_id: bundleId, bundle_id: bundleId,
quantity, quantity,
items items,
}, },
headers, headers,
}) })
@@ -2442,7 +2443,7 @@ export const bundleActionsComponentHighlights2 = [
// For each product, if it has only 1 variant, preselect it // For each product, if it has only 1 variant, preselect it
useEffect(() => { useEffect(() => {
const initialOptions: Record<string, Record<string, string>> = {} const initialOptions: Record<string, Record<string, string>> = {}
bundle.items.forEach(item => { bundle.items.forEach((item) => {
if (item.product.variants?.length === 1) { if (item.product.variants?.length === 1) {
const variantOptions = optionsAsKeymap(item.product.variants[0].options) const variantOptions = optionsAsKeymap(item.product.variants[0].options)
initialOptions[item.product.id] = variantOptions ?? {} initialOptions[item.product.id] = variantOptions ?? {}
@@ -2454,10 +2455,10 @@ useEffect(() => {
}, [bundle.items]) }, [bundle.items])
const selectedVariants = useMemo(() => { const selectedVariants = useMemo(() => {
return bundle.items.map(item => { return bundle.items.map((item) => {
if (!item.product.variants || item.product.variants.length === 0) return undefined if (!item.product.variants || item.product.variants.length === 0) {return undefined}
return item.product.variants.find(v => { return item.product.variants.find((v) => {
const variantOptions = optionsAsKeymap(v.options) const variantOptions = optionsAsKeymap(v.options)
return isEqual(variantOptions, productOptions[item.product.id]) return isEqual(variantOptions, productOptions[item.product.id])
}) })
@@ -2465,17 +2466,17 @@ const selectedVariants = useMemo(() => {
}, [bundle.items, productOptions]) }, [bundle.items, productOptions])
const setOptionValue = (productId: string, optionId: string, value: string) => { const setOptionValue = (productId: string, optionId: string, value: string) => {
setProductOptions(prev => ({ setProductOptions((prev) => ({
...prev, ...prev,
[productId]: { [productId]: {
...prev[productId], ...prev[productId],
[optionId]: value, [optionId]: value,
} },
})) }))
} }
const allVariantsSelected = useMemo(() => { const allVariantsSelected = useMemo(() => {
return selectedVariants.every(v => v !== undefined) return selectedVariants.every((v) => v !== undefined)
}, [selectedVariants]) }, [selectedVariants])
// TODO handle add to cart // TODO handle add to cart
@@ -2497,7 +2498,7 @@ Replace the `TODO` in the `BundleActions` component with the following code:
```tsx title="src/modules/products/components/bundle-actions/index.tsx" badgeLabel="Storefront" badgeColor="blue" ```tsx title="src/modules/products/components/bundle-actions/index.tsx" badgeLabel="Storefront" badgeColor="blue"
const handleAddToCart = async () => { const handleAddToCart = async () => {
if (!allVariantsSelected) return if (!allVariantsSelected) {return}
setIsAdding(true) setIsAdding(true)
await addBundleToCart({ await addBundleToCart({
@@ -2541,6 +2542,7 @@ Then, in the `return` statement, pass the `className` prop in the classes of the
className={clx("text-xl-semi", { className={clx("text-xl-semi", {
"text-ui-fg-interactive": selectedPrice.price_type === "sale", "text-ui-fg-interactive": selectedPrice.price_type === "sale",
}, className)} }, className)}
>
{/* ... */} {/* ... */}
</span> </span>
</div> </div>
@@ -2805,11 +2807,11 @@ export const removeBundleFromCartWorkflowHighlights = [
import { import {
createWorkflow, createWorkflow,
transform, transform,
WorkflowResponse WorkflowResponse,
} from "@medusajs/framework/workflows-sdk" } from "@medusajs/framework/workflows-sdk"
import { import {
deleteLineItemsWorkflow, deleteLineItemsWorkflow,
useQueryGraphStep useQueryGraphStep,
} from "@medusajs/medusa/core-flows" } from "@medusajs/medusa/core-flows"
type RemoveBundleFromCartWorkflowInput = { type RemoveBundleFromCartWorkflowInput = {
@@ -2831,7 +2833,7 @@ export const removeBundleFromCartWorkflow = createWorkflow(
}, },
options: { options: {
throwIfKeyNotFound: true, throwIfKeyNotFound: true,
} },
}) })
const itemsToRemove = transform({ const itemsToRemove = transform({
@@ -2847,7 +2849,7 @@ export const removeBundleFromCartWorkflow = createWorkflow(
input: { input: {
cart_id, cart_id,
ids: itemsToRemove, ids: itemsToRemove,
} },
}) })
// retrieve cart again // retrieve cart again
@@ -2886,10 +2888,10 @@ Next, you'll create the API route that exposes the workflow's functionality to s
Create the file `src/api/store/carts/[id]/line-item-bundles/[bundle_id]/route.ts` with the following content: Create the file `src/api/store/carts/[id]/line-item-bundles/[bundle_id]/route.ts` with the following content:
```ts title="src/api/store/carts/[id]/line-item-bundles/[bundle_id]/route.ts" ```ts title="src/api/store/carts/[id]/line-item-bundles/[bundle_id]/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"; import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import { import {
removeBundleFromCartWorkflow removeBundleFromCartWorkflow,
} from "../../../../../../workflows/remove-bundle-from-cart"; } from "../../../../../../workflows/remove-bundle-from-cart"
export async function DELETE( export async function DELETE(
req: MedusaRequest, req: MedusaRequest,
@@ -2900,11 +2902,11 @@ export async function DELETE(
input: { input: {
cart_id: req.params.id, cart_id: req.params.id,
bundle_id: req.params.bundle_id, bundle_id: req.params.bundle_id,
} },
}) })
res.json({ res.json({
cart cart,
}) })
} }
``` ```
@@ -0,0 +1,78 @@
"use client"
import { Brackets, CheckCircle, SquareTwoStack, Tag } from "@medusajs/icons"
import {
DropdownMenu,
H2,
H2Props,
H3,
H3Props,
useCopy,
useGenerateSnippet,
} from "docs-ui"
type EventHeaderProps = (
| {
headerLvl: "2"
headerProps: H2Props
}
| {
headerLvl: "3"
headerProps: H3Props
}
) & {
eventName: string
payload: string
}
export const EventHeader = ({
headerLvl,
headerProps,
eventName,
payload: payloadStr,
}: EventHeaderProps) => {
const Header = headerLvl === "2" ? H2 : H3
const { snippet } = useGenerateSnippet({
type: "subscriber",
options: {
event: eventName,
payload: payloadStr,
},
})
const { handleCopy: handleEventNameCopy, isCopied: eventNameCopied } =
useCopy(eventName)
const { handleCopy: handleSnippetCopy, isCopied: snippetCopied } =
useCopy(snippet)
return (
<div className="flex items-center justify-between flex-wrap">
<Header {...headerProps}>{eventName}</Header>
<DropdownMenu
dropdownButtonContent={
<>
{eventNameCopied || snippetCopied ? (
<CheckCircle />
) : (
<SquareTwoStack />
)}
</>
}
menuItems={[
{
type: "action",
title: "Copy event name",
action: () => handleEventNameCopy(),
icon: <Tag />,
},
{
type: "action",
title: "Copy subscriber for event",
action: () => handleSnippetCopy(),
icon: <Brackets />,
},
]}
menuClassName="z-10"
/>
</div>
)
}
@@ -10,8 +10,10 @@ import {
Table, Table,
Badge, Badge,
Tooltip, Tooltip,
CopyGeneratedSnippetButton,
} from "docs-ui" } from "docs-ui"
import { CommerceModuleSections } from "../CommerceModuleSections" import { CommerceModuleSections } from "../CommerceModuleSections"
import { EventHeader } from "../EventHeader"
const MDXComponents: MDXComponentsType = { const MDXComponents: MDXComponentsType = {
...UiMdxComponents, ...UiMdxComponents,
@@ -27,6 +29,8 @@ const MDXComponents: MDXComponentsType = {
Tooltip: (props) => { Tooltip: (props) => {
return <Tooltip {...props} /> return <Tooltip {...props} />
}, },
EventHeader,
CopyGeneratedSnippetButton,
} }
export default MDXComponents export default MDXComponents
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -16,7 +16,7 @@
"dependencies": { "dependencies": {
"@mdx-js/loader": "^3.1.0", "@mdx-js/loader": "^3.1.0",
"@mdx-js/react": "^3.1.0", "@mdx-js/react": "^3.1.0",
"@medusajs/icons": "~2.5.1", "@medusajs/icons": "~2.7.1",
"@next/mdx": "15.3.1", "@next/mdx": "15.3.1",
"clsx": "^2.1.0", "clsx": "^2.1.0",
"docs-ui": "*", "docs-ui": "*",
+3 -3
View File
@@ -16,9 +16,9 @@
"dependencies": { "dependencies": {
"@faker-js/faker": "^8.0.2", "@faker-js/faker": "^8.0.2",
"@mdx-js/react": "^3.1.0", "@mdx-js/react": "^3.1.0",
"@medusajs/icons": "2.6.0", "@medusajs/icons": "2.7.1",
"@medusajs/ui": "4.0.6", "@medusajs/ui": "4.0.9",
"@medusajs/ui-preset": "2.6.0", "@medusajs/ui-preset": "2.7.1",
"autoprefixer": "10.4.14", "autoprefixer": "10.4.14",
"build-scripts": "*", "build-scripts": "*",
"clsx": "^2.0.0", "clsx": "^2.0.0",
+1 -1
View File
@@ -16,7 +16,7 @@
"dependencies": { "dependencies": {
"@mdx-js/loader": "^3.1.0", "@mdx-js/loader": "^3.1.0",
"@mdx-js/react": "^3.1.0", "@mdx-js/react": "^3.1.0",
"@medusajs/icons": "~2.5.1", "@medusajs/icons": "~2.7.1",
"@next/mdx": "15.0.4", "@next/mdx": "15.0.4",
"clsx": "^2.1.0", "clsx": "^2.1.0",
"docs-ui": "*", "docs-ui": "*",
+2 -2
View File
@@ -57,8 +57,8 @@
}, },
"dependencies": { "dependencies": {
"@emotion/is-prop-valid": "^1.3.1", "@emotion/is-prop-valid": "^1.3.1",
"@medusajs/icons": "2.6.0", "@medusajs/icons": "2.7.1",
"@medusajs/ui": "4.0.6", "@medusajs/ui": "4.0.9",
"@next/third-parties": "15.3.1", "@next/third-parties": "15.3.1",
"@octokit/request": "^8.1.1", "@octokit/request": "^8.1.1",
"@react-hook/resize-observer": "^1.2.6", "@react-hook/resize-observer": "^1.2.6",
@@ -5,6 +5,10 @@ import clsx from "clsx"
import { Tooltip } from "@/components" import { Tooltip } from "@/components"
import { useCopy } from "../../hooks" import { useCopy } from "../../hooks"
export type CopyButtonChildFn = (props: {
isCopied: boolean
}) => React.ReactNode
export type CopyButtonProps = { export type CopyButtonProps = {
text: string text: string
buttonClassName?: string buttonClassName?: string
@@ -17,7 +21,8 @@ export type CopyButtonProps = {
| React.TouchEvent<HTMLSpanElement> | React.TouchEvent<HTMLSpanElement>
) => void ) => void
handleTouch?: boolean handleTouch?: boolean
} & Omit<React.HTMLAttributes<HTMLDivElement>, "onCopy"> children?: React.ReactNode | CopyButtonChildFn
} & Omit<React.HTMLAttributes<HTMLDivElement>, "onCopy" | "children">
export const CopyButton = ({ export const CopyButton = ({
text, text,
@@ -64,7 +69,7 @@ export const CopyButton = ({
} }
}} }}
> >
{children} {typeof children === "function" ? children({ isCopied }) : children}
</span> </span>
</Tooltip> </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 React from "react"
import { LlmDropdown } from "../../LlmDropdown" import { LlmDropdown } from "../../LlmDropdown"
type H1Props = React.HTMLAttributes<HTMLHeadingElement> & { export type H1Props = React.HTMLAttributes<HTMLHeadingElement> & {
id?: string id?: string
hideLlmDropdown?: boolean hideLlmDropdown?: boolean
} }
@@ -5,7 +5,7 @@ import React from "react"
import { CopyButton, Link } from "@/components" import { CopyButton, Link } from "@/components"
import { useHeadingUrl, useLayout } from "../../.." import { useHeadingUrl, useLayout } from "../../.."
type H2Props = React.HTMLAttributes<HTMLHeadingElement> & { export type H2Props = React.HTMLAttributes<HTMLHeadingElement> & {
id?: string id?: string
passRef?: React.RefObject<HTMLHeadingElement | null> passRef?: React.RefObject<HTMLHeadingElement | null>
} }
@@ -5,7 +5,7 @@ import React from "react"
import { CopyButton, Link } from "@/components" import { CopyButton, Link } from "@/components"
import { useHeadingUrl, useLayout } from "../../.." import { useHeadingUrl, useLayout } from "../../.."
type H3Props = React.HTMLAttributes<HTMLHeadingElement> & { export type H3Props = React.HTMLAttributes<HTMLHeadingElement> & {
id?: string id?: string
} }
@@ -7,7 +7,7 @@ export const H4 = ({
}: React.HTMLAttributes<HTMLHeadingElement>) => { }: React.HTMLAttributes<HTMLHeadingElement>) => {
return ( return (
<h4 <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} {...props}
/> />
) )
@@ -3,9 +3,8 @@
import React, { useRef, useState } from "react" import React, { useRef, useState } from "react"
import { useAiAssistant, useSiteConfig } from "../../providers" import { useAiAssistant, useSiteConfig } from "../../providers"
import { usePathname } from "next/navigation" import { usePathname } from "next/navigation"
import { Button } from "../Button"
import { AiAssistent, Book } from "@medusajs/icons" import { AiAssistent, Book } from "@medusajs/icons"
import { Menu } from "../Menu" import { DropdownMenu, Menu } from "../Menu"
import { MarkdownIcon } from "../Icons/Markdown" import { MarkdownIcon } from "../Icons/Markdown"
import { useAiAssistantChat } from "../../providers/AiAssistant/Chat" import { useAiAssistantChat } from "../../providers/AiAssistant/Chat"
import clsx from "clsx" import clsx from "clsx"
@@ -30,43 +29,41 @@ export const LlmDropdown = () => {
const pageUrl = `${baseUrl}${basePath}${pathname}` const pageUrl = `${baseUrl}${basePath}${pathname}`
return ( return (
<div className="relative hidden md:block"> <DropdownMenu
<Button open={open}
variant="transparent" setOpen={setOpen}
onClick={() => setOpen(!open)} dropdownButtonContent={<Book />}
className="!p-[6px] text-medusa-fg-subtle" menuComponent={
buttonRef={ref} <Menu
> items={[
<Book /> {
</Button> type: "link",
<Menu title: "View as Markdown",
items={[ link: `${pageUrl}/index.html.md`,
{ icon: <MarkdownIcon width={19} height={19} />,
type: "link", openInNewTab: true,
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)
}, },
icon: <AiAssistent />, {
}, type: "action",
]} title: "Ask AI Assistant",
className={clsx( action: () => {
"absolute right-0 top-[calc(100%+8px)] w-max", if (loading) {
!open && "hidden" return
)} }
/> setQuestion(`Explain the page ${pageUrl}`)
</div> 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> </div>
) )
} }
export * from "./Dropdown"
@@ -15,6 +15,7 @@ export * from "./CodeMdx"
export * from "./CodeTabs" export * from "./CodeTabs"
export * from "./CodeTabs/Item" export * from "./CodeTabs/Item"
export * from "./CopyButton" export * from "./CopyButton"
export * from "./CopyGeneratedSnippetButton"
export * from "./Details" export * from "./Details"
export * from "./Details/Summary" export * from "./Details/Summary"
export * from "./DetailsList" export * from "./DetailsList"
+1
View File
@@ -5,6 +5,7 @@ export * from "./use-click-outside"
export * from "./use-collapsible" export * from "./use-collapsible"
export * from "./use-collapsible-code-lines" export * from "./use-collapsible-code-lines"
export * from "./use-copy" export * from "./use-copy"
export * from "./use-generate-snippet"
export * from "./use-heading-url" export * from "./use-heading-url"
export * from "./use-current-learning-path" export * from "./use-current-learning-path"
export * from "./use-is-external-link" 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 "./check-sidebar-item-visibility"
export * from "./decode-str" export * from "./decode-str"
export * from "./dom-utils" export * from "./dom-utils"
export * from "./event-parser"
export * from "./get-link-with-base-path" export * from "./get-link-with-base-path"
export * from "./get-local-search" export * from "./get-local-search"
export * from "./get-navbar-items" export * from "./get-navbar-items"
+20 -2
View File
@@ -5,6 +5,8 @@ import {
Expression, Expression,
ExpressionJsVar, ExpressionJsVar,
ExpressionJsVarLiteral, ExpressionJsVarLiteral,
JSXElementExpression,
JSXTextExpression,
LiteralExpression, LiteralExpression,
ObjectExpression, ObjectExpression,
} from "types" } from "types"
@@ -64,7 +66,23 @@ function expressionToJs(
data: (expression as LiteralExpression).value, data: (expression as LiteralExpression).value,
} as ExpressionJsVarLiteral } as ExpressionJsVarLiteral
case "JSXElement": case "JSXElement":
// ignore JSXElements case "JSXFragment":
return // 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, parseComponentExample,
parseComponentReference, parseComponentReference,
parseDetails, parseDetails,
parseEventHeader,
parseHookValues, parseHookValues,
parseIconSearch, parseIconSearch,
parseNote, parseNote,
@@ -48,6 +49,7 @@ const parsers: Record<string, ComponentParser> = {
HookValues: parseHookValues, HookValues: parseHookValues,
Colors: parseColors, Colors: parseColors,
SplitList: parseSplitList, SplitList: parseSplitList,
EventHeader: parseEventHeader,
} }
const isComponentAllowed = (nodeName: string): boolean => { const isComponentAllowed = (nodeName: string): boolean => {
+51 -1
View File
@@ -951,6 +951,54 @@ export const parseSplitList: ComponentParser = (
return [SKIP, index] 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 * Helpers
*/ */
@@ -964,11 +1012,13 @@ const getTextNode = (node: UnistNode): UnistNode | undefined => {
textNode = child textNode = child
} else if (child.type === "paragraph") { } else if (child.type === "paragraph") {
textNode = getTextNode(child) textNode = getTextNode(child)
} else if (child.type === "link") {
textNode = getTextNode(child)
} else if (child.value) { } else if (child.value) {
textNode = child textNode = child
} }
return false return textNode !== undefined
}) })
return textNode return textNode
+1 -1
View File
@@ -32,7 +32,7 @@
"watch": "tsc --watch" "watch": "tsc --watch"
}, },
"devDependencies": { "devDependencies": {
"@medusajs/icons": "2.6.0", "@medusajs/icons": "2.7.1",
"@types/node": "^20.11.20", "@types/node": "^20.11.20",
"rimraf": "^5.0.5", "rimraf": "^5.0.5",
"tsconfig": "*", "tsconfig": "*",
+13
View File
@@ -37,6 +37,17 @@ export type LiteralExpression = {
raw: string raw: string
} }
export type JSXElementExpression = {
type: "JSXElement" | "JSXFragment"
children: Expression[]
}
export type JSXTextExpression = {
type: "JSXText"
value: string
raw: string
}
export type Expression = export type Expression =
| { | {
type: string type: string
@@ -44,6 +55,8 @@ export type Expression =
| ArrayExpression | ArrayExpression
| ObjectExpression | ObjectExpression
| LiteralExpression | LiteralExpression
| JSXElementExpression
| JSXTextExpression
export interface Estree { export interface Estree {
body?: { body?: {
@@ -67,7 +67,7 @@ function formatEventsType(
eventVariable.name.replaceAll("WorkflowEvents", "") eventVariable.name.replaceAll("WorkflowEvents", "")
) )
if (showHeader) { if (showHeader) {
content.push(`## ${header} Events`) content.push(`${"#".repeat(subtitleLevel - 1)} ${header} Events`)
} }
content.push("") content.push("")
@@ -93,9 +93,7 @@ function formatEventsType(
.find((tag) => tag.tag === "@eventName") .find((tag) => tag.tag === "@eventName")
?.content.map((content) => content.text) ?.content.map((content) => content.text)
.join("") || "" .join("") || ""
eventName = `[${eventName}](#${slugify(eventName.replace(".", ""), { eventName = `[${eventName}](#${getEventNameSlug(eventName)})`
lower: true,
})})`
const eventDescription = event.comment?.summary const eventDescription = event.comment?.summary
.map((content) => content.text) .map((content) => content.text)
.join("") .join("")
@@ -149,32 +147,30 @@ function formatEventsType(
const deprecatedTag = event.comment?.blockTags.find( const deprecatedTag = event.comment?.blockTags.find(
(tag) => tag.tag === "@deprecated" (tag) => tag.tag === "@deprecated"
) )
const deprecatedMessage = deprecatedTag?.content
.map((content) => content.text)
.join("")
.trim()
content.push(`${subHeaderPrefix} \`${eventName}\``) content.push(
getEventHeading({
titleLevel: subtitleLevel,
eventName: eventName || "",
payload: eventPayload || "",
deprecated: !!deprecatedTag,
deprecatedMessage,
})
)
content.push("") content.push("")
if (deprecatedTag) {
const deprecationText = deprecatedTag.content
.map((content) => content.text)
.join("")
.trim()
if (deprecationText.length) {
content.push(`<Tooltip text="${deprecationText}">`)
}
content.push(`<Badge variant="orange">Deprecated</Badge>`)
if (deprecationText.length) {
content.push(`</Tooltip>`)
}
content.push("")
}
content.push(eventDescription || "") content.push(eventDescription || "")
content.push("") content.push("")
content.push(`${subHeaderPrefix}# Payload`) content.push(`${subHeaderPrefix}# Payload`)
content.push("") content.push("")
content.push(eventPayload || "") content.push(eventPayload || "")
content.push("") content.push("")
content.push(`${subHeaderPrefix}# Workflows Emitting this Event`) content.push(
`${subHeaderPrefix}# Workflows Emitting this Event\n\nThe following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.`
)
content.push("") content.push("")
workflows?.forEach((workflow) => { workflows?.forEach((workflow) => {
content.push(`- [${workflow}](/references/medusa-workflows/${workflow})`) content.push(`- [${workflow}](/references/medusa-workflows/${workflow})`)
@@ -188,3 +184,42 @@ function formatEventsType(
return content.join("\n") return content.join("\n")
} }
function getEventHeading({
titleLevel,
eventName,
payload,
deprecated = false,
deprecatedMessage,
}: {
titleLevel: number
eventName: string
payload: string
deprecated?: boolean
deprecatedMessage?: string
}) {
const heading = [eventName]
if (deprecated) {
if (deprecatedMessage?.length) {
heading.push(`<Tooltip text="${deprecatedMessage}">`)
}
heading.push(`<Badge variant="orange">Deprecated</Badge>`)
if (deprecatedMessage?.length) {
heading.push(`</Tooltip>`)
}
}
return `<EventHeader headerLvl="${titleLevel}" headerProps={{ id: "${getEventNameSlug(
eventName
)}", children: (<>${heading.join(
"\n"
)}</>), className: "flex flex-wrap justify-center gap-docs_0.25" }} eventName="${eventName}" payload={\`${payload.replaceAll(
"`",
"\\`"
)}\`} />`
}
function getEventNameSlug(eventName: string) {
return slugify(eventName.replace(".", ""), { lower: true })
}
@@ -25,6 +25,7 @@ export default function () {
str += ` <Table.HeaderCell>\nEvent\n</Table.HeaderCell>\n` str += ` <Table.HeaderCell>\nEvent\n</Table.HeaderCell>\n`
str += ` <Table.HeaderCell>\nDescription\n</Table.HeaderCell>\n` str += ` <Table.HeaderCell>\nDescription\n</Table.HeaderCell>\n`
str += ` <Table.HeaderCell>\nPayload\n</Table.HeaderCell>\n` str += ` <Table.HeaderCell>\nPayload\n</Table.HeaderCell>\n`
str += ` <Table.HeaderCell>\nAction\n</Table.HeaderCell>\n`
str += ` </Table.Row>\n` str += ` </Table.Row>\n`
str += ` </Table.Header>\n` str += ` </Table.Header>\n`
str += ` <Table.Body>\n` str += ` <Table.Body>\n`
@@ -33,29 +34,35 @@ export default function () {
.map((c) => c.text) .map((c) => c.text)
.join(" ") .join(" ")
.split("--") .split("--")
let eventName = `\`${commentContent[0].trim()}\`` const eventName = commentContent[0].trim()
const eventPayload = commentContent[2]?.trim() || ""
let eventNameFormatted = `\`${eventName}\``
const eventDescription = commentContent[1]?.trim() || "" const eventDescription = commentContent[1]?.trim() || ""
const eventPayload = (commentContent[2]?.trim() || "") const eventPayloadFormatted = eventPayload
.replace("```ts\n", "") .replace("```ts\n", "")
.replace("\n```", "") .replace("\n```", "")
const isDeprecated = commentContent.length >= 4 const isDeprecated = commentContent.length >= 4
if (isDeprecated) { if (isDeprecated) {
const deprecatedText = commentContent[4]?.trim() const deprecatedText = commentContent[4]?.trim()
eventName += `\n` eventNameFormatted += `\n`
if (deprecatedText) { if (deprecatedText) {
eventName += `<Tooltip text="${deprecatedText}">` eventNameFormatted += `<Tooltip text="${deprecatedText}">`
} }
eventName += `<Badge variant="orange">Deprecated</Badge>` eventNameFormatted += `<Badge variant="orange">Deprecated</Badge>`
if (deprecatedText) { if (deprecatedText) {
eventName += `</Tooltip>` eventNameFormatted += `</Tooltip>`
} }
} }
str += ` <Table.Row>\n` str += ` <Table.Row>\n`
str += ` <Table.Cell>\n${eventName}\n</Table.Cell>\n` str += ` <Table.Cell>\n${eventNameFormatted}\n</Table.Cell>\n`
str += ` <Table.Cell>\n${eventDescription}\n</Table.Cell>\n` str += ` <Table.Cell>\n${eventDescription}\n</Table.Cell>\n`
str += ` <Table.Cell>\n\`\`\`ts blockStyle="inline"\n${eventPayload}\n\`\`\`\n</Table.Cell>\n` str += ` <Table.Cell>\n\`\`\`ts blockStyle="inline"\n${eventPayloadFormatted}\n\`\`\`\n</Table.Cell>\n`
str += ` <Table.Cell>\n<CopyGeneratedSnippetButton tooltipText="Copy subscriber for event" type="subscriber" options={{
event: "${eventName}",
payload: \`${eventPayload.replaceAll("`", "\\`")}\`
}}/>\n</Table.Cell>\n`
str += ` </Table.Row>\n` str += ` </Table.Row>\n`
}) })
str += ` </Table.Body>\n` str += ` </Table.Body>\n`
+32 -29
View File
@@ -1868,21 +1868,12 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@medusajs/icons@npm:2.6.0": "@medusajs/icons@npm:2.7.1, @medusajs/icons@npm:~2.7.1":
version: 2.6.0 version: 2.7.1
resolution: "@medusajs/icons@npm:2.6.0" resolution: "@medusajs/icons@npm:2.7.1"
peerDependencies: peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
checksum: cb8628018398aba010bb7b1f653cb7dc91d9bd59a40f1e7ee4cdc7732559e75062db790fb609fd3f748a9b80ccc0f421267a414ed9f5a7a4e079ef643e65333e checksum: 6c271671b8ca5d912b14c8f6e91adcd9312e4290b13e9fabe2ad6eb2635051c769fadaa7f2157c90161d8a90f57fd5fb6f8b24b4f86024248414871478f0997c
languageName: node
linkType: hard
"@medusajs/icons@npm:^2.5.1, @medusajs/icons@npm:~2.5.1":
version: 2.5.1
resolution: "@medusajs/icons@npm:2.5.1"
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
checksum: f7fbe17475d8c39bd584103ec662f90aa5c8f07159f5820648eba8a0adf42a0d48057518da5a22503e5d5eaca552cbb7b26792613e2ec84827be1bbbafb64201
languageName: node languageName: node
linkType: hard linkType: hard
@@ -1898,11 +1889,23 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@medusajs/ui@npm:4.0.6": "@medusajs/ui-preset@npm:2.7.1":
version: 4.0.6 version: 2.7.1
resolution: "@medusajs/ui@npm:4.0.6" resolution: "@medusajs/ui-preset@npm:2.7.1"
dependencies: dependencies:
"@medusajs/icons": ^2.5.1 "@tailwindcss/forms": ^0.5.3
tailwindcss-animate: ^1.0.6
peerDependencies:
tailwindcss: ">=3.0.0"
checksum: 6b67c5a614e1a8f985cac256c7a363292ca3326ee02a067ec310fec4b3b24b36726e5bc34427b75efb3a395cba2c4e7ba7bd97acc2fb2bb25004570b979e63c0
languageName: node
linkType: hard
"@medusajs/ui@npm:4.0.9":
version: 4.0.9
resolution: "@medusajs/ui@npm:4.0.9"
dependencies:
"@medusajs/icons": 2.7.1
"@tanstack/react-table": 8.20.5 "@tanstack/react-table": 8.20.5
clsx: ^1.2.1 clsx: ^1.2.1
copy-to-clipboard: ^3.3.3 copy-to-clipboard: ^3.3.3
@@ -1918,7 +1921,7 @@ __metadata:
peerDependencies: peerDependencies:
react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
checksum: ff45b909b3573fd6b443eee0aac47a2f13dbd145ed161e0485f7e22e1e963345e2e38dba236d1b927715feaa79d3dc79671bdeb2075c6f83682dcf8827e6f962 checksum: 17f265fa8075cd26f54cda0ef96ffc9afe853fdf10bd42d82ce830f64bc0b932e93f43a9863606a482875fc1f6214a8acb43fd2a6c4fc8f593fa6a4521fbae76
languageName: node languageName: node
linkType: hard linkType: hard
@@ -6237,8 +6240,8 @@ __metadata:
dependencies: dependencies:
"@mdx-js/loader": ^3.1.0 "@mdx-js/loader": ^3.1.0
"@mdx-js/react": ^3.1.0 "@mdx-js/react": ^3.1.0
"@medusajs/icons": 2.6.0 "@medusajs/icons": 2.7.1
"@medusajs/ui": 4.0.6 "@medusajs/ui": 4.0.9
"@next/bundle-analyzer": 15.3.1 "@next/bundle-analyzer": 15.3.1
"@next/mdx": 15.3.1 "@next/mdx": 15.3.1
"@react-hook/resize-observer": ^2.0.2 "@react-hook/resize-observer": ^2.0.2
@@ -6617,7 +6620,7 @@ __metadata:
dependencies: dependencies:
"@mdx-js/loader": ^3.1.0 "@mdx-js/loader": ^3.1.0
"@mdx-js/react": ^3.1.0 "@mdx-js/react": ^3.1.0
"@medusajs/icons": ~2.5.1 "@medusajs/icons": ~2.7.1
"@next/mdx": 15.3.1 "@next/mdx": 15.3.1
"@types/mdx": ^2.0.13 "@types/mdx": ^2.0.13
"@types/node": ^20 "@types/node": ^20
@@ -7979,8 +7982,8 @@ __metadata:
resolution: "docs-ui@workspace:packages/docs-ui" resolution: "docs-ui@workspace:packages/docs-ui"
dependencies: dependencies:
"@emotion/is-prop-valid": ^1.3.1 "@emotion/is-prop-valid": ^1.3.1
"@medusajs/icons": 2.6.0 "@medusajs/icons": 2.7.1
"@medusajs/ui": 4.0.6 "@medusajs/ui": 4.0.9
"@next/third-parties": 15.3.1 "@next/third-parties": 15.3.1
"@octokit/request": ^8.1.1 "@octokit/request": ^8.1.1
"@react-hook/resize-observer": ^1.2.6 "@react-hook/resize-observer": ^1.2.6
@@ -15011,7 +15014,7 @@ __metadata:
dependencies: dependencies:
"@mdx-js/loader": ^3.1.0 "@mdx-js/loader": ^3.1.0
"@mdx-js/react": ^3.1.0 "@mdx-js/react": ^3.1.0
"@medusajs/icons": ~2.5.1 "@medusajs/icons": ~2.7.1
"@next/bundle-analyzer": ^15.1.1 "@next/bundle-analyzer": ^15.1.1
"@next/mdx": 15.3.1 "@next/mdx": 15.3.1
"@types/mdx": ^2.0.13 "@types/mdx": ^2.0.13
@@ -16737,7 +16740,7 @@ turbo@latest:
version: 0.0.0-use.local version: 0.0.0-use.local
resolution: "types@workspace:packages/types" resolution: "types@workspace:packages/types"
dependencies: dependencies:
"@medusajs/icons": 2.6.0 "@medusajs/icons": 2.7.1
"@types/node": ^20.11.20 "@types/node": ^20.11.20
openapi-types: ^12.1.3 openapi-types: ^12.1.3
rimraf: ^5.0.5 rimraf: ^5.0.5
@@ -16826,9 +16829,9 @@ turbo@latest:
dependencies: dependencies:
"@faker-js/faker": ^8.0.2 "@faker-js/faker": ^8.0.2
"@mdx-js/react": ^3.1.0 "@mdx-js/react": ^3.1.0
"@medusajs/icons": 2.6.0 "@medusajs/icons": 2.7.1
"@medusajs/ui": 4.0.6 "@medusajs/ui": 4.0.9
"@medusajs/ui-preset": 2.6.0 "@medusajs/ui-preset": 2.7.1
"@types/node": 20.4.9 "@types/node": 20.4.9
"@types/react": "npm:types-react@rc" "@types/react": "npm:types-react@rc"
"@types/react-dom": "npm:types-react@rc" "@types/react-dom": "npm:types-react@rc"
@@ -17220,7 +17223,7 @@ turbo@latest:
dependencies: dependencies:
"@mdx-js/loader": ^3.1.0 "@mdx-js/loader": ^3.1.0
"@mdx-js/react": ^3.1.0 "@mdx-js/react": ^3.1.0
"@medusajs/icons": ~2.5.1 "@medusajs/icons": ~2.7.1
"@next/mdx": 15.0.4 "@next/mdx": 15.0.4
"@types/mdx": ^2.0.13 "@types/mdx": ^2.0.13
"@types/node": ^20 "@types/node": ^20