@mercurjs/docs 2.2.0-canary.46 → 2.2.0-canary.47
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/content/references/overview.mdx +10 -0
- package/content/references/panel-extension-api.mdx +337 -0
- package/content/resources/tutorials/add-a-widget.mdx +1 -1
- package/content/resources/tutorials/extend-onboarding.mdx +8 -8
- package/content/resources/tutorials/store-setup-checklist.mdx +214 -0
- package/llms.txt +2 -0
- package/package.json +1 -1
|
@@ -58,6 +58,16 @@ Mercur extends the Medusa server with three route surfaces. Start with the conve
|
|
|
58
58
|
</Card>
|
|
59
59
|
</CardGroup>
|
|
60
60
|
|
|
61
|
+
## Panel extension API
|
|
62
|
+
|
|
63
|
+
The file-based helpers for extending the admin and vendor panels without forking.
|
|
64
|
+
|
|
65
|
+
<CardGroup cols={1}>
|
|
66
|
+
<Card title="Panel Extension API" href="/rc/references/panel-extension-api">
|
|
67
|
+
<code>defineWidgetConfig</code>, <code>defineNavigationConfig</code>, <code>defineCustomFieldsConfig</code>, and <code>createFormHelper</code> — config shapes and the exact props passed to every component you supply.
|
|
68
|
+
</Card>
|
|
69
|
+
</CardGroup>
|
|
70
|
+
|
|
61
71
|
## Workflows and configuration
|
|
62
72
|
|
|
63
73
|
<CardGroup cols={2}>
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Panel Extension API"
|
|
3
|
+
description: "The file-based helpers for extending the admin and vendor panels — defineWidgetConfig, defineNavigationConfig, defineCustomFieldsConfig, createFormHelper — and the exact props passed to every component you supply."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
This is the technical contract for the panel extension API.
|
|
7
|
+
Where the [Extend forms and tables](/rc/resources/tutorials/extend-forms-and-tables),
|
|
8
|
+
[Add a widget](/rc/resources/tutorials/add-a-widget), and
|
|
9
|
+
[Customize navigation](/rc/resources/tutorials/customize-navigation) tutorials
|
|
10
|
+
walk through *building* an extension, this page documents the exact config shape
|
|
11
|
+
of each helper and — most importantly — **the props each `component` you pass in
|
|
12
|
+
receives at render time**.
|
|
13
|
+
|
|
14
|
+
<Info>
|
|
15
|
+
**Separate apps, no `surface` field.** Admin (`@mercurjs/admin`, port 7000) and
|
|
16
|
+
vendor (`@mercurjs/vendor`, port 7001) are separate Vite apps. A file dropped
|
|
17
|
+
under a panel's `src/` targets *that* panel — the folder you author in *is* the
|
|
18
|
+
surface. The helpers are the same in both; import them from
|
|
19
|
+
`@mercurjs/dashboard-sdk` (config helpers) and `@mercurjs/dashboard-shared`
|
|
20
|
+
(`createFormHelper`).
|
|
21
|
+
</Info>
|
|
22
|
+
|
|
23
|
+
## Typed targets (register once)
|
|
24
|
+
|
|
25
|
+
Zone ids, nav item ids, models, and built-in field ids are typed per panel from a
|
|
26
|
+
generated `extension-targets.d.ts`. Reference it once per host app so every
|
|
27
|
+
extension file type-checks with no per-file import:
|
|
28
|
+
|
|
29
|
+
```typescript apps/vendor/src/extension-targets.d.ts
|
|
30
|
+
/// <reference types="@mercurjs/vendor/extension-targets" />
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```typescript apps/admin-test/src/extension-targets.d.ts
|
|
34
|
+
/// <reference types="@mercurjs/admin/extension-targets" />
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
A wrong `zone`, `model`, or nav `id` fails `tsc` (`bun run lint`) rather than
|
|
38
|
+
silently no-op'ing at runtime.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## `defineWidgetConfig`
|
|
43
|
+
|
|
44
|
+
A widget is a React component attached to a named zone on a built-in page. The
|
|
45
|
+
placement (`before | after | replace`) is the last segment of the zone id.
|
|
46
|
+
|
|
47
|
+
```tsx apps/vendor/src/widgets/product-list-banner.tsx
|
|
48
|
+
import { defineWidgetConfig } from "@mercurjs/dashboard-sdk"
|
|
49
|
+
|
|
50
|
+
export const config = defineWidgetConfig({
|
|
51
|
+
zone: "product.list.before", // WidgetZoneId — <domain>.<view>[.<slot>].<before|after|replace>
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
export default ProductListBanner
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Config
|
|
58
|
+
|
|
59
|
+
| Field | Type | Description |
|
|
60
|
+
| ------ | ------------------------------- | --------------------------------------------------------------------------- |
|
|
61
|
+
| `zone` | `WidgetZoneId \| WidgetZoneId[]` | Target zone(s). Placement is the suffix. Multiple widgets stack in order. |
|
|
62
|
+
| `id` | `string` (optional) | Stable id; derived from the file path at build time when omitted. |
|
|
63
|
+
|
|
64
|
+
### Component props
|
|
65
|
+
|
|
66
|
+
The widget component receives a single prop:
|
|
67
|
+
|
|
68
|
+
| Prop | Type | Description |
|
|
69
|
+
| ------ | --------- | ----------------------------------------------------------------------------------------- |
|
|
70
|
+
| `data` | `unknown` | The zone's contextual entity — e.g. the loaded product on a `product.detail.*` zone. Undefined on list/public zones that have no single entity. |
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
const ProductDetailNote = ({ data }: { data?: HttpTypes.AdminProduct }) => (
|
|
74
|
+
<Container>{data?.title}</Container>
|
|
75
|
+
)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
<Note>
|
|
79
|
+
The public `login.logo` / `login.before` / `login.after` zones render before
|
|
80
|
+
authentication and receive no `data`.
|
|
81
|
+
</Note>
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## `defineNavigationConfig`
|
|
86
|
+
|
|
87
|
+
Reorder, hide, relabel, or re-parent **built-in** sidebar items. Single
|
|
88
|
+
host-owned file — blocks cannot contribute nav overrides.
|
|
89
|
+
|
|
90
|
+
```ts apps/vendor/src/_navigation.ts
|
|
91
|
+
import { defineNavigationConfig } from "@mercurjs/dashboard-sdk"
|
|
92
|
+
|
|
93
|
+
export default defineNavigationConfig({
|
|
94
|
+
items: [
|
|
95
|
+
{ id: "orders", rank: 0 },
|
|
96
|
+
{ id: "price-lists", hidden: true },
|
|
97
|
+
{ id: "payouts", label: "settlements" },
|
|
98
|
+
{ id: "categories", nested: null, rank: 1 },
|
|
99
|
+
{ id: "campaigns", nested: "orders" },
|
|
100
|
+
],
|
|
101
|
+
})
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### `NavItemOverride`
|
|
105
|
+
|
|
106
|
+
| Field | Type | Description |
|
|
107
|
+
| -------- | -------------------------- | ------------------------------------------------------------------------------ |
|
|
108
|
+
| `id` | `NavItemId` | Built-in item to override (top-level or nested). Typed against the panel registry. |
|
|
109
|
+
| `rank` | `number` (optional) | Order within the item's parent (or among top-level items). |
|
|
110
|
+
| `hidden` | `boolean` (optional) | Remove from the sidebar (route may still be directly reachable). |
|
|
111
|
+
| `label` | `string` (optional) | i18n key or literal replacing the item's label. |
|
|
112
|
+
| `icon` | `ComponentType` (optional) | Icon component replacing the item's icon (from `@medusajs/icons`). |
|
|
113
|
+
| `nested` | `NavParentId \| null` (optional) | Re-parent under a built-in parent id; `null` promotes a nested item to top level. |
|
|
114
|
+
|
|
115
|
+
There is no `component` prop here — navigation overrides reshape existing items
|
|
116
|
+
only.
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## `defineCustomFieldsConfig`
|
|
121
|
+
|
|
122
|
+
The model-scoped surface. One file per model adds form fields, section displays,
|
|
123
|
+
section actions, and list-table columns. Import
|
|
124
|
+
`createFormHelper` from `@mercurjs/dashboard-shared` to turn a Zod schema into an
|
|
125
|
+
input type + validation.
|
|
126
|
+
|
|
127
|
+
```tsx apps/vendor/src/custom-fields/product.tsx
|
|
128
|
+
import { defineCustomFieldsConfig } from "@mercurjs/dashboard-sdk"
|
|
129
|
+
import { createFormHelper } from "@mercurjs/dashboard-shared"
|
|
130
|
+
|
|
131
|
+
type ProductWithMeta = { metadata?: Record<string, unknown> }
|
|
132
|
+
const form = createFormHelper<ProductWithMeta>()
|
|
133
|
+
|
|
134
|
+
export default defineCustomFieldsConfig({
|
|
135
|
+
model: "product",
|
|
136
|
+
link: "brand", // string | string[] — module link(s) fetched alongside the entity
|
|
137
|
+
forms: [/* ... */],
|
|
138
|
+
displays: [/* ... */],
|
|
139
|
+
list: {/* ... */},
|
|
140
|
+
})
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Top-level config
|
|
144
|
+
|
|
145
|
+
| Field | Type | Description |
|
|
146
|
+
| ---------- | ------------------------ | ------------------------------------------------------------------------ |
|
|
147
|
+
| `model` | `CustomFieldModel` | Target model (start with `"product"`). Typed against `CustomFieldsRegistry`. |
|
|
148
|
+
| `link` | `string \| string[]` | Module link(s) fetched alongside the entity; their data is available to columns and displays via the `+link.*` fetch. |
|
|
149
|
+
| `forms` | `CustomFormEntry[]` | Fields injected into built-in create/edit/onboarding forms. |
|
|
150
|
+
| `displays` | `CustomDisplayEntry[]` | Field replace/remove/add + `ActionMenu` actions on detail sections. |
|
|
151
|
+
| `list` | `CustomListExtension` | Columns, bulk actions, filters, view defaults on the model's list table. |
|
|
152
|
+
|
|
153
|
+
### `forms[]` — form fields
|
|
154
|
+
|
|
155
|
+
```tsx
|
|
156
|
+
forms: [
|
|
157
|
+
{
|
|
158
|
+
zone: "edit", // CustomFormZone — "create" | "edit" | "organize" | "attributes" | "onboarding"
|
|
159
|
+
tab: "general", // TabbedForm tab id, or wizard step id for zone: "onboarding"
|
|
160
|
+
fields: {
|
|
161
|
+
erp_id: form.define({
|
|
162
|
+
validation: form.string().nullish(), // Zod → input type + validation
|
|
163
|
+
defaultValue: (data) => (data?.metadata?.erp_id as string) ?? "",
|
|
164
|
+
label: "ERP ID",
|
|
165
|
+
description: "External system identifier",
|
|
166
|
+
placeholder: "ERP-000",
|
|
167
|
+
component: MyErpInput, // optional custom render
|
|
168
|
+
}),
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
]
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Each field is a `CustomFormField`:
|
|
175
|
+
|
|
176
|
+
| Field | Type | Description |
|
|
177
|
+
| -------------- | ---------------------------------------- | ----------------------------------------------------------------- |
|
|
178
|
+
| `validation` | Zod schema | Drives both the default input type and validation. |
|
|
179
|
+
| `defaultValue` | `unknown \| ((data) => unknown)` | Static value or a resolver from the loaded entity. |
|
|
180
|
+
| `label` | `string` (optional) | Field label. |
|
|
181
|
+
| `description` | `string` (optional) | Help text below the input. |
|
|
182
|
+
| `placeholder` | `string` (optional) | Input placeholder. |
|
|
183
|
+
| `component` | `ComponentType` (optional) | Custom render; falls back to a default input for the Zod type. |
|
|
184
|
+
|
|
185
|
+
<Warning>
|
|
186
|
+
**Form-field `component` receives no props.** It is rendered as `<Component />`
|
|
187
|
+
inside the field's `additional_data.<field>` React Hook Form context. Read and
|
|
188
|
+
write the value with RHF's `useFormContext()` / `useController()`, and render
|
|
189
|
+
through the mandated `Form.Field → Form.Item` chain — do not use a raw
|
|
190
|
+
`Controller`. Values live in form state under `additional_data`; in the MVP,
|
|
191
|
+
`product` custom fields persist onto the product's `metadata`.
|
|
192
|
+
</Warning>
|
|
193
|
+
|
|
194
|
+
### `displays[]` — detail sections
|
|
195
|
+
|
|
196
|
+
```tsx
|
|
197
|
+
displays: [
|
|
198
|
+
{
|
|
199
|
+
zone: "general", // CustomFieldsRegistry displayZones — an existing detail section id
|
|
200
|
+
fields: [
|
|
201
|
+
{ id: "erp_id", component: ErpRow }, // ADD (unknown id → new read-only row)
|
|
202
|
+
{ id: "status", component: BrandedStatusBadge }, // REPLACE a built-in field's render
|
|
203
|
+
{ id: "created_by", component: null }, // REMOVE a built-in field
|
|
204
|
+
],
|
|
205
|
+
actions: [
|
|
206
|
+
{ rank: 0, component: SyncErpAction }, // add to the section's ActionMenu
|
|
207
|
+
],
|
|
208
|
+
},
|
|
209
|
+
]
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
`fields[]` is `CustomDisplayField`:
|
|
213
|
+
|
|
214
|
+
| Field | Type | Description |
|
|
215
|
+
| ----------- | --------------------------------------- | -------------------------------------------------------------------------------------- |
|
|
216
|
+
| `id` | `displayFieldIds \| (string & {})` | Built-in field id (autocompletes) → replace/remove; any other string → add a new row. |
|
|
217
|
+
| `component` | `ComponentType<{ data? }> \| null` | Render component, or `null` to remove a built-in field. |
|
|
218
|
+
|
|
219
|
+
**Display field `component` props:**
|
|
220
|
+
|
|
221
|
+
| Prop | Type | Description |
|
|
222
|
+
| ------ | --------- | ---------------------------------- |
|
|
223
|
+
| `data` | `unknown` | The loaded detail entity, **including any `link`ed module data**. If the config declares `link: "brand"`, read it off `data.brand` here. |
|
|
224
|
+
|
|
225
|
+
```tsx
|
|
226
|
+
// with `link: "brand"` on the config, the linked module data rides on `data`
|
|
227
|
+
const BrandRow = ({ data }: { data?: { brand?: { name: string } } }) => (
|
|
228
|
+
<Text>{data?.brand?.name}</Text>
|
|
229
|
+
)
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
`actions[]` is `SectionAction` (the same shape as list `bulkActions`):
|
|
233
|
+
|
|
234
|
+
| Field | Type | Description |
|
|
235
|
+
| ----------- | ------------------------------- | ----------------------------------------------------------------- |
|
|
236
|
+
| `rank` | `number` (optional) | Position within the section's `ActionMenu`. |
|
|
237
|
+
| `component` | `ComponentType<{ data? }>` | Owns its own label, icon, group placement, and `onClick`. |
|
|
238
|
+
|
|
239
|
+
**Section-action `component` props:**
|
|
240
|
+
|
|
241
|
+
| Prop | Type | Description |
|
|
242
|
+
| ------ | --------- | ----------------------------------------------------------------------- |
|
|
243
|
+
| `data` | `unknown` | The loaded detail entity, **including any `link`ed module data** (e.g. `data.brand`). |
|
|
244
|
+
|
|
245
|
+
### `list` — list table
|
|
246
|
+
|
|
247
|
+
```tsx
|
|
248
|
+
list: {
|
|
249
|
+
columns: [
|
|
250
|
+
{ id: "title", component: ({ value }) => <strong>{value}</strong> }, // override a cell
|
|
251
|
+
{ id: "brand_name", header: "Brand", component: ({ row }) => row.brand?.name }, // add (from link)
|
|
252
|
+
],
|
|
253
|
+
bulkActions: [{ rank: 0, component: ArchiveBulkAction }],
|
|
254
|
+
filters: [/* add / remove list filters */],
|
|
255
|
+
viewDefaults: {
|
|
256
|
+
columnVisibility: { created_at: false }, // hide a column
|
|
257
|
+
columnOrder: ["title", "sku", "erp_id"],
|
|
258
|
+
},
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
`columns[]` is `CustomColumn`:
|
|
263
|
+
|
|
264
|
+
| Field | Type | Description |
|
|
265
|
+
| ----------- | ------------------------------------------ | -------------------------------------------------------- |
|
|
266
|
+
| `id` | `string` | Column id — matches a built-in column to override, or adds a new one. |
|
|
267
|
+
| `header` | `string` (optional) | Column header text (for added columns). |
|
|
268
|
+
| `component` | `ComponentType<{ row?, value? }>` (optional) | Cell renderer. |
|
|
269
|
+
|
|
270
|
+
**Column `component` props:**
|
|
271
|
+
|
|
272
|
+
| Prop | Type | Description |
|
|
273
|
+
| ------- | --------- | -------------------------------------------------------- |
|
|
274
|
+
| `row` | `unknown` | The full row entity (including `link`ed module data). |
|
|
275
|
+
| `value` | `unknown` | The cell value for this column id, when derivable. |
|
|
276
|
+
|
|
277
|
+
`bulkActions[]` is `SectionAction` (`{ rank?, component }`, component props `{ data? }`).
|
|
278
|
+
|
|
279
|
+
<Note>
|
|
280
|
+
**Bulk-action rendering is deferred in the MVP.** `bulkActions` are accepted and
|
|
281
|
+
surfaced by the config but not yet mounted into the list toolbar.
|
|
282
|
+
</Note>
|
|
283
|
+
|
|
284
|
+
<Warning>
|
|
285
|
+
**Vendor product list is field-constrained.** The vendor `product` list must use
|
|
286
|
+
the curated fields from `useProductTableQuery` — the SDK merges `link` fetches
|
|
287
|
+
with the `+`/`-` convention, never bare fields, or the list 500s. You never
|
|
288
|
+
hand-write the field list; the `link` declaration drives it.
|
|
289
|
+
</Warning>
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## `createFormHelper`
|
|
294
|
+
|
|
295
|
+
`createFormHelper<T>()` returns Medusa's zod surface for describing field
|
|
296
|
+
validation and value types:
|
|
297
|
+
|
|
298
|
+
```ts
|
|
299
|
+
const form = createFormHelper<ProductWithMeta>()
|
|
300
|
+
|
|
301
|
+
form.define({ validation, defaultValue?, label?, description?, placeholder?, component? })
|
|
302
|
+
form.string() / form.number() / form.boolean() / form.date()
|
|
303
|
+
form.array() / form.object() / form.null() / form.nullable() / form.coerce
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
The generated registry types the **target** (which zone/tab/field ids exist); the
|
|
307
|
+
Zod `validation` types the **value**. The two compose — codegen types the address,
|
|
308
|
+
Zod types the payload.
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
## Persistence
|
|
313
|
+
|
|
314
|
+
<Warning>
|
|
315
|
+
**The MVP is a UI surface only.** Custom fields render, validate, and display
|
|
316
|
+
through the built-in forms/sections/tables — there is no generic core-side write
|
|
317
|
+
path. For `product`, values are submitted under `additional_data` and persisted
|
|
318
|
+
onto `metadata`. To store data for other models, wire your own route/workflow or
|
|
319
|
+
use the backend [Custom Fields module](/rc/resources/customization/custom-fields).
|
|
320
|
+
</Warning>
|
|
321
|
+
|
|
322
|
+
## Related
|
|
323
|
+
|
|
324
|
+
<CardGroup cols={2}>
|
|
325
|
+
<Card title="Extend forms and tables" href="/rc/resources/tutorials/extend-forms-and-tables">
|
|
326
|
+
Step-by-step build with `defineCustomFieldsConfig`.
|
|
327
|
+
</Card>
|
|
328
|
+
<Card title="Add a widget" href="/rc/resources/tutorials/add-a-widget">
|
|
329
|
+
Inject a component into a zone.
|
|
330
|
+
</Card>
|
|
331
|
+
<Card title="Customize navigation" href="/rc/resources/tutorials/customize-navigation">
|
|
332
|
+
Reorder and hide sidebar items.
|
|
333
|
+
</Card>
|
|
334
|
+
<Card title="Custom Fields module" href="/rc/references/modules/custom-fields">
|
|
335
|
+
The backend storage layer.
|
|
336
|
+
</Card>
|
|
337
|
+
</CardGroup>
|
|
@@ -70,7 +70,7 @@ Zones mounted today in the **vendor portal**:
|
|
|
70
70
|
| Zone | Where it renders |
|
|
71
71
|
|------|------------------|
|
|
72
72
|
| `product.list.before` / `.after` | Vendor product list page |
|
|
73
|
-
| `
|
|
73
|
+
| `seller.setup.before` / `.after` | The store-setup / onboarding surface (dashboard home + store settings), passed the `seller` as `data` |
|
|
74
74
|
| `login.logo.*` | The logo slot on the public login screen |
|
|
75
75
|
| `login.before.*` / `login.after.*` | Around the login form (rendered before authentication) |
|
|
76
76
|
|
|
@@ -3,10 +3,10 @@ title: "Extend the onboarding flow"
|
|
|
3
3
|
description: "Add a field to the vendor store-setup surface, submit it through additional_data, and persist it durably from a workflow hook — end to end, without forking."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
The vendor **store-setup / onboarding** surface is a widget zone (`
|
|
6
|
+
The vendor **store-setup / onboarding** surface is a widget zone (`seller.setup`) that renders the full `seller` object as its `data`. That makes onboarding a full extension seam: drop a widget to add UI, carry the new value to the API on the built-in seller routes through `additional_data`, and persist it from a workflow hook — the same three layers you'd wire in plain Medusa, kept intact by Mercur.
|
|
7
7
|
|
|
8
8
|
<Info>
|
|
9
|
-
**Three layers, one flow.** The panel (a `
|
|
9
|
+
**Three layers, one flow.** The panel (a `seller.setup` widget) *renders and collects*. The vendor seller route *carries* the value through `additional_data` — no core schema change. A `sellersUpdated` workflow hook *persists* it. Each layer is additive: nothing built-in is replaced.
|
|
10
10
|
</Info>
|
|
11
11
|
|
|
12
12
|
## What you'll build
|
|
@@ -21,13 +21,13 @@ Widget zones are typed ids the vendor panel generates from its own pages and shi
|
|
|
21
21
|
/// <reference types="@mercurjs/vendor/extension-targets" />
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
With it present, `
|
|
24
|
+
With it present, `seller.setup` autocompletes and an invalid zone fails `tsc` instead of silently doing nothing.
|
|
25
25
|
|
|
26
26
|
## 1 — Render on the onboarding surface
|
|
27
27
|
|
|
28
28
|
<Steps>
|
|
29
29
|
<Step title="Add the store-setup widget">
|
|
30
|
-
Drop a file under `src/widgets/`. Export the component as the **default** and a `config` with `zone: "
|
|
30
|
+
Drop a file under `src/widgets/`. Export the component as the **default** and a `config` with `zone: "seller.setup.before"`. The zone hands your component the `seller` as `data`:
|
|
31
31
|
|
|
32
32
|
```tsx apps/vendor/src/widgets/tax-id-setup.tsx
|
|
33
33
|
import "@mercurjs/vendor/extension-targets"
|
|
@@ -40,7 +40,7 @@ With it present, `store.setup` autocompletes and an invalid zone fails `tsc` ins
|
|
|
40
40
|
import { client } from "../lib/client"
|
|
41
41
|
|
|
42
42
|
export const config = defineWidgetConfig({
|
|
43
|
-
zone: "
|
|
43
|
+
zone: "seller.setup.before",
|
|
44
44
|
})
|
|
45
45
|
|
|
46
46
|
const TaxIdSetup = ({ data: seller }: { data?: SellerDTO }) => {
|
|
@@ -94,14 +94,14 @@ With it present, `store.setup` autocompletes and an invalid zone fails `tsc` ins
|
|
|
94
94
|
```
|
|
95
95
|
</Step>
|
|
96
96
|
<Step title="Understand where it renders">
|
|
97
|
-
`
|
|
97
|
+
`seller.setup` is hosted in two places, both passing the same `seller` as `data`:
|
|
98
98
|
|
|
99
99
|
| Host | When it shows |
|
|
100
100
|
|------|---------------|
|
|
101
101
|
| The vendor shell (above the page outlet) | On top-level routes — the dashboard "home" onboarding banner |
|
|
102
102
|
| The store settings detail page | Always, above the store status banner |
|
|
103
103
|
|
|
104
|
-
A single widget file covers both. Multiple `
|
|
104
|
+
A single widget file covers both. Multiple `seller.setup.before` / `.after` widgets stack in registration order.
|
|
105
105
|
</Step>
|
|
106
106
|
</Steps>
|
|
107
107
|
|
|
@@ -203,7 +203,7 @@ That is the whole "wiring" step: your `{ tax_id }` payload arrives in the workfl
|
|
|
203
203
|
## How the layers connect
|
|
204
204
|
|
|
205
205
|
```
|
|
206
|
-
|
|
206
|
+
seller.setup widget → client.vendor.sellers.$id.mutate({ additional_data })
|
|
207
207
|
│
|
|
208
208
|
▼
|
|
209
209
|
POST /vendor/sellers/:id → updateSellersWorkflow({ update, additional_data })
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Build a store-setup checklist"
|
|
3
|
+
description: "Render an onboarding progress checklist on the vendor store-setup surface with a seller.setup widget — the seller is handed to you as data, no fetching required."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
The vendor **store-setup / onboarding** surface is a widget zone (`seller.setup`)
|
|
7
|
+
that renders the full `seller` object as its `data`. That makes it the perfect
|
|
8
|
+
seam for an onboarding **checklist**: derive completion from the seller you're
|
|
9
|
+
handed, show what's left, and link each step to the page that completes it — all
|
|
10
|
+
from a single widget file, with nothing fetched and nothing forked.
|
|
11
|
+
|
|
12
|
+
<Info>
|
|
13
|
+
**This is the pattern the built-in store-setup card uses.** The shipped
|
|
14
|
+
"Complete store profile" card is itself a `seller.setup` widget that reads the
|
|
15
|
+
`seller` and renders a progress list. This tutorial rebuilds it so you can
|
|
16
|
+
shape your own onboarding steps.
|
|
17
|
+
</Info>
|
|
18
|
+
|
|
19
|
+
## What you'll build
|
|
20
|
+
|
|
21
|
+
A collapsible progress card on the store-setup surface that checks four parts of
|
|
22
|
+
the seller profile (details, address, company, payment), draws a progress bar,
|
|
23
|
+
and routes the vendor to the settings page for each incomplete step. When every
|
|
24
|
+
step is done, the card removes itself.
|
|
25
|
+
|
|
26
|
+
## Register the typed targets (once)
|
|
27
|
+
|
|
28
|
+
Widget zones are typed ids the vendor panel generates from its own pages and
|
|
29
|
+
ships as `@mercurjs/vendor/extension-targets`. Reference them once so `seller.setup`
|
|
30
|
+
autocompletes and a wrong zone fails `tsc`:
|
|
31
|
+
|
|
32
|
+
```typescript apps/vendor/src/extension-targets.d.ts
|
|
33
|
+
/// <reference types="@mercurjs/vendor/extension-targets" />
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Build the checklist widget
|
|
37
|
+
|
|
38
|
+
<Steps>
|
|
39
|
+
<Step title="Derive the steps from the seller">
|
|
40
|
+
The `data` prop is the full `SellerDTO`. Compute each step's `completed` flag
|
|
41
|
+
from it — no request needed. Give every step a `path` to the settings page
|
|
42
|
+
that finishes it:
|
|
43
|
+
|
|
44
|
+
```tsx apps/vendor/src/widgets/store-setup-checklist.tsx
|
|
45
|
+
import "@mercurjs/vendor/extension-targets"
|
|
46
|
+
import { SellerDTO } from "@mercurjs/types"
|
|
47
|
+
|
|
48
|
+
type ProfileStep = {
|
|
49
|
+
key: string
|
|
50
|
+
label: string
|
|
51
|
+
completed: boolean
|
|
52
|
+
path: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getProfileSteps(seller: SellerDTO): ProfileStep[] {
|
|
56
|
+
const hasStoreDetails = !!(seller.name && seller.email && seller.description)
|
|
57
|
+
const hasAddress = !!(
|
|
58
|
+
seller.address?.address_1 &&
|
|
59
|
+
seller.address?.city &&
|
|
60
|
+
seller.address?.country_code
|
|
61
|
+
)
|
|
62
|
+
const hasCompanyDetails = !!seller.professional_details?.corporate_name
|
|
63
|
+
const hasPaymentDetails = !!(
|
|
64
|
+
seller.payment_details?.holder_name &&
|
|
65
|
+
seller.payment_details?.country_code
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
return [
|
|
69
|
+
{ key: "store_details", label: "Add store details", completed: hasStoreDetails, path: "/settings/store/edit" },
|
|
70
|
+
{ key: "address", label: "Add address", completed: hasAddress, path: "/settings/store/address" },
|
|
71
|
+
{ key: "company_details", label: "Add company details", completed: hasCompanyDetails, path: "/settings/store/professional-details" },
|
|
72
|
+
{ key: "payment_details", label: "Add payment details", completed: hasPaymentDetails, path: "/settings/store/payment-details" },
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
</Step>
|
|
78
|
+
<Step title="Render the collapsible progress card">
|
|
79
|
+
Use `@medusajs/ui` primitives and `@medusajs/icons` only. Draw a progress bar
|
|
80
|
+
from the completed ratio, list each step with a status icon, and navigate on
|
|
81
|
+
click. Return `null` when everything is done so the card disappears:
|
|
82
|
+
|
|
83
|
+
```tsx apps/vendor/src/widgets/store-setup-checklist.tsx
|
|
84
|
+
import { useMemo, useState } from "react"
|
|
85
|
+
import { useNavigate } from "react-router-dom"
|
|
86
|
+
import { Container, Text, clx } from "@medusajs/ui"
|
|
87
|
+
import { CheckCircleSolid, TriangleDownMini, CircleDottedLine } from "@medusajs/icons"
|
|
88
|
+
import { Collapsible as RadixCollapsible } from "radix-ui"
|
|
89
|
+
|
|
90
|
+
const StoreSetupChecklist = ({ data: seller }: { data?: SellerDTO }) => {
|
|
91
|
+
const navigate = useNavigate()
|
|
92
|
+
const [open, setOpen] = useState(true)
|
|
93
|
+
|
|
94
|
+
const steps = useMemo(
|
|
95
|
+
() => (seller ? getProfileSteps(seller) : []),
|
|
96
|
+
[seller]
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
if (!seller) {
|
|
100
|
+
return null
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const completedCount = steps.filter((s) => s.completed).length
|
|
104
|
+
const totalCount = steps.length
|
|
105
|
+
const progressPercent = (completedCount / totalCount) * 100
|
|
106
|
+
|
|
107
|
+
if (completedCount === totalCount) {
|
|
108
|
+
return null
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<RadixCollapsible.Root open={open} onOpenChange={setOpen}>
|
|
113
|
+
<Container className="overflow-hidden p-0">
|
|
114
|
+
<div
|
|
115
|
+
className="h-1 bg-ui-tag-green-icon transition-all duration-500"
|
|
116
|
+
style={{ width: `${progressPercent}%` }}
|
|
117
|
+
/>
|
|
118
|
+
<div className="p-6">
|
|
119
|
+
<RadixCollapsible.Trigger asChild>
|
|
120
|
+
<button className="flex w-full items-center justify-between">
|
|
121
|
+
<Text size="large" weight="plus" leading="compact">
|
|
122
|
+
Complete store profile
|
|
123
|
+
</Text>
|
|
124
|
+
<TriangleDownMini
|
|
125
|
+
className={clx(
|
|
126
|
+
"text-ui-fg-muted transition-transform duration-200",
|
|
127
|
+
!open && "-rotate-90"
|
|
128
|
+
)}
|
|
129
|
+
/>
|
|
130
|
+
</button>
|
|
131
|
+
</RadixCollapsible.Trigger>
|
|
132
|
+
|
|
133
|
+
<RadixCollapsible.Content>
|
|
134
|
+
<div className="mt-4 flex flex-col gap-y-3">
|
|
135
|
+
{steps.map((step) => (
|
|
136
|
+
<button
|
|
137
|
+
key={step.key}
|
|
138
|
+
className="flex items-center gap-x-3 text-left"
|
|
139
|
+
onClick={() => !step.completed && navigate(step.path)}
|
|
140
|
+
disabled={step.completed}
|
|
141
|
+
>
|
|
142
|
+
{step.completed ? (
|
|
143
|
+
<CheckCircleSolid className="text-ui-tag-green-icon shrink-0" />
|
|
144
|
+
) : (
|
|
145
|
+
<CircleDottedLine className="text-ui-fg-muted shrink-0" />
|
|
146
|
+
)}
|
|
147
|
+
<Text size="small" leading="compact" className="text-ui-fg-base">
|
|
148
|
+
{step.label}
|
|
149
|
+
</Text>
|
|
150
|
+
</button>
|
|
151
|
+
))}
|
|
152
|
+
</div>
|
|
153
|
+
</RadixCollapsible.Content>
|
|
154
|
+
</div>
|
|
155
|
+
</Container>
|
|
156
|
+
</RadixCollapsible.Root>
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
</Step>
|
|
162
|
+
<Step title="Attach it to the store-setup zone">
|
|
163
|
+
Export the component as the **default** and a `config` with a `seller.setup`
|
|
164
|
+
zone. `before` renders it above the built-in card; use `after` to place it
|
|
165
|
+
below:
|
|
166
|
+
|
|
167
|
+
```tsx apps/vendor/src/widgets/store-setup-checklist.tsx
|
|
168
|
+
import { defineWidgetConfig } from "@mercurjs/dashboard-sdk"
|
|
169
|
+
|
|
170
|
+
export const config = defineWidgetConfig({
|
|
171
|
+
zone: "seller.setup.before",
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
export default StoreSetupChecklist
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
</Step>
|
|
178
|
+
</Steps>
|
|
179
|
+
|
|
180
|
+
## Where it renders
|
|
181
|
+
|
|
182
|
+
`seller.setup` is hosted in two places, both passing the same `seller` as `data`:
|
|
183
|
+
|
|
184
|
+
| Host | When it shows |
|
|
185
|
+
| ---------------------------------------- | ------------------------------------------------------------ |
|
|
186
|
+
| The vendor shell (above the page outlet) | On top-level routes — the dashboard "home" onboarding banner |
|
|
187
|
+
| The store settings detail page | Always, above the store status banner |
|
|
188
|
+
|
|
189
|
+
A single widget file covers both. Multiple `seller.setup.before` / `.after`
|
|
190
|
+
widgets stack in registration order, so your checklist and the built-in card can
|
|
191
|
+
coexist — or hide the built-in one by rendering your own and letting theirs
|
|
192
|
+
complete.
|
|
193
|
+
|
|
194
|
+
<Note>
|
|
195
|
+
**No data fetching.** Because the zone hands you the resolved `seller`, the
|
|
196
|
+
widget is pure render — you never call the SDK. To *collect and persist* a new
|
|
197
|
+
onboarding value (e.g. a Tax ID) rather than just link to existing pages, see
|
|
198
|
+
[Extend the onboarding flow](/rc/resources/tutorials/extend-onboarding), which
|
|
199
|
+
carries a value through `additional_data` to a workflow hook.
|
|
200
|
+
</Note>
|
|
201
|
+
|
|
202
|
+
## Next steps
|
|
203
|
+
|
|
204
|
+
<CardGroup cols={2}>
|
|
205
|
+
<Card
|
|
206
|
+
title="Extend the onboarding flow"
|
|
207
|
+
href="/rc/resources/tutorials/extend-onboarding"
|
|
208
|
+
>
|
|
209
|
+
Collect a new value and persist it through a workflow hook.
|
|
210
|
+
</Card>
|
|
211
|
+
<Card title="Add a widget" href="/rc/resources/tutorials/add-a-widget">
|
|
212
|
+
The full widget model and the published zone registry.
|
|
213
|
+
</Card>
|
|
214
|
+
</CardGroup>
|
package/llms.txt
CHANGED
|
@@ -50,6 +50,7 @@ package (`node_modules/@mercurjs/docs/`).
|
|
|
50
50
|
- [Import and export products via CSV](content/resources/tutorials/import-export-products.mdx) — Install the product-import-export block and give vendors CSV import and export drawers on their product list.
|
|
51
51
|
- [Work with master products and offers](content/resources/tutorials/master-products-and-offers.mdx) — Two sellers compete on one catalog entry: publish a master product, create competing offers, and read per-offer prices from the Store API.
|
|
52
52
|
- [Set up seller payouts](content/resources/tutorials/seller-payouts-stripe.mdx) — Take a seller from zero to paid: Stripe Connect onboarding, an order through fulfillment, capture, and the automated payout.
|
|
53
|
+
- [Build a store-setup checklist](content/resources/tutorials/store-setup-checklist.mdx) — Render an onboarding progress checklist on the vendor store-setup surface with a seller.setup widget — the seller is handed to you as data, no fetching required.
|
|
53
54
|
|
|
54
55
|
## Tools — CLI, API client, dashboard SDK
|
|
55
56
|
|
|
@@ -215,6 +216,7 @@ package (`node_modules/@mercurjs/docs/`).
|
|
|
215
216
|
- [Search module](content/references/modules/search.mdx) — The provider-agnostic search module, its document shapes, and the provider contract.
|
|
216
217
|
- [Seller module](content/references/modules/seller.mdx) — Data models, links, and service methods for the Seller module.
|
|
217
218
|
- [References](content/references/overview.mdx) — Technical references for Mercur's modules, HTTP APIs, workflows, and configuration.
|
|
219
|
+
- [Panel Extension API](content/references/panel-extension-api.mdx) — The file-based helpers for extending the admin and vendor panels — defineWidgetConfig, defineNavigationConfig, defineCustomFieldsConfig, createFormHelper — and the exact props passed to every component you supply.
|
|
218
220
|
- [addSellerShippingMethodToCartWorkflow](content/references/workflows/cart/add-seller-shipping-method-to-cart.mdx) — Add shipping methods to a cart, replacing only the same seller's existing methods.
|
|
219
221
|
- [completeCartWithSplitOrdersWorkflow](content/references/workflows/cart/complete-cart-with-split-orders.mdx) — Complete a multi-seller cart by splitting it into per-seller orders under an order group.
|
|
220
222
|
- [listSellerShippingOptionsForCartWorkflow](content/references/workflows/cart/list-seller-shipping-options-for-cart.mdx) — List available shipping options for a cart, grouped by seller.
|