@mercurjs/docs 2.2.0-rc.0 → 2.2.0-rc.2
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/learn/concepts.mdx +1 -1
- package/content/references/overview.mdx +10 -0
- package/content/references/panel-extension-api.mdx +337 -0
- package/content/references/workflows/cart/add-seller-shipping-method-to-cart.mdx +5 -1
- package/content/references/workflows/commission/batch-commission-rules.mdx +6 -2
- package/content/references/workflows/offer/batch-offer-inventory-items.mdx +6 -2
- package/content/references/workflows/offer/create-offers.mdx +3 -1
- package/content/references/workflows/offer/update-offers.mdx +3 -1
- package/content/references/workflows/order-group/get-order-groups-list.mdx +3 -1
- package/content/references/workflows/payout/process-payout-for-webhook.mdx +3 -1
- package/content/references/workflows/product/confirm-products.mdx +2 -0
- package/content/references/workflows/product/reject-product.mdx +2 -0
- package/content/references/workflows/product/request-product-change.mdx +2 -0
- package/content/references/workflows/seller/approve-seller.mdx +2 -0
- package/content/references/workflows/seller/suspend-seller.mdx +2 -0
- package/content/references/workflows/seller/terminate-seller.mdx +2 -0
- package/content/references/workflows/seller/unsuspend-seller.mdx +2 -0
- package/content/references/workflows/seller/unterminate-seller.mdx +2 -0
- package/content/resources/best-practices/api-routes.mdx +306 -0
- package/content/resources/best-practices/custom-fields.mdx +241 -0
- package/content/resources/best-practices/frontend.mdx +286 -0
- package/content/resources/best-practices/module-links.mdx +157 -0
- package/content/resources/best-practices/modules.mdx +144 -0
- package/content/resources/best-practices/overview.mdx +115 -0
- package/content/resources/best-practices/subscribers-and-jobs.mdx +133 -0
- package/content/resources/best-practices/types.mdx +120 -0
- package/content/resources/best-practices/workflows.mdx +173 -0
- package/content/resources/customization/custom-fields.mdx +5 -1
- package/content/resources/customization/extending-panels.mdx +170 -113
- package/content/resources/tutorials/add-a-widget.mdx +110 -0
- package/content/resources/tutorials/add-order-detail-button.mdx +113 -0
- package/content/resources/tutorials/custom-panel-page.mdx +2 -2
- package/content/resources/tutorials/customize-navigation.mdx +110 -0
- package/content/resources/tutorials/extend-forms-and-tables.mdx +188 -0
- package/content/resources/tutorials/extend-onboarding.mdx +253 -0
- package/content/resources/tutorials/import-export-products.mdx +3 -3
- package/content/resources/tutorials/store-setup-checklist.mdx +214 -0
- package/llms.txt +17 -3
- package/package.json +1 -1
- package/content/resources/tutorials/recompose-a-page.mdx +0 -129
- package/content/resources/tutorials/replace-panel-components.mdx +0 -116
|
@@ -1,32 +1,43 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: "Extending Panels"
|
|
3
|
-
description: "Add pages, customize navigation,
|
|
3
|
+
description: "Add pages, inject widgets, customize navigation, and extend forms and tables in the admin and vendor panels."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
Both the admin panel and vendor portal use the same SDK (`@mercurjs/dashboard-sdk`). Customization is file-based and convention-driven — you add pages by
|
|
6
|
+
Both the admin panel and vendor portal use the same SDK (`@mercurjs/dashboard-sdk`). Customization is **file-based and convention-driven** — you add pages, widgets, and field extensions by dropping files under `src/`, and configure navigation through a single file. Nothing is registered by hand; the SDK crawls your `src/` at build time and wires everything in.
|
|
7
7
|
|
|
8
8
|
<Info>
|
|
9
|
-
**
|
|
9
|
+
**Coming from Medusa?** The extension model is deliberately Medusa-shaped. The helpers you know — `defineWidgetConfig`, `defineRouteConfig`, `defineCustomFieldsConfig`, `createFormHelper` — all exist here, re-exported from `@mercurjs/dashboard-sdk` (without Medusa's `unstable_` prefix). What's different: Mercur ships **two** panels (admin *and* vendor) from one framework, so a file only ever targets the panel it lives in (there is no `surface` field), and widget zones carry a `before | after` placement suffix on the zone id. Everything is additive by default — your contribution augments the built-in page instead of replacing it.
|
|
10
10
|
</Info>
|
|
11
11
|
|
|
12
|
-
##
|
|
12
|
+
## The extension mechanisms
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
Every panel customization is **additive** — you augment a built-in page without owning it. One concern per file, discovered by the build-time crawl:
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
| Change global chrome (sidebar, topbar, store setup) | A component override in the plugin config | One component, applied everywhere its slot renders — see [Replace components](#replace-components) |
|
|
21
|
-
| Reuse a feature across projects | A [block](/rc/learn/blocks) | Ships API + admin + vendor files installable with `mercurjs add` — see the [build-a-block tutorial](/rc/resources/tutorials/build-a-block) |
|
|
16
|
+
- **Widgets** — inject a React component at a named zone on a built-in page (`defineWidgetConfig`).
|
|
17
|
+
- **Navigation** — reorder, hide, relabel, or re-parent built-in sidebar items (`defineNavigationConfig`).
|
|
18
|
+
- **Custom fields** — add validated fields to built-in forms, replace/remove/add fields in detail sections, and add columns to list tables (`defineCustomFieldsConfig` + `createFormHelper`).
|
|
19
|
+
- **Pages** — add a brand-new screen with a drop-in `page.tsx` route.
|
|
22
20
|
|
|
23
21
|
<Note>
|
|
24
|
-
|
|
22
|
+
Additive tools leave the rest of the built-in page — data fetching, filters, pagination, i18n — completely intact. A widget, a nav override, or a custom field changes only the spot you target.
|
|
25
23
|
</Note>
|
|
26
24
|
|
|
25
|
+
## Choosing your extension mechanism
|
|
26
|
+
|
|
27
|
+
Rule of thumb: **injecting UI into an existing page? Use a widget. Adding data to a form or section? Use a custom field. Reshaping the sidebar? Use the navigation file. Adding a whole new screen? Drop in a route.**
|
|
28
|
+
|
|
29
|
+
| You want to… | Use | Why it's the right tool |
|
|
30
|
+
|--------------|-----|------------------------|
|
|
31
|
+
| Show a banner, panel, or CTA on a built-in page | A **widget** (`defineWidgetConfig`) | Renders at a named zone; the rest of the page is untouched — see [Add a widget](/rc/resources/tutorials/add-a-widget) |
|
|
32
|
+
| Add a field to a built-in form, or add/replace/remove a field in a detail section | A **custom field** (`defineCustomFieldsConfig`) | Validated field wired into the existing form/section — see [Extend forms and tables](/rc/resources/tutorials/extend-forms-and-tables) |
|
|
33
|
+
| Add or override a column on a built-in list table | The `list` block of a **custom field** file | Model-scoped column extension — see [Extend forms and tables](/rc/resources/tutorials/extend-forms-and-tables) |
|
|
34
|
+
| Reorder / hide / relabel / re-parent sidebar items | The **navigation** file (`defineNavigationConfig`) | One host-owned `_navigation.ts` — see [Customize navigation](/rc/resources/tutorials/customize-navigation) |
|
|
35
|
+
| Add a new screen or feature | A drop-in `page.tsx` route | New URL, auto-registered, sidebar entry via `config` — see [Add a page](/rc/resources/tutorials/custom-panel-page) |
|
|
36
|
+
| Reuse a feature across projects | A [block](/rc/learn/blocks) | Ships API + admin + vendor files installable with `mercurjs add` |
|
|
37
|
+
|
|
27
38
|
## Set up
|
|
28
39
|
|
|
29
|
-
All configuration lives in the panel app's Vite config — there is no separate config
|
|
40
|
+
All configuration lives in the panel app's Vite config — there is no separate `mercur.config.ts`.
|
|
30
41
|
|
|
31
42
|
<Steps>
|
|
32
43
|
<Step title="Register the plugin">
|
|
@@ -54,8 +65,17 @@ All configuration lives in the panel app's Vite config — there is no separate
|
|
|
54
65
|
<Step title="Pass environment values explicitly">
|
|
55
66
|
The plugin doesn't read `.env` itself — load environment variables in `vite.config.ts` (e.g. with Vite's `loadEnv`) and pass them into the plugin options, as the starter template does with `backendUrl`.
|
|
56
67
|
</Step>
|
|
68
|
+
<Step title="Register typed extension targets (once per panel)">
|
|
69
|
+
Widgets, navigation, and custom fields target **typed ids** (zone ids, nav item ids, model field ids) that each panel package generates from its own built-in pages and ships as `@mercurjs/{admin,vendor}/extension-targets`. Register them once so the ids resolve in every extension file, with a single ambient reference in your app's `src`:
|
|
70
|
+
|
|
71
|
+
```typescript apps/vendor/src/extension-targets.d.ts
|
|
72
|
+
/// <reference types="@mercurjs/vendor/extension-targets" />
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
(For the admin app, reference `@mercurjs/admin/extension-targets`.) With this file present, a typo in a zone, model, field, or nav id fails `tsc` instead of silently doing nothing at runtime. `create-mercur-app` ships this file already.
|
|
76
|
+
</Step>
|
|
57
77
|
<Step title="Restart after config changes">
|
|
58
|
-
Options are applied at build time through virtual modules. Adding or removing routes hot-reloads automatically, but changes to the plugin options require a dev-server restart.
|
|
78
|
+
Options are applied at build time through virtual modules. Adding or removing routes, widgets, and custom-field files hot-reloads automatically, but changes to the plugin options (in `vite.config.ts`) require a dev-server restart.
|
|
59
79
|
</Step>
|
|
60
80
|
</Steps>
|
|
61
81
|
|
|
@@ -68,11 +88,127 @@ All configuration lives in the panel app's Vite config — there is no separate
|
|
|
68
88
|
| `vendorUrl` | `string` | Absolute vendor portal URL including its path prefix |
|
|
69
89
|
| `name` | `string` | Application name shown in the sidebar |
|
|
70
90
|
| `logo` | `string` | URL to a logo image |
|
|
71
|
-
| `components` | `object` | Component overrides (see [Replace components](#replace-components)) |
|
|
72
91
|
| `i18n` | `object` | Internationalization settings (`{ defaultLanguage }`) |
|
|
73
92
|
| `enableSellerRegistration` | `boolean` | Enable the public seller registration flow (vendor portal) |
|
|
74
93
|
| `imageLimit` | `number` | Max upload size for images in bytes (default: 2 MB) |
|
|
75
94
|
|
|
95
|
+
## Widgets
|
|
96
|
+
|
|
97
|
+
Inject a React component at a named zone on a built-in page. Drop a file under `src/widgets/`, export the component as the default and a `config` built with `defineWidgetConfig`:
|
|
98
|
+
|
|
99
|
+
```tsx src/widgets/product-list-banner.tsx
|
|
100
|
+
import { defineWidgetConfig } from "@mercurjs/dashboard-sdk"
|
|
101
|
+
import { Container, Text } from "@medusajs/ui"
|
|
102
|
+
|
|
103
|
+
export const config = defineWidgetConfig({
|
|
104
|
+
zone: "product.list.before",
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
export default function ProductListBanner() {
|
|
108
|
+
return (
|
|
109
|
+
<Container className="mb-2">
|
|
110
|
+
<Text size="small" className="text-ui-fg-subtle">
|
|
111
|
+
Tip: bulk-import products from the Products menu.
|
|
112
|
+
</Text>
|
|
113
|
+
</Container>
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The zone id is `<domain>.<view>.<placement>`. The placement is the last segment:
|
|
119
|
+
|
|
120
|
+
- `before` / `after` — stack your widget before or after the built-in content (multiple widgets stack in registration order).
|
|
121
|
+
|
|
122
|
+
Zones mounted today (**vendor portal**):
|
|
123
|
+
|
|
124
|
+
| Zone id | Where it renders |
|
|
125
|
+
|---------|------------------|
|
|
126
|
+
| `product.list.before` / `.after` | Vendor product list page |
|
|
127
|
+
| `login.logo.*` / `login.before.*` / `login.after.*` | The public login screen (rendered before authentication) |
|
|
128
|
+
|
|
129
|
+
The full set of valid zones is typed as `WidgetZoneId` and generated into each panel's `extension-targets.d.ts` — your editor autocompletes them, and an unknown zone fails `tsc`. Walk through it end to end in [Add a widget](/rc/resources/tutorials/add-a-widget).
|
|
130
|
+
|
|
131
|
+
## Navigation
|
|
132
|
+
|
|
133
|
+
Custom drop-in routes place their own sidebar item via `defineRouteConfig` (see [Add a page](#add-a-page)). To reshape the **built-in** sidebar items, author a single host-owned file, `src/_navigation.ts`:
|
|
134
|
+
|
|
135
|
+
```ts src/_navigation.ts
|
|
136
|
+
import { defineNavigationConfig } from "@mercurjs/dashboard-sdk"
|
|
137
|
+
|
|
138
|
+
export default defineNavigationConfig({
|
|
139
|
+
items: [
|
|
140
|
+
{ id: "orders", rank: 0 }, // pin to the top
|
|
141
|
+
{ id: "price-lists", hidden: true }, // hide a built-in item
|
|
142
|
+
{ id: "payouts", label: "Settlements" }, // relabel
|
|
143
|
+
{ id: "campaigns", nested: "orders" }, // re-parent under Orders
|
|
144
|
+
{ id: "categories", nested: null, rank: 1 }, // promote a nested item to top level
|
|
145
|
+
],
|
|
146
|
+
})
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
- `id` targets any built-in item — top-level routes *or* nested children — by its stable id, typed as `NavItemId`.
|
|
150
|
+
- `rank` orders an item within its parent; `hidden` removes it from the sidebar; `label` / `icon` relabel it; `nested` re-parents it (`nested: null` promotes to top level, typed against `NavParentId`).
|
|
151
|
+
- Navigation is a **single host-owned file** — installed blocks cannot reorder the sidebar, so it stays one source of truth. It does not change custom routes, which still place themselves via `defineRouteConfig`.
|
|
152
|
+
|
|
153
|
+
Walk through it in [Customize navigation](/rc/resources/tutorials/customize-navigation).
|
|
154
|
+
|
|
155
|
+
## Custom fields
|
|
156
|
+
|
|
157
|
+
Add validated fields to a model's built-in create/edit forms, replace/remove/add fields in its detail sections, and add columns to its list table — all from one model-scoped file. Drop `src/custom-fields/<model>.tsx` and default-export a `defineCustomFieldsConfig`:
|
|
158
|
+
|
|
159
|
+
```tsx src/custom-fields/product.tsx
|
|
160
|
+
import { defineCustomFieldsConfig } from "@mercurjs/dashboard-sdk"
|
|
161
|
+
import { createFormHelper } from "@mercurjs/dashboard-shared"
|
|
162
|
+
import { Text } from "@medusajs/ui"
|
|
163
|
+
|
|
164
|
+
const form = createFormHelper<{ metadata?: Record<string, unknown> }>()
|
|
165
|
+
|
|
166
|
+
export default defineCustomFieldsConfig({
|
|
167
|
+
model: "product",
|
|
168
|
+
forms: [
|
|
169
|
+
{
|
|
170
|
+
zone: "edit",
|
|
171
|
+
fields: {
|
|
172
|
+
erp_id: form.define({
|
|
173
|
+
validation: form.string().optional(), // Zod → input type + validation
|
|
174
|
+
label: "ERP ID",
|
|
175
|
+
description: "External system identifier",
|
|
176
|
+
placeholder: "ERP-000",
|
|
177
|
+
}),
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
displays: [
|
|
182
|
+
{
|
|
183
|
+
zone: "general",
|
|
184
|
+
fields: [
|
|
185
|
+
{ id: "erp_id", component: ({ data }) => <Text className="px-6 py-4">ERP: {String(data.metadata?.erp_id ?? "-")}</Text> }, // ADD
|
|
186
|
+
{ id: "subtitle", component: null }, // REMOVE a built-in field
|
|
187
|
+
{ id: "handle", component: BrandedHandle }, // REPLACE a built-in field's render
|
|
188
|
+
],
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
list: {
|
|
192
|
+
columns: [{ id: "erp_id", header: "ERP", component: ({ row }) => String(row.metadata?.erp_id ?? "-") }],
|
|
193
|
+
viewDefaults: {
|
|
194
|
+
columnVisibility: { collection: false }, // hide a built-in column
|
|
195
|
+
columnOrder: ["product", "erp_id", "status"],
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
})
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
- `forms[]` adds validated fields to a form `zone` (`create` / `edit` / `organize` / …). Input type and validation come from a Zod schema via `createFormHelper`; fields render through the standard `Form.Field` chain and participate in the existing submit + validation flow.
|
|
202
|
+
- `displays[]` targets detail-page sections: keyed by field `id`, an entry **adds** a read-only row (unknown id), **replaces** a built-in field's render (matching id + `component`), or **removes** it (matching id + `component: null`).
|
|
203
|
+
- `list` extends the model's list table: add/override columns by id, hide via `viewDefaults.columnVisibility`, reorder via `viewDefaults.columnOrder`.
|
|
204
|
+
- Everything is typed against the panel-generated `CustomFieldsRegistry` — valid `model`, `zone`, and built-in field ids autocomplete; an invalid target fails `tsc`.
|
|
205
|
+
|
|
206
|
+
<Warning>
|
|
207
|
+
**Panel custom fields vs. the Custom Fields module.** `defineCustomFieldsConfig` (this section) is a **UI** surface — it renders, validates, and displays fields in the panels. It does not create database columns. To *store* extra data on an entity, use the backend [Custom Fields module](/rc/resources/customization/custom-fields), or wire your own API route/workflow. In the MVP, panel custom fields for `product` are submitted under `additional_data` and persisted onto the product's `metadata`.
|
|
208
|
+
</Warning>
|
|
209
|
+
|
|
210
|
+
Full walkthrough (forms, sections, list columns): [Extend forms and tables](/rc/resources/tutorials/extend-forms-and-tables).
|
|
211
|
+
|
|
76
212
|
## Add a page
|
|
77
213
|
|
|
78
214
|
<Steps>
|
|
@@ -114,7 +250,7 @@ All configuration lives in the panel app's Vite config — there is no separate
|
|
|
114
250
|
</Steps>
|
|
115
251
|
|
|
116
252
|
<Info>
|
|
117
|
-
**Matching paths replace, new paths append.** If your route's path matches a built-in page (e.g. `src/routes/products/page.tsx` → `/products`), your page **replaces** the built-in one
|
|
253
|
+
**Matching paths replace, new paths append.** If your route's path matches a built-in page (e.g. `src/routes/products/page.tsx` → `/products`), your page **replaces** the built-in one. Any other path is added alongside the built-in routes. Delete the file and the built-in page returns. To change *part* of a built-in page without owning it, prefer a widget or a custom field.
|
|
118
254
|
</Info>
|
|
119
255
|
|
|
120
256
|
### Routing conventions
|
|
@@ -131,43 +267,6 @@ File paths map to URL routes automatically:
|
|
|
131
267
|
| `src/routes/(settings)/page.tsx` | Route grouping | Groups routes without adding a URL segment |
|
|
132
268
|
| `src/routes/dashboard/@sidebar/page.tsx` | Parallel route | Renders alongside parent |
|
|
133
269
|
|
|
134
|
-
## Navigation
|
|
135
|
-
|
|
136
|
-
Sidebar items are generated from pages that export a `config` with a `label`.
|
|
137
|
-
|
|
138
|
-
### Ordering
|
|
139
|
-
|
|
140
|
-
Use `rank` to control the order. Lower values appear higher:
|
|
141
|
-
|
|
142
|
-
```tsx
|
|
143
|
-
// src/routes/orders/page.tsx
|
|
144
|
-
export const config: RouteConfig = {
|
|
145
|
-
label: "Orders",
|
|
146
|
-
icon: ShoppingCart,
|
|
147
|
-
rank: 1,
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// src/routes/products/page.tsx
|
|
151
|
-
export const config: RouteConfig = {
|
|
152
|
-
label: "Products",
|
|
153
|
-
icon: Tag,
|
|
154
|
-
rank: 2,
|
|
155
|
-
}
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
### Nested menus
|
|
159
|
-
|
|
160
|
-
Use `nested` to group pages under a parent:
|
|
161
|
-
|
|
162
|
-
```tsx src/routes/settings/shipping/page.tsx
|
|
163
|
-
export const config: RouteConfig = {
|
|
164
|
-
label: "Shipping",
|
|
165
|
-
nested: "/settings",
|
|
166
|
-
}
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
This places "Shipping" as a child item under `/settings` in the sidebar.
|
|
170
|
-
|
|
171
270
|
## Branding
|
|
172
271
|
|
|
173
272
|
Set `name` and `logo` in the plugin options to customize the sidebar header:
|
|
@@ -180,54 +279,6 @@ mercurDashboardPlugin({
|
|
|
180
279
|
})
|
|
181
280
|
```
|
|
182
281
|
|
|
183
|
-
## Replace components
|
|
184
|
-
|
|
185
|
-
<Steps>
|
|
186
|
-
<Step title="Write the replacement">
|
|
187
|
-
Create the component anywhere under `src/`. It must have a default export:
|
|
188
|
-
|
|
189
|
-
```tsx src/components/custom-topbar-actions.tsx
|
|
190
|
-
export default function CustomTopbarActions() {
|
|
191
|
-
return (
|
|
192
|
-
<div>
|
|
193
|
-
<button>Help</button>
|
|
194
|
-
<button>Notifications</button>
|
|
195
|
-
</div>
|
|
196
|
-
)
|
|
197
|
-
}
|
|
198
|
-
```
|
|
199
|
-
</Step>
|
|
200
|
-
<Step title="Point the config at it">
|
|
201
|
-
Register it in the `components` option. Paths are relative to `src/`:
|
|
202
|
-
|
|
203
|
-
```typescript vite.config.ts
|
|
204
|
-
mercurDashboardPlugin({
|
|
205
|
-
medusaConfigPath: '../../packages/api/medusa-config.ts',
|
|
206
|
-
name: 'My Marketplace',
|
|
207
|
-
components: {
|
|
208
|
-
MainSidebar: 'components/custom-sidebar',
|
|
209
|
-
SettingsSidebar: 'components/custom-settings-sidebar',
|
|
210
|
-
TopbarActions: 'components/custom-topbar-actions',
|
|
211
|
-
},
|
|
212
|
-
})
|
|
213
|
-
```
|
|
214
|
-
</Step>
|
|
215
|
-
<Step title="Restart and verify">
|
|
216
|
-
Restart the dev server — your component now renders everywhere its slot appears. Remove the entry and restart to get the built-in back; the originals are never modified.
|
|
217
|
-
</Step>
|
|
218
|
-
</Steps>
|
|
219
|
-
|
|
220
|
-
| Component | Description |
|
|
221
|
-
|-----------|-------------|
|
|
222
|
-
| `MainSidebar` | Primary navigation sidebar |
|
|
223
|
-
| `SettingsSidebar` | Settings section sidebar |
|
|
224
|
-
| `TopbarActions` | Action buttons in the top bar |
|
|
225
|
-
| `StoreSetup` | Store setup screen for new sellers (vendor portal) |
|
|
226
|
-
|
|
227
|
-
<Warning>
|
|
228
|
-
These four slots are the complete list — there is no generic "replace any component" registry. If what you want to change isn't one of them, it's a page concern: [re-compose the page](/rc/resources/tutorials/recompose-a-page) instead.
|
|
229
|
-
</Warning>
|
|
230
|
-
|
|
231
282
|
## Internationalization
|
|
232
283
|
|
|
233
284
|
<Steps>
|
|
@@ -274,36 +325,42 @@ mercurDashboardPlugin({
|
|
|
274
325
|
## FAQ
|
|
275
326
|
|
|
276
327
|
<AccordionGroup>
|
|
277
|
-
<Accordion title="Can I use Medusa's defineWidgetConfig
|
|
278
|
-
|
|
328
|
+
<Accordion title="Can I use Medusa's defineWidgetConfig and widget zones?">
|
|
329
|
+
Yes. `defineWidgetConfig` is re-exported from `@mercurjs/dashboard-sdk` and drives file-based widgets under `src/widgets/`. The difference from Medusa is the `before | after` placement suffix on the zone id and that each panel ships its own typed zone set. See [Add a widget](/rc/resources/tutorials/add-a-widget).
|
|
330
|
+
</Accordion>
|
|
331
|
+
<Accordion title="How do I add a field to a built-in form or detail page?">
|
|
332
|
+
Use `defineCustomFieldsConfig` in `src/custom-fields/<model>.tsx` — `forms[]` adds validated fields to create/edit forms, and `displays[]` adds/replaces/removes fields in detail sections. See [Extend forms and tables](/rc/resources/tutorials/extend-forms-and-tables).
|
|
279
333
|
</Accordion>
|
|
280
334
|
<Accordion title="How do I change just one part of a built-in page?">
|
|
281
|
-
|
|
335
|
+
For a spot inside the page (a banner, an extra field, a column), use a [widget](#widgets) or a [custom field](#custom-fields) — the rest of the page keeps all its behavior.
|
|
282
336
|
</Accordion>
|
|
283
337
|
<Accordion title="Why doesn't my page show up in the sidebar?">
|
|
284
338
|
A sidebar item is only generated when the route file exports a `config` object with a `label`. Also check that the file is named exactly `page.tsx` (or `.ts`/`.jsx`/`.js`) under `src/routes/` and has a **default** export — files without one are skipped entirely.
|
|
285
339
|
</Accordion>
|
|
340
|
+
<Accordion title="Why does my widget / custom field / nav override not type-check?">
|
|
341
|
+
Make sure the panel's typed targets are registered — a single `src/extension-targets.d.ts` with `/// <reference types="@mercurjs/vendor/extension-targets" />` (or the admin equivalent). Without it, zone/model/nav ids aren't known to TypeScript. `create-mercur-app` ships this file.
|
|
342
|
+
</Accordion>
|
|
286
343
|
<Accordion title="Is there a mercur.config.ts file?">
|
|
287
344
|
No — all panel configuration is passed inline to `mercurDashboardPlugin()` in `vite.config.ts`. If you've seen references to a separate config file, they're outdated.
|
|
288
345
|
</Accordion>
|
|
289
346
|
<Accordion title="Does this apply to both the admin panel and the vendor portal?">
|
|
290
|
-
|
|
347
|
+
Both panels use the same SDK and conventions, and a file only targets the panel it lives in. Which built-in zones, models, and nav ids exist differs per panel (each ships its own `extension-targets.d.ts`). Today the widget zones and product custom fields are mounted in the **vendor** portal; navigation overrides work in both.
|
|
291
348
|
</Accordion>
|
|
292
349
|
</AccordionGroup>
|
|
293
350
|
|
|
294
351
|
## Next steps
|
|
295
352
|
|
|
296
353
|
<CardGroup cols={2}>
|
|
297
|
-
<Card title="Add a
|
|
298
|
-
|
|
354
|
+
<Card title="Add a widget" href="/rc/resources/tutorials/add-a-widget">
|
|
355
|
+
Inject a component at a built-in zone with `defineWidgetConfig`.
|
|
299
356
|
</Card>
|
|
300
|
-
<Card title="
|
|
301
|
-
|
|
357
|
+
<Card title="Extend forms and tables" href="/rc/resources/tutorials/extend-forms-and-tables">
|
|
358
|
+
Add fields and columns with `defineCustomFieldsConfig`.
|
|
302
359
|
</Card>
|
|
303
|
-
<Card title="
|
|
304
|
-
|
|
360
|
+
<Card title="Customize navigation" href="/rc/resources/tutorials/customize-navigation">
|
|
361
|
+
Reorder, hide, and re-parent sidebar items.
|
|
305
362
|
</Card>
|
|
306
|
-
<Card title="
|
|
307
|
-
|
|
363
|
+
<Card title="Add a custom panel page" href="/rc/resources/tutorials/custom-panel-page">
|
|
364
|
+
Your first drop-in route, end to end.
|
|
308
365
|
</Card>
|
|
309
366
|
</CardGroup>
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Add a widget"
|
|
3
|
+
description: "Inject a React component at a named zone on a built-in panel page with defineWidgetConfig — no forking."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
A **widget** is a React component attached to a named zone on a built-in page. You drop one file under `src/widgets/`, and the SDK renders it at that zone while the rest of the page — its data fetching, filters, pagination — stays exactly as shipped. This is the lightest way to add UI to a page you don't own.
|
|
7
|
+
|
|
8
|
+
<Info>
|
|
9
|
+
**Additive, not a replacement.** Unlike a drop-in route (which owns the whole page), a widget layers your component onto the built-in page at a documented zone. Reach for this first when you just want to *add* something to an existing screen.
|
|
10
|
+
</Info>
|
|
11
|
+
|
|
12
|
+
## What you'll build
|
|
13
|
+
|
|
14
|
+
A tip banner above the vendor product list, rendered from a single file — with the list itself untouched.
|
|
15
|
+
|
|
16
|
+
## Register the typed targets (once)
|
|
17
|
+
|
|
18
|
+
Widget zones are typed ids that the vendor panel generates from its own pages and ships as `@mercurjs/vendor/extension-targets`. Register them once so the ids resolve everywhere, with a single ambient reference in your app's `src`:
|
|
19
|
+
|
|
20
|
+
```typescript apps/vendor/src/extension-targets.d.ts
|
|
21
|
+
/// <reference types="@mercurjs/vendor/extension-targets" />
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Projects from `create-mercur-app` already ship this file. With it present, an invalid zone fails `tsc` instead of silently doing nothing.
|
|
25
|
+
|
|
26
|
+
## Add the widget
|
|
27
|
+
|
|
28
|
+
<Steps>
|
|
29
|
+
<Step title="Create the widget file">
|
|
30
|
+
Drop a file under `src/widgets/`. Export the component as the **default** and a `config` built with `defineWidgetConfig`. The `zone` names where it renders:
|
|
31
|
+
|
|
32
|
+
```tsx apps/vendor/src/widgets/product-list-banner.tsx
|
|
33
|
+
import { defineWidgetConfig } from "@mercurjs/dashboard-sdk"
|
|
34
|
+
import { Container, Text } from "@medusajs/ui"
|
|
35
|
+
|
|
36
|
+
export const config = defineWidgetConfig({
|
|
37
|
+
zone: "product.list.before",
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
export default function ProductListBanner() {
|
|
41
|
+
return (
|
|
42
|
+
<Container className="mb-2">
|
|
43
|
+
<Text size="small" className="text-ui-fg-subtle">
|
|
44
|
+
Tip: bulk-import products from the Products menu.
|
|
45
|
+
</Text>
|
|
46
|
+
</Container>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
</Step>
|
|
51
|
+
<Step title="Understand the zone id">
|
|
52
|
+
A zone id is `<domain>.<view>.<placement>`. The last segment is the placement:
|
|
53
|
+
|
|
54
|
+
| Placement | Effect |
|
|
55
|
+
|-----------|--------|
|
|
56
|
+
| `before` | Renders before the built-in content of the zone |
|
|
57
|
+
| `after` | Renders after the built-in content |
|
|
58
|
+
|
|
59
|
+
Multiple `before` / `after` widgets on the same zone stack in registration order.
|
|
60
|
+
</Step>
|
|
61
|
+
<Step title="Reload the panel">
|
|
62
|
+
Start the project (`bun run dev`) and open the vendor portal. Widget files hot-reload — the banner appears above the product list, and the table below it works exactly as before.
|
|
63
|
+
</Step>
|
|
64
|
+
</Steps>
|
|
65
|
+
|
|
66
|
+
## Available zones
|
|
67
|
+
|
|
68
|
+
Zones mounted today in the **vendor portal**:
|
|
69
|
+
|
|
70
|
+
| Zone | Where it renders |
|
|
71
|
+
|------|------------------|
|
|
72
|
+
| `product.list.before` / `.after` | Vendor product list page |
|
|
73
|
+
| `seller.setup.before` / `.after` | The store-setup / onboarding surface (dashboard home + store settings), passed the `seller` as `data` |
|
|
74
|
+
| `login.logo.*` | The logo slot on the public login screen |
|
|
75
|
+
| `login.before.*` / `login.after.*` | Around the login form (rendered before authentication) |
|
|
76
|
+
|
|
77
|
+
The full, valid set is typed as `WidgetZoneId` and generated into `@mercurjs/vendor/extension-targets` from the panel's own zone hosts — let your editor autocomplete `zone:` to see every option. A zone no page renders can't be targeted and won't type-check.
|
|
78
|
+
|
|
79
|
+
## Verify
|
|
80
|
+
|
|
81
|
+
1. The tip banner renders above the product list.
|
|
82
|
+
2. Search, filter, and paginate the list — all built-in behavior still works.
|
|
83
|
+
3. Change the zone to `product.list.after` and reload — the banner moves below the list.
|
|
84
|
+
4. Set `zone: "not.a.zone"` — `tsc` (`bun run lint`) fails with a "not assignable to `WidgetZoneId`" error.
|
|
85
|
+
5. Delete the file — the banner disappears; nothing else changed.
|
|
86
|
+
|
|
87
|
+
## FAQ
|
|
88
|
+
|
|
89
|
+
<AccordionGroup>
|
|
90
|
+
<Accordion title="Can a widget target more than one zone?">
|
|
91
|
+
Yes — `zone` accepts an array (`zone: ["product.list.before", "login.after.before"]`), and the same component renders at each.
|
|
92
|
+
</Accordion>
|
|
93
|
+
<Accordion title="Can a block ship widgets?">
|
|
94
|
+
Yes. A [block](/rc/learn/blocks) can include `src/widgets/` files in its `vendor_ui` / `admin_ui` entry, and they're aggregated just like the host app's — installing the block adds the widget with no wiring.
|
|
95
|
+
</Accordion>
|
|
96
|
+
<Accordion title="Does the admin panel have widget zones too?">
|
|
97
|
+
The zone set is per panel and generated from each panel's pages. Today the mounted zones live in the vendor portal (`product.list.*`, `login.*`); the admin panel exposes navigation and product custom fields. Check `@mercurjs/admin/extension-targets` for its current zones.
|
|
98
|
+
</Accordion>
|
|
99
|
+
</AccordionGroup>
|
|
100
|
+
|
|
101
|
+
## Next steps
|
|
102
|
+
|
|
103
|
+
<CardGroup cols={2}>
|
|
104
|
+
<Card title="Extend forms and tables" href="/rc/resources/tutorials/extend-forms-and-tables">
|
|
105
|
+
Add validated fields and columns with defineCustomFieldsConfig.
|
|
106
|
+
</Card>
|
|
107
|
+
<Card title="Extend the onboarding flow" href="/rc/resources/tutorials/extend-onboarding">
|
|
108
|
+
Add a store-setup field and persist it through a workflow hook.
|
|
109
|
+
</Card>
|
|
110
|
+
</CardGroup>
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Add a button to order details"
|
|
3
|
+
description: "Drop a 'Copy link' button onto the vendor order detail page with a widget — no forking, no page override."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
The order detail page is a built-in panel screen you don't own. To add a small piece of UI to it — a button, a badge, a note — you don't copy the page. You drop a **widget** at one of its zones, and the SDK renders your component there while the rest of the page keeps working exactly as shipped.
|
|
7
|
+
|
|
8
|
+
This tutorial adds a **"Copy link"** button to the order summary section that copies a link to the order.
|
|
9
|
+
|
|
10
|
+
<Info>
|
|
11
|
+
**Additive, not a replacement.** A widget layers your component onto a built-in page at a documented zone. Reach for it first whenever you just want to *add* something to an existing screen.
|
|
12
|
+
</Info>
|
|
13
|
+
|
|
14
|
+
## Add the button
|
|
15
|
+
|
|
16
|
+
<Steps>
|
|
17
|
+
<Step title="Create the widget file">
|
|
18
|
+
Drop a file under `src/widgets/`. Export the component as the **default** and a `config` built with `defineWidgetConfig`. Target `orders.detail.summary.after` — your component renders in the order summary section footer and receives the loaded order as `data`.
|
|
19
|
+
|
|
20
|
+
```tsx apps/vendor/src/widgets/order-copy-link.tsx
|
|
21
|
+
import { defineWidgetConfig } from "@mercurjs/dashboard-sdk"
|
|
22
|
+
import type { HttpTypes } from "@medusajs/types"
|
|
23
|
+
import { Button, Container, toast } from "@medusajs/ui"
|
|
24
|
+
|
|
25
|
+
export const config = defineWidgetConfig({
|
|
26
|
+
zone: "orders.detail.summary.after",
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const OrderCopyLink = ({ data: order }: { data?: HttpTypes.AdminOrder }) => {
|
|
30
|
+
if (!order) return null
|
|
31
|
+
|
|
32
|
+
const orderLink = `${window.location.origin}/orders/${order.id}`
|
|
33
|
+
|
|
34
|
+
const handleCopy = async () => {
|
|
35
|
+
try {
|
|
36
|
+
await navigator.clipboard.writeText(orderLink)
|
|
37
|
+
toast.success("Link copied")
|
|
38
|
+
} catch {
|
|
39
|
+
toast.error("Couldn't copy the link")
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<Container className="flex items-center justify-end p-3">
|
|
45
|
+
<Button size="small" variant="secondary" onClick={handleCopy}>
|
|
46
|
+
Copy link
|
|
47
|
+
</Button>
|
|
48
|
+
</Container>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export default OrderCopyLink
|
|
53
|
+
```
|
|
54
|
+
</Step>
|
|
55
|
+
<Step title="Understand the zone id">
|
|
56
|
+
A zone id is `<domain>.<view>.<slot>.<placement>`. The last segment is the placement:
|
|
57
|
+
|
|
58
|
+
| Placement | Effect |
|
|
59
|
+
|-----------|--------|
|
|
60
|
+
| `before` | Renders before the built-in content of the zone |
|
|
61
|
+
| `after` | Renders after the built-in content |
|
|
62
|
+
|
|
63
|
+
Multiple `before` / `after` widgets on the same zone stack in registration order.
|
|
64
|
+
</Step>
|
|
65
|
+
<Step title="Reload the panel">
|
|
66
|
+
Start the project (`bun run dev`) and open any order in the vendor portal. Widget files hot-reload — the "Copy link" button appears in the summary section footer, and the rest of the page is untouched.
|
|
67
|
+
</Step>
|
|
68
|
+
</Steps>
|
|
69
|
+
|
|
70
|
+
## Order detail zones
|
|
71
|
+
|
|
72
|
+
Zones mounted on the vendor order detail page:
|
|
73
|
+
|
|
74
|
+
| Zone | Where it renders |
|
|
75
|
+
|------|------------------|
|
|
76
|
+
| `orders.detail.summary.before` / `.after` | Inside the order summary section (around its footer) |
|
|
77
|
+
| `orders.detail.main.before` / `.after` | Around the main column (summary, payment, fulfillment) |
|
|
78
|
+
| `orders.detail.side.before` / `.after` | Around the sidebar (customer, activity) |
|
|
79
|
+
|
|
80
|
+
Each is passed the loaded `order` as `data`. The full, valid set is typed as `WidgetZoneId` and generated from the panel's own zone hosts — let your editor autocomplete `zone:` to see every option. A zone no page renders can't be targeted and won't type-check.
|
|
81
|
+
|
|
82
|
+
## Verify
|
|
83
|
+
|
|
84
|
+
1. Open an order — the "Copy link" button renders in the summary section footer.
|
|
85
|
+
2. Click it — the link is copied and a toast appears.
|
|
86
|
+
3. Change the zone to `orders.detail.side.before` and reload — the button moves to the top of the sidebar.
|
|
87
|
+
4. Set `zone: "not.a.zone"` — `tsc` (`bun run lint`) fails with a "not assignable to `WidgetZoneId`" error.
|
|
88
|
+
5. Delete the file — the button disappears; nothing else changed.
|
|
89
|
+
|
|
90
|
+
## FAQ
|
|
91
|
+
|
|
92
|
+
<AccordionGroup>
|
|
93
|
+
<Accordion title="What can I read from the order?">
|
|
94
|
+
The zone passes the loaded order as `data` (`HttpTypes.AdminOrder`) — id, display id, totals, items, `payment_collections`, customer, and more. Build the link (or any UI) from it.
|
|
95
|
+
</Accordion>
|
|
96
|
+
<Accordion title="Can a block ship this instead of the host app?">
|
|
97
|
+
Yes. Put the same file in a [block](/rc/learn/blocks)'s `vendor_ui` entry under `src/widgets/`; installing the block adds the button with no wiring.
|
|
98
|
+
</Accordion>
|
|
99
|
+
<Accordion title="Can I render more than a button?">
|
|
100
|
+
The zone renders any React component — a badge, an action menu, a whole section. You have the full order in `data`.
|
|
101
|
+
</Accordion>
|
|
102
|
+
</AccordionGroup>
|
|
103
|
+
|
|
104
|
+
## Next steps
|
|
105
|
+
|
|
106
|
+
<CardGroup cols={2}>
|
|
107
|
+
<Card title="Add a widget" href="/rc/resources/tutorials/add-a-widget">
|
|
108
|
+
The general widget model and the full list of zones.
|
|
109
|
+
</Card>
|
|
110
|
+
<Card title="Extend forms and tables" href="/rc/resources/tutorials/extend-forms-and-tables">
|
|
111
|
+
Add validated fields and columns with defineCustomFieldsConfig.
|
|
112
|
+
</Card>
|
|
113
|
+
</CardGroup>
|
|
@@ -6,7 +6,7 @@ description: "Add a page to the vendor portal with file-based routing — the da
|
|
|
6
6
|
Adding a page to the admin panel or vendor portal takes one file. The dashboard SDK scans `src/routes/` at build time, registers the route, and — if you export a `config` — adds it to the sidebar with a label and icon. No route table, no registration call.
|
|
7
7
|
|
|
8
8
|
<Info>
|
|
9
|
-
**Adding vs changing.** This tutorial *adds* a brand-new page — the right move for new features. To *change* an existing page, don't rebuild it:
|
|
9
|
+
**Adding vs changing.** This tutorial *adds* a brand-new page — the right move for new features. To *change* an existing page, don't rebuild it: add a [widget](/rc/resources/tutorials/add-a-widget) or a [custom field](/rc/resources/tutorials/extend-forms-and-tables) to inject into it. The [decision guide](/rc/resources/customization/extending-panels#choosing-your-extension-mechanism) compares every extension mechanism.
|
|
10
10
|
</Info>
|
|
11
11
|
|
|
12
12
|
## What you'll build
|
|
@@ -73,6 +73,6 @@ A `/reviews` page in the vendor portal with a sidebar entry — from a single fi
|
|
|
73
73
|
## Next steps
|
|
74
74
|
|
|
75
75
|
<CardGroup cols={2}>
|
|
76
|
-
<Card title="
|
|
76
|
+
<Card title="Add a widget" href="/rc/resources/tutorials/add-a-widget" />
|
|
77
77
|
<Card title="Add a custom API route" href="/rc/resources/tutorials/custom-api-route" />
|
|
78
78
|
</CardGroup>
|