@mercurjs/docs 2.2.0-canary.40 → 2.2.0-canary.41
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/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/custom-panel-page.mdx +2 -2
- package/content/resources/tutorials/customize-navigation.mdx +111 -0
- package/content/resources/tutorials/extend-forms-and-tables.mdx +189 -0
- package/content/resources/tutorials/import-export-products.mdx +3 -3
- package/llms.txt +4 -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
|
@@ -77,7 +77,7 @@ The [Mercur CLI](/rc/tools/cli) (`@mercurjs/cli@rc`) manages your project — sc
|
|
|
77
77
|
|
|
78
78
|
## Dashboard SDK
|
|
79
79
|
|
|
80
|
-
The [Dashboard SDK](/rc/tools/dashboard-sdk) (`@mercurjs/dashboard-sdk`) is a Vite plugin shared by the admin panel and vendor portal. It provides file-based routing, automatic navigation generation,
|
|
80
|
+
The [Dashboard SDK](/rc/tools/dashboard-sdk) (`@mercurjs/dashboard-sdk`) is a Vite plugin shared by the admin panel and vendor portal. It provides file-based routing, automatic navigation generation, widget and custom-field extensions, and i18n support.
|
|
81
81
|
|
|
82
82
|
## API Client
|
|
83
83
|
|
|
@@ -9,6 +9,10 @@ Custom Fields let you attach extra data to any existing Medusa entity — produc
|
|
|
9
9
|
**Where this sits in the stack.** Custom Fields is a Mercur module (`@mercurjs/core/modules/custom-fields`) layered on top of Medusa's standard module-link system — it generates the side table, the link, and the schema for you. It's the lightest way to add data to an entity.
|
|
10
10
|
</Info>
|
|
11
11
|
|
|
12
|
+
<Note>
|
|
13
|
+
**This is the *data* layer — not the panel UI.** This module *stores* extra data. The panel-side `defineCustomFieldsConfig` helper (see [Extend forms and tables](/rc/resources/tutorials/extend-forms-and-tables)) *renders* fields into the built-in forms, sections, and tables. They share a name but solve different halves of the problem: use this module for storage and queryability, and the panel helper to expose the values to admins and vendors.
|
|
14
|
+
</Note>
|
|
15
|
+
|
|
12
16
|
## When to use Custom Fields
|
|
13
17
|
|
|
14
18
|
<Tip>
|
|
@@ -163,7 +167,7 @@ import {
|
|
|
163
167
|
Yes — edit the `customFields` config and run `db:migrate` again; the module updates the side table's schema. Removing a field from config stops exposing it; treat destructive column changes with the same care as any schema migration.
|
|
164
168
|
</Accordion>
|
|
165
169
|
<Accordion title="How do vendors or admins edit these values from the panels?">
|
|
166
|
-
Through your own UI: add a page or
|
|
170
|
+
Through your own UI: add a page or a [panel custom field](/rc/resources/customization/extending-panels#custom-fields) and call an API route that uses the service's `upsert` — or compose `upsertCustomFieldsStep` into a workflow behind a [custom route](/rc/resources/tutorials/custom-api-route).
|
|
167
171
|
</Accordion>
|
|
168
172
|
<Accordion title="Can one entity have multiple custom-field records?">
|
|
169
173
|
No — the side table has a unique constraint on the parent ID, enforcing one-to-one. If you need many records per parent, that's a custom module, not custom fields.
|
|
@@ -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
|
+
| `login.logo.*` | The logo slot on the public login screen |
|
|
74
|
+
| `login.before.*` / `login.after.*` | Around the login form (rendered before authentication) |
|
|
75
|
+
|
|
76
|
+
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.
|
|
77
|
+
|
|
78
|
+
## Verify
|
|
79
|
+
|
|
80
|
+
1. The tip banner renders above the product list.
|
|
81
|
+
2. Search, filter, and paginate the list — all built-in behavior still works.
|
|
82
|
+
3. Change the zone to `product.list.after` and reload — the banner moves below the list.
|
|
83
|
+
4. Set `zone: "not.a.zone"` — `tsc` (`bun run lint`) fails with a "not assignable to `WidgetZoneId`" error.
|
|
84
|
+
5. Delete the file — the banner disappears; nothing else changed.
|
|
85
|
+
|
|
86
|
+
## FAQ
|
|
87
|
+
|
|
88
|
+
<AccordionGroup>
|
|
89
|
+
<Accordion title="Can a widget target more than one zone?">
|
|
90
|
+
Yes — `zone` accepts an array (`zone: ["product.list.before", "login.after.before"]`), and the same component renders at each.
|
|
91
|
+
</Accordion>
|
|
92
|
+
<Accordion title="Can a block ship widgets?">
|
|
93
|
+
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.
|
|
94
|
+
</Accordion>
|
|
95
|
+
<Accordion title="Does the admin panel have widget zones too?">
|
|
96
|
+
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.
|
|
97
|
+
</Accordion>
|
|
98
|
+
</AccordionGroup>
|
|
99
|
+
|
|
100
|
+
## Next steps
|
|
101
|
+
|
|
102
|
+
<CardGroup cols={2}>
|
|
103
|
+
<Card title="Extend forms and tables" href="/rc/resources/tutorials/extend-forms-and-tables">
|
|
104
|
+
Add validated fields and columns with defineCustomFieldsConfig.
|
|
105
|
+
</Card>
|
|
106
|
+
<Card title="Customize navigation" href="/rc/resources/tutorials/customize-navigation">
|
|
107
|
+
Reorder, hide, and re-parent sidebar items.
|
|
108
|
+
</Card>
|
|
109
|
+
</CardGroup>
|
|
110
|
+
</content>
|
|
@@ -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>
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Customize navigation"
|
|
3
|
+
description: "Reorder, hide, relabel, and re-parent built-in sidebar items with a single _navigation.ts file and defineNavigationConfig."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
The built-in sidebar ships a fixed set of items — Orders, Products, Customers, and so on. To reshape them without replacing the whole sidebar, author one host-owned file: `src/_navigation.ts`. It reorders, hides, relabels, and re-parents **built-in** items, and it is the single source of truth for the sidebar's shape.
|
|
7
|
+
|
|
8
|
+
<Info>
|
|
9
|
+
**When to use this vs. a `config` export.** New pages you add via [drop-in routes](/rc/resources/tutorials/custom-panel-page) place their own sidebar item through `defineRouteConfig({ label, rank, nested })`. `_navigation.ts` is for the items you *didn't* create — the built-in ones. The two layer cleanly: custom routes place themselves; `_navigation.ts` reshapes the built-ins.
|
|
10
|
+
</Info>
|
|
11
|
+
|
|
12
|
+
## What you'll build
|
|
13
|
+
|
|
14
|
+
A vendor sidebar with Orders pinned to the top, Price Lists hidden, and Campaigns moved under Orders.
|
|
15
|
+
|
|
16
|
+
## Register the typed targets (once)
|
|
17
|
+
|
|
18
|
+
Nav item ids are typed and generated per panel. Register them once with a single ambient reference in your app's `src` (already shipped by `create-mercur-app`):
|
|
19
|
+
|
|
20
|
+
```typescript apps/vendor/src/extension-targets.d.ts
|
|
21
|
+
/// <reference types="@mercurjs/vendor/extension-targets" />
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
With it present, `id` and `nested` autocomplete and an unknown id fails `tsc`.
|
|
25
|
+
|
|
26
|
+
## Author the navigation file
|
|
27
|
+
|
|
28
|
+
<Steps>
|
|
29
|
+
<Step title="Create src/_navigation.ts">
|
|
30
|
+
The file is host-owned and underscore-prefixed. Default-export a `defineNavigationConfig` with an `items` array of overrides:
|
|
31
|
+
|
|
32
|
+
```ts apps/vendor/src/_navigation.ts
|
|
33
|
+
import { defineNavigationConfig } from "@mercurjs/dashboard-sdk"
|
|
34
|
+
|
|
35
|
+
export default defineNavigationConfig({
|
|
36
|
+
items: [
|
|
37
|
+
{ id: "orders", rank: 0 }, // pin to the top
|
|
38
|
+
{ id: "price-lists", hidden: true }, // hide from the sidebar
|
|
39
|
+
{ id: "campaigns", nested: "orders" }, // re-parent under Orders
|
|
40
|
+
],
|
|
41
|
+
})
|
|
42
|
+
```
|
|
43
|
+
</Step>
|
|
44
|
+
<Step title="Know the override fields">
|
|
45
|
+
Each entry targets one built-in item by its stable `id`:
|
|
46
|
+
|
|
47
|
+
| Field | Type | Effect |
|
|
48
|
+
|-------|------|--------|
|
|
49
|
+
| `id` | `NavItemId` | **Required.** The built-in item to override (top-level or nested) |
|
|
50
|
+
| `rank` | `number` | Order within its parent (lower first) |
|
|
51
|
+
| `hidden` | `boolean` | Remove it from the sidebar |
|
|
52
|
+
| `label` | `string` | Relabel (i18n key or literal) |
|
|
53
|
+
| `icon` | `ComponentType` | Replace its icon |
|
|
54
|
+
| `nested` | `NavParentId \| null` | Re-parent under another top-level item; `null` promotes a nested item to top level |
|
|
55
|
+
|
|
56
|
+
Both `id` and `nested` are checked against the panel's generated `NavItemRegistry` / `NavParentRegistry`.
|
|
57
|
+
</Step>
|
|
58
|
+
<Step title="Reload the panel">
|
|
59
|
+
Open the vendor portal — Orders sits at the top, Price Lists is gone from the menu, and Campaigns now appears under Orders. The route for a hidden item stays reachable directly by URL unless you also remove it.
|
|
60
|
+
</Step>
|
|
61
|
+
</Steps>
|
|
62
|
+
|
|
63
|
+
## Common recipes
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
export default defineNavigationConfig({
|
|
67
|
+
items: [
|
|
68
|
+
{ id: "payouts", label: "Settlements" }, // relabel
|
|
69
|
+
{ id: "categories", nested: null, rank: 1 }, // promote a nested item to top level
|
|
70
|
+
{ id: "collections", nested: "orders" }, // move a nested item under a different parent
|
|
71
|
+
{ id: "inventory", hidden: true }, // hide a built-in
|
|
72
|
+
],
|
|
73
|
+
})
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Verify
|
|
77
|
+
|
|
78
|
+
1. The top-level order reflects your `rank` values, with `orders` first.
|
|
79
|
+
2. `price-lists` no longer appears in the sidebar.
|
|
80
|
+
3. `campaigns` renders as a child under Orders.
|
|
81
|
+
4. Set `id: "not-an-item"` — `bun run lint` (tsc) fails against `NavItemRegistry`.
|
|
82
|
+
5. Delete `_navigation.ts` — the default sidebar returns.
|
|
83
|
+
|
|
84
|
+
## FAQ
|
|
85
|
+
|
|
86
|
+
<AccordionGroup>
|
|
87
|
+
<Accordion title="Can an installed block reorder the sidebar?">
|
|
88
|
+
No. Navigation is deliberately host-only — blocks can ship pages, widgets, and custom fields, but the sidebar order stays a single source of truth in your app's `_navigation.ts`.
|
|
89
|
+
</Accordion>
|
|
90
|
+
<Accordion title="What ids can I target?">
|
|
91
|
+
Any built-in item, top-level or nested, by its own id — e.g. `orders`, `products`, `categories`, `collections`, `campaigns`, `customer-groups`. Let your editor autocomplete `id:` against `NavItemId`; the full set is generated into your panel's `extension-targets.d.ts`.
|
|
92
|
+
</Accordion>
|
|
93
|
+
<Accordion title="Does this work in the admin panel too?">
|
|
94
|
+
Yes — drop `src/_navigation.ts` in the admin app and reference `@mercurjs/admin/extension-targets`. Each panel ships its own nav id set.
|
|
95
|
+
</Accordion>
|
|
96
|
+
<Accordion title="How do I add a brand-new sidebar item?">
|
|
97
|
+
That's a [drop-in route](/rc/resources/tutorials/custom-panel-page) with a `config` export — `_navigation.ts` only reshapes built-in items, it doesn't create routes.
|
|
98
|
+
</Accordion>
|
|
99
|
+
</AccordionGroup>
|
|
100
|
+
|
|
101
|
+
## Next steps
|
|
102
|
+
|
|
103
|
+
<CardGroup cols={2}>
|
|
104
|
+
<Card title="Add a custom panel page" href="/rc/resources/tutorials/custom-panel-page">
|
|
105
|
+
Add a new screen with its own sidebar item.
|
|
106
|
+
</Card>
|
|
107
|
+
<Card title="Add a widget" href="/rc/resources/tutorials/add-a-widget">
|
|
108
|
+
Inject a component into a built-in page.
|
|
109
|
+
</Card>
|
|
110
|
+
</CardGroup>
|
|
111
|
+
</content>
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Extend forms and tables"
|
|
3
|
+
description: "Add validated fields to built-in forms, replace fields in detail sections, and add list columns with defineCustomFieldsConfig and createFormHelper."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
`defineCustomFieldsConfig` is Mercur's model-scoped extension surface: from one file per model, you add validated fields to built-in create/edit forms, replace/remove/add fields in detail sections, and add columns to the list table — all wired into the built-in page, all typed against the model's generated registry.
|
|
7
|
+
|
|
8
|
+
<Info>
|
|
9
|
+
**UI, not schema.** This helper is a *panel* surface — it renders, validates, and displays fields. It does not create database columns. To *store* extra data, use the backend [Custom Fields module](/rc/resources/customization/custom-fields) or 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`.
|
|
10
|
+
</Info>
|
|
11
|
+
|
|
12
|
+
## What you'll build
|
|
13
|
+
|
|
14
|
+
An `ERP ID` field on the vendor product edit form, shown in the product's detail section and as a list-table column — from a single `src/custom-fields/product.tsx`.
|
|
15
|
+
|
|
16
|
+
## Register the typed targets (once)
|
|
17
|
+
|
|
18
|
+
Models, form zones, display zones, and built-in field ids are typed per panel. Register them once (shipped by `create-mercur-app`):
|
|
19
|
+
|
|
20
|
+
```typescript apps/vendor/src/extension-targets.d.ts
|
|
21
|
+
/// <reference types="@mercurjs/vendor/extension-targets" />
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Build the config
|
|
25
|
+
|
|
26
|
+
<Steps>
|
|
27
|
+
<Step title="Create the model file">
|
|
28
|
+
Drop `src/custom-fields/<model>.tsx` and default-export a `defineCustomFieldsConfig`. `createFormHelper` (from `@mercurjs/dashboard-shared`) turns a Zod schema into an input type + validation:
|
|
29
|
+
|
|
30
|
+
```tsx apps/vendor/src/custom-fields/product.tsx
|
|
31
|
+
import { defineCustomFieldsConfig } from "@mercurjs/dashboard-sdk"
|
|
32
|
+
import { createFormHelper } from "@mercurjs/dashboard-shared"
|
|
33
|
+
|
|
34
|
+
type ProductWithMeta = { metadata?: Record<string, unknown> }
|
|
35
|
+
const form = createFormHelper<ProductWithMeta>()
|
|
36
|
+
|
|
37
|
+
export default defineCustomFieldsConfig({
|
|
38
|
+
model: "product",
|
|
39
|
+
forms: [
|
|
40
|
+
{
|
|
41
|
+
zone: "edit",
|
|
42
|
+
fields: {
|
|
43
|
+
erp_id: form.define({
|
|
44
|
+
validation: form.string().optional(),
|
|
45
|
+
label: "ERP ID",
|
|
46
|
+
description: "External system identifier",
|
|
47
|
+
placeholder: "ERP-000",
|
|
48
|
+
defaultValue: (data) => (data?.metadata?.erp_id as string) ?? "",
|
|
49
|
+
}),
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
})
|
|
54
|
+
```
|
|
55
|
+
</Step>
|
|
56
|
+
<Step title="Add a detail-section display">
|
|
57
|
+
`displays[]` targets a detail-page section by its `zone` id. Keyed by field `id`, an entry **adds**, **replaces**, or **removes** a field:
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
import { Text } from "@medusajs/ui"
|
|
61
|
+
|
|
62
|
+
// ...inside defineCustomFieldsConfig:
|
|
63
|
+
displays: [
|
|
64
|
+
{
|
|
65
|
+
zone: "general",
|
|
66
|
+
fields: [
|
|
67
|
+
// ADD — an unknown id appends a new read-only row
|
|
68
|
+
{
|
|
69
|
+
id: "erp_id",
|
|
70
|
+
component: ({ data }) => (
|
|
71
|
+
<Text size="small" className="text-ui-fg-subtle px-6 py-4">
|
|
72
|
+
ERP ID: {String(data?.metadata?.erp_id ?? "-")}
|
|
73
|
+
</Text>
|
|
74
|
+
),
|
|
75
|
+
},
|
|
76
|
+
// REMOVE — a built-in id + null hides the field
|
|
77
|
+
{ id: "subtitle", component: null },
|
|
78
|
+
// REPLACE — a built-in id + component overrides its render
|
|
79
|
+
{
|
|
80
|
+
id: "handle",
|
|
81
|
+
component: ({ data }) => (
|
|
82
|
+
<Text size="small" className="text-ui-fg-subtle px-6 py-4">
|
|
83
|
+
/{(data as { handle?: string })?.handle}
|
|
84
|
+
</Text>
|
|
85
|
+
),
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Built-in field ids (like `subtitle`, `handle`, `status`, `title`) autocomplete from the panel's generated `CustomFieldsRegistry`; an unknown id is treated as an added row.
|
|
93
|
+
</Step>
|
|
94
|
+
<Step title="Add a list column">
|
|
95
|
+
The `list` block extends the model's list table — add or override columns by id, hide built-in columns, and reorder:
|
|
96
|
+
|
|
97
|
+
```tsx
|
|
98
|
+
// ...inside defineCustomFieldsConfig:
|
|
99
|
+
list: {
|
|
100
|
+
columns: [
|
|
101
|
+
{
|
|
102
|
+
id: "erp_id",
|
|
103
|
+
header: "ERP",
|
|
104
|
+
component: ({ row }) => String(row.metadata?.erp_id ?? "-"),
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
viewDefaults: {
|
|
108
|
+
columnVisibility: { collection: false }, // hide a built-in column
|
|
109
|
+
columnOrder: ["product", "erp_id", "status"], // reorder
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
```
|
|
113
|
+
</Step>
|
|
114
|
+
<Step title="Reload the panel">
|
|
115
|
+
Open the vendor portal. The product **edit** drawer shows the ERP ID field (validated on submit and persisted via `additional_data`), the detail **general** section shows the ERP ID row (with `subtitle` removed and `handle` re-rendered), and the product **list** shows the ERP column.
|
|
116
|
+
</Step>
|
|
117
|
+
</Steps>
|
|
118
|
+
|
|
119
|
+
## The `createFormHelper` surface
|
|
120
|
+
|
|
121
|
+
`createFormHelper<T>()` exposes a Zod-based surface that drives both the input type and its validation:
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
const form = createFormHelper<T>()
|
|
125
|
+
|
|
126
|
+
form.define({
|
|
127
|
+
validation: form.string().min(1), // string | number | boolean | date | array | object | null | nullable | coerce
|
|
128
|
+
label: "…",
|
|
129
|
+
description: "…",
|
|
130
|
+
placeholder: "…",
|
|
131
|
+
defaultValue: "" | ((data) => /* derive from the entity */),
|
|
132
|
+
component, // optional custom render component
|
|
133
|
+
})
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Fields render through the standard `Form.Field → Form.Item` chain (never a raw `Controller`) and participate in the existing `TabbedForm` / `RouteDrawer` submit and validation flow.
|
|
137
|
+
|
|
138
|
+
## Linked-module data
|
|
139
|
+
|
|
140
|
+
To read data from a linked module alongside the entity, declare it with `link`; those relations are fetched with the entity and become available to columns and displays:
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
export default defineCustomFieldsConfig({
|
|
144
|
+
model: "product",
|
|
145
|
+
link: "brand", // or ["brand", "warranty"]
|
|
146
|
+
list: {
|
|
147
|
+
columns: [{ id: "brand_name", header: "Brand", component: ({ row }) => row.brand?.name }],
|
|
148
|
+
},
|
|
149
|
+
})
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The SDK derives the fetch query from `link` and merges it into the built-in query with the `+`/`-` convention — you never hand-write the field list.
|
|
153
|
+
|
|
154
|
+
## Verify
|
|
155
|
+
|
|
156
|
+
1. The ERP ID field renders in the product edit drawer and validates on submit.
|
|
157
|
+
2. Saving persists the value (visible on reload) via `additional_data` → `metadata`.
|
|
158
|
+
3. The detail general section shows the ERP row, hides `subtitle`, and re-renders `handle`.
|
|
159
|
+
4. The product list shows the ERP column, hides `collection`, and reorders columns.
|
|
160
|
+
5. Set `zone: "nope"` in `forms` — `bun run lint` (tsc) fails against the model's registry.
|
|
161
|
+
|
|
162
|
+
## FAQ
|
|
163
|
+
|
|
164
|
+
<AccordionGroup>
|
|
165
|
+
<Accordion title="Which models and zones are available?">
|
|
166
|
+
Today: the `product` model in the vendor portal, with form zone `edit` and display zone `general`. The valid set per panel is generated into `CustomFieldsRegistry` in `extension-targets.d.ts` — autocomplete `model` / `zone` to see what's mounted.
|
|
167
|
+
</Accordion>
|
|
168
|
+
<Accordion title="Where is the value actually stored?">
|
|
169
|
+
In the MVP, product custom fields are submitted under `additional_data` and persisted onto `product.metadata`. `defineCustomFieldsConfig` itself doesn't create a column — for durable, queryable storage model it with the backend [Custom Fields module](/rc/resources/customization/custom-fields) or a custom route/workflow.
|
|
170
|
+
</Accordion>
|
|
171
|
+
<Accordion title="Can I extend the onboarding wizard?">
|
|
172
|
+
That's the same helper with `zone: "onboarding"` and `tab` set to a wizard step id (vendor only). It's designed but not mounted in the current MVP — the runtime host exists; the wizard mount is a follow-up.
|
|
173
|
+
</Accordion>
|
|
174
|
+
<Accordion title="Can a block ship custom fields?">
|
|
175
|
+
Yes — a [block](/rc/learn/blocks) can include `src/custom-fields/` files in its `vendor_ui` / `admin_ui` entry, aggregated like the host app's.
|
|
176
|
+
</Accordion>
|
|
177
|
+
</AccordionGroup>
|
|
178
|
+
|
|
179
|
+
## Next steps
|
|
180
|
+
|
|
181
|
+
<CardGroup cols={2}>
|
|
182
|
+
<Card title="Custom Fields module" href="/rc/resources/customization/custom-fields">
|
|
183
|
+
Persist extra data on an entity with a generated side table.
|
|
184
|
+
</Card>
|
|
185
|
+
<Card title="Add a widget" href="/rc/resources/tutorials/add-a-widget">
|
|
186
|
+
Inject a component at a built-in zone.
|
|
187
|
+
</Card>
|
|
188
|
+
</CardGroup>
|
|
189
|
+
</content>
|
|
@@ -6,7 +6,7 @@ description: "Install the product-import-export block and give vendors CSV impor
|
|
|
6
6
|
Bulk catalog operations belong to vendors, not the operator — a seller migrating from another platform needs to bring hundreds of listings with them. The `product-import-export` block adds CSV import and export to the Vendor Portal: API routes, workflows built on Medusa's product CSV steps, and drawer UIs wired into the product list.
|
|
7
7
|
|
|
8
8
|
<Info>
|
|
9
|
-
**This block is also a masterclass in Mercur's extension model.** It ships backend workflows and routes into your API package, and it
|
|
9
|
+
**This block is also a masterclass in Mercur's extension model.** It ships backend workflows and routes into your API package, and it adds Import/Export actions to the vendor products area through the panel extension surface. After installing, read its source; you own every file.
|
|
10
10
|
</Info>
|
|
11
11
|
|
|
12
12
|
## What you'll build
|
|
@@ -87,8 +87,8 @@ A vendor product list with **Import** and **Export** header buttons, a working C
|
|
|
87
87
|
## Next steps
|
|
88
88
|
|
|
89
89
|
<CardGroup cols={2}>
|
|
90
|
-
<Card title="
|
|
91
|
-
|
|
90
|
+
<Card title="Add a widget" href="/rc/resources/tutorials/add-a-widget">
|
|
91
|
+
Inject a component at a built-in zone with `defineWidgetConfig`.
|
|
92
92
|
</Card>
|
|
93
93
|
<Card title="Build your own block" href="/rc/resources/tutorials/build-a-block">
|
|
94
94
|
Package a feature like this one and publish it to your own registry.
|
package/llms.txt
CHANGED
|
@@ -30,23 +30,24 @@ package (`node_modules/@mercurjs/docs/`).
|
|
|
30
30
|
- [Building with AI](content/resources/ai/overview.mdx) — Mercur ships version-matched docs inside your project so AI coding agents build from accurate APIs instead of stale training data.
|
|
31
31
|
- [Custom Fields](content/resources/customization/custom-fields.mdx) — Extend any Medusa entity with additional fields without modifying core code.
|
|
32
32
|
- [Extend a workflow](content/resources/customization/extend-a-workflow.mdx) — Inject custom logic into an existing Mercur workflow through hooks — without rewriting it.
|
|
33
|
-
- [Extending Panels](content/resources/customization/extending-panels.mdx) — Add pages, customize navigation,
|
|
33
|
+
- [Extending Panels](content/resources/customization/extending-panels.mdx) — Add pages, inject widgets, customize navigation, and extend forms and tables in the admin and vendor panels.
|
|
34
34
|
- [Medusa Cloud deployment](content/resources/deployment/medusa-cloud.mdx) — Deploy Mercur — backend, admin panel, and vendor panel — on Medusa Cloud.
|
|
35
35
|
- [Notifications](content/resources/integrations/notifications.mdx) — Send transactional emails and marketplace notifications with Resend.
|
|
36
36
|
- [Search](content/resources/integrations/search.mdx) — Mercur's provider-agnostic search module — zero-infrastructure Orama by default, swappable for Algolia, Meilisearch, or your own provider.
|
|
37
37
|
- [Stripe Connect Integration](content/resources/integrations/stripe-connect.mdx) — Set up Stripe Connect for marketplace payments and seller payouts — from Stripe Dashboard configuration to the full end-to-end payment lifecycle.
|
|
38
38
|
- [Add a feature with a block](content/resources/tutorials/add-a-block.mdx) — Install the reviews block end-to-end and see it live across the admin, vendor, and storefront surfaces.
|
|
39
|
+
- [Add a widget](content/resources/tutorials/add-a-widget.mdx) — Inject a React component at a named zone on a built-in panel page with defineWidgetConfig — no forking.
|
|
39
40
|
- [Create attributes and variant axes](content/resources/tutorials/attributes-and-variant-axes.mdx) — Build the attribute catalog: a filterable attribute, a variant axis backed by a native product option, and an inline product-scoped axis.
|
|
40
41
|
- [Build your own block](content/resources/tutorials/build-a-block.mdx) — Author a reusable feature as a block — backend, panel UI, and docs — build it into a registry, and install it into any Mercur project.
|
|
41
42
|
- [Configure commissions](content/resources/tutorials/configure-commissions.mdx) — Set the global commission, add scoped rules for categories and sellers, and confirm the right rate lands on an order.
|
|
42
43
|
- [Add a custom API route](content/resources/tutorials/custom-api-route.mdx) — Create a backend endpoint, regenerate the route map, and call it from a panel page with full type safety end to end.
|
|
43
44
|
- [Add a custom panel page](content/resources/tutorials/custom-panel-page.mdx) — Add a page to the vendor portal with file-based routing — the dashboard SDK wires it in automatically.
|
|
45
|
+
- [Customize navigation](content/resources/tutorials/customize-navigation.mdx) — Reorder, hide, relabel, and re-parent built-in sidebar items with a single _navigation.ts file and defineNavigationConfig.
|
|
46
|
+
- [Extend forms and tables](content/resources/tutorials/extend-forms-and-tables.mdx) — Add validated fields to built-in forms, replace fields in detail sections, and add list columns with defineCustomFieldsConfig and createFormHelper.
|
|
44
47
|
- [Build your first marketplace](content/resources/tutorials/first-marketplace.mdx) — Go end-to-end: create a project, approve a seller, list a product, place a split order, and watch a payout settle.
|
|
45
48
|
- [Handle product requests](content/resources/tutorials/handle-product-requests.mdx) — Walk a vendor's product submission and a follow-up edit through the operator review pipeline — confirm, decline, or send back.
|
|
46
49
|
- [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.
|
|
47
50
|
- [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.
|
|
48
|
-
- [Re-compose a built-in page](content/resources/tutorials/recompose-a-page.mdx) — Override a built-in panel page by dropping a route file at the same path and re-composing its compound component slots.
|
|
49
|
-
- [Replace panel components](content/resources/tutorials/replace-panel-components.mdx) — Swap the sidebar, topbar actions, or store setup screen for your own components through dashboard SDK configuration.
|
|
50
51
|
- [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.
|
|
51
52
|
|
|
52
53
|
## Tools — CLI, API client, dashboard SDK
|
package/package.json
CHANGED
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: "Re-compose a built-in page"
|
|
3
|
-
description: "Override a built-in panel page by dropping a route file at the same path and re-composing its compound component slots."
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
Every page in the admin and vendor panels is exported as a **compound component** — a root plus named slots like `Header`, `HeaderActions`, and `DataTable`. That means you never fork a page to change one part of it: you drop a `page.tsx` at the same route path, render the original page, and swap only the slot you care about. Everything you don't touch keeps its default behavior — data fetching, filters, pagination, i18n, all of it.
|
|
7
|
-
|
|
8
|
-
<Info>
|
|
9
|
-
**This is not Medusa's widget system.** Medusa's admin lets you inject widgets into predefined zones around a page. Mercur takes a different approach: pages are React compound components you re-compose directly. There are no injection zones and no `defineWidgetConfig` — you get the actual page component and decide what renders inside it. See [Extending Panels](/rc/resources/customization/extending-panels) for the full comparison.
|
|
10
|
-
</Info>
|
|
11
|
-
|
|
12
|
-
## What you'll build
|
|
13
|
-
|
|
14
|
-
A vendor portal `/products` page with **Import** and **Export** buttons added next to the built-in Create button — while the table, search, filters, and everything else stay exactly as shipped. This is the same technique the official `product-import-export` block uses.
|
|
15
|
-
|
|
16
|
-
## Slot anatomy
|
|
17
|
-
|
|
18
|
-
Built-in pages are exported from the `/pages` subpath of each panel package:
|
|
19
|
-
|
|
20
|
-
```typescript
|
|
21
|
-
import { ProductListPage } from "@mercurjs/vendor/pages" // vendor portal
|
|
22
|
-
import { CustomerListPage } from "@mercurjs/admin/pages" // admin panel
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
Each list page exposes the same family of slots:
|
|
26
|
-
|
|
27
|
-
| Slot | Renders |
|
|
28
|
-
|------|---------|
|
|
29
|
-
| `Table` | The `Container` shell holding header + table |
|
|
30
|
-
| `Header` | The title/actions row |
|
|
31
|
-
| `HeaderTitle` | Heading and subtitle |
|
|
32
|
-
| `HeaderActions` | The action button cluster |
|
|
33
|
-
| `HeaderCreateButton` | The built-in Create button |
|
|
34
|
-
| `DataTable` | The wired data table — fetching, columns, filters, pagination |
|
|
35
|
-
|
|
36
|
-
Detail pages expose section slots instead (e.g. `CustomerDetailPage.Main`, `CustomerDetailPage.Sidebar`, `CustomerDetailPage.MainGeneralSection`).
|
|
37
|
-
|
|
38
|
-
## Override the page
|
|
39
|
-
|
|
40
|
-
<Steps>
|
|
41
|
-
<Step title="Drop a route file at the same path">
|
|
42
|
-
Create the file in your vendor app at the path that matches the built-in route. `/products` maps to `src/routes/products/page.tsx`:
|
|
43
|
-
|
|
44
|
-
```tsx apps/vendor/src/routes/products/page.tsx
|
|
45
|
-
import { Link } from "react-router-dom"
|
|
46
|
-
import { Button } from "@medusajs/ui"
|
|
47
|
-
import { ArrowDownTray, ArrowUpTray } from "@medusajs/icons"
|
|
48
|
-
import { ProductListPage } from "@mercurjs/vendor/pages"
|
|
49
|
-
|
|
50
|
-
export default function ProductsWithImportExport() {
|
|
51
|
-
return (
|
|
52
|
-
<ProductListPage>
|
|
53
|
-
<ProductListPage.Table>
|
|
54
|
-
<ProductListPage.Header>
|
|
55
|
-
<ProductListPage.HeaderTitle />
|
|
56
|
-
<ProductListPage.HeaderActions>
|
|
57
|
-
<Button size="small" variant="secondary" asChild>
|
|
58
|
-
<Link to="import">
|
|
59
|
-
<ArrowUpTray />
|
|
60
|
-
Import
|
|
61
|
-
</Link>
|
|
62
|
-
</Button>
|
|
63
|
-
<Button size="small" variant="secondary" asChild>
|
|
64
|
-
<Link to="export">
|
|
65
|
-
<ArrowDownTray />
|
|
66
|
-
Export
|
|
67
|
-
</Link>
|
|
68
|
-
</Button>
|
|
69
|
-
<ProductListPage.HeaderCreateButton />
|
|
70
|
-
</ProductListPage.HeaderActions>
|
|
71
|
-
</ProductListPage.Header>
|
|
72
|
-
<ProductListPage.DataTable />
|
|
73
|
-
</ProductListPage.Table>
|
|
74
|
-
</ProductListPage>
|
|
75
|
-
)
|
|
76
|
-
}
|
|
77
|
-
```
|
|
78
|
-
</Step>
|
|
79
|
-
<Step title="Reuse the slots you don't change">
|
|
80
|
-
Note what happened in that file: `HeaderTitle`, `HeaderCreateButton`, and `DataTable` are the originals, rendered untouched. Only `HeaderActions` gained two buttons. You re-compose top-down — render the page, re-declare only the branch you're changing, and keep original slots for everything inside it.
|
|
81
|
-
</Step>
|
|
82
|
-
<Step title="Reload the panel">
|
|
83
|
-
The dashboard SDK picks the file up automatically (adding or removing a route file triggers a full reload in dev). Because the path matches a built-in route, **your page replaces the built-in one** — no configuration, no registration.
|
|
84
|
-
</Step>
|
|
85
|
-
</Steps>
|
|
86
|
-
|
|
87
|
-
<Note>
|
|
88
|
-
When a custom route's path matches a built-in route, your page replaces the built-in one. Routes at new paths are appended instead. That one rule is the entire override mechanism.
|
|
89
|
-
</Note>
|
|
90
|
-
|
|
91
|
-
## How defaults work
|
|
92
|
-
|
|
93
|
-
If you render a compound page with **no children**, it renders its full default composition — so `<ProductListPage />` alone is identical to the built-in page. Every page root follows the pattern:
|
|
94
|
-
|
|
95
|
-
```tsx
|
|
96
|
-
Children.count(children) > 0 ? children : <DefaultComposition />
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
## Verify
|
|
100
|
-
|
|
101
|
-
1. Run your project (`bun run dev`) and open the vendor portal.
|
|
102
|
-
2. Navigate to **Products** — the list should look identical to before, plus Import and Export buttons in the header.
|
|
103
|
-
3. Search, filter, and paginate the table — all built-in behavior must still work, since `DataTable` is untouched.
|
|
104
|
-
4. Delete your `src/routes/products/page.tsx` and reload — the built-in page comes back. Nothing was forked.
|
|
105
|
-
|
|
106
|
-
## FAQ
|
|
107
|
-
|
|
108
|
-
<AccordionGroup>
|
|
109
|
-
<Accordion title="Where do I find which slots a page exposes?">
|
|
110
|
-
Import the page from `@mercurjs/vendor/pages` or `@mercurjs/admin/pages` and let your editor's autocomplete list the attached members — every slot is a static property on the page component. The naming is consistent across pages: list pages expose `Table`/`Header`/`HeaderTitle`/`HeaderActions`/`HeaderCreateButton`/`DataTable`; detail pages expose `Main`/`Sidebar` plus one property per section.
|
|
111
|
-
</Accordion>
|
|
112
|
-
<Accordion title="Can I remove a built-in element, like the Create button?">
|
|
113
|
-
Yes — re-composition is declarative. Anything you don't render doesn't appear: re-declare `HeaderActions` with only your own buttons and omit `HeaderCreateButton`.
|
|
114
|
-
</Accordion>
|
|
115
|
-
<Accordion title="What about changing the table columns?">
|
|
116
|
-
`DataTable` is a single slot — you either keep it wholesale or replace it with your own table. For a different column set, replace the slot with your own component built on the shared `DataTable`/`useDataTable` primitives from the panel package.
|
|
117
|
-
</Accordion>
|
|
118
|
-
</AccordionGroup>
|
|
119
|
-
|
|
120
|
-
## Next steps
|
|
121
|
-
|
|
122
|
-
<CardGroup cols={2}>
|
|
123
|
-
<Card title="Replace panel components" href="/rc/resources/tutorials/replace-panel-components">
|
|
124
|
-
Swap global chrome like the sidebar or topbar instead of a single page.
|
|
125
|
-
</Card>
|
|
126
|
-
<Card title="Add a custom API route" href="/rc/resources/tutorials/custom-api-route">
|
|
127
|
-
Back your custom page with a typed endpoint of your own.
|
|
128
|
-
</Card>
|
|
129
|
-
</CardGroup>
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: "Replace panel components"
|
|
3
|
-
description: "Swap the sidebar, topbar actions, or store setup screen for your own components through dashboard SDK configuration."
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
Some customizations aren't about one page — they change the panel's global chrome. For those, the dashboard SDK exposes a small set of **named component overrides**: you point the plugin config at your own file, and the panel renders your component in place of the built-in one everywhere it appears.
|
|
7
|
-
|
|
8
|
-
<Info>
|
|
9
|
-
**Choose the right tool.** A component override replaces one of four global layout slots across the whole panel. If you want to change a single page, [re-compose it](/rc/resources/tutorials/recompose-a-page) instead — and if you want to add a new screen, just [drop in a route](/rc/resources/tutorials/custom-panel-page). The decision guide in [Extending Panels](/rc/resources/customization/extending-panels#choosing-your-extension-mechanism) compares all three.
|
|
10
|
-
</Info>
|
|
11
|
-
|
|
12
|
-
## What you'll build
|
|
13
|
-
|
|
14
|
-
A vendor portal with custom topbar actions (a help link and a status badge) and a custom main sidebar, configured entirely from `vite.config.ts`.
|
|
15
|
-
|
|
16
|
-
## The available slots
|
|
17
|
-
|
|
18
|
-
Exactly four layout components can be replaced:
|
|
19
|
-
|
|
20
|
-
| Component | Where it renders |
|
|
21
|
-
|-----------|------------------|
|
|
22
|
-
| `MainSidebar` | Primary navigation sidebar on every main page |
|
|
23
|
-
| `SettingsSidebar` | Sidebar of the `/settings` section |
|
|
24
|
-
| `TopbarActions` | Action cluster on the right side of the top bar |
|
|
25
|
-
| `StoreSetup` | The store setup screen shown to new sellers (vendor portal) |
|
|
26
|
-
|
|
27
|
-
Anything else — a page, a section, a table — is customized through routes and compound components, not through overrides.
|
|
28
|
-
|
|
29
|
-
## Set up the override
|
|
30
|
-
|
|
31
|
-
<Steps>
|
|
32
|
-
<Step title="Write the replacement component">
|
|
33
|
-
Create the component anywhere under `src/`. It must have a **default export**:
|
|
34
|
-
|
|
35
|
-
```tsx apps/vendor/src/components/topbar-actions.tsx
|
|
36
|
-
import { Badge, Button } from "@medusajs/ui"
|
|
37
|
-
|
|
38
|
-
export default function TopbarActions() {
|
|
39
|
-
return (
|
|
40
|
-
<div className="flex items-center gap-x-2">
|
|
41
|
-
<Badge size="2xsmall" color="green">
|
|
42
|
-
All systems operational
|
|
43
|
-
</Badge>
|
|
44
|
-
<Button size="small" variant="transparent" asChild>
|
|
45
|
-
<a href="https://help.example.com" target="_blank" rel="noreferrer">
|
|
46
|
-
Help
|
|
47
|
-
</a>
|
|
48
|
-
</Button>
|
|
49
|
-
</div>
|
|
50
|
-
)
|
|
51
|
-
}
|
|
52
|
-
```
|
|
53
|
-
</Step>
|
|
54
|
-
<Step title="Register it in the Vite config">
|
|
55
|
-
Component overrides live in the `components` option of `mercurDashboardPlugin`, in your panel app's `vite.config.ts`. Paths are **relative to `src/`**:
|
|
56
|
-
|
|
57
|
-
```typescript apps/vendor/vite.config.ts
|
|
58
|
-
import { defineConfig } from 'vite'
|
|
59
|
-
import react from '@vitejs/plugin-react'
|
|
60
|
-
import { mercurDashboardPlugin } from '@mercurjs/dashboard-sdk'
|
|
61
|
-
|
|
62
|
-
export default defineConfig({
|
|
63
|
-
plugins: [
|
|
64
|
-
react(),
|
|
65
|
-
mercurDashboardPlugin({
|
|
66
|
-
medusaConfigPath: '../../packages/api/medusa-config.ts',
|
|
67
|
-
components: {
|
|
68
|
-
TopbarActions: 'components/topbar-actions',
|
|
69
|
-
MainSidebar: 'components/main-sidebar',
|
|
70
|
-
},
|
|
71
|
-
}),
|
|
72
|
-
],
|
|
73
|
-
})
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
The plugin resolves each path at build time and swaps your component in through the `virtual:mercur/components` module — no runtime registration, no context providers to wire.
|
|
77
|
-
</Step>
|
|
78
|
-
<Step title="Restart the dev server">
|
|
79
|
-
Vite config changes require a restart. After it, your components render everywhere their slots appear.
|
|
80
|
-
</Step>
|
|
81
|
-
</Steps>
|
|
82
|
-
|
|
83
|
-
<Tip>
|
|
84
|
-
Build overrides with `@medusajs/ui` components and Medusa UI color tokens so they match the rest of the panel. The panels ship no other UI library.
|
|
85
|
-
</Tip>
|
|
86
|
-
|
|
87
|
-
## Verify
|
|
88
|
-
|
|
89
|
-
1. Open the vendor portal — the top bar shows your badge and Help link on every page.
|
|
90
|
-
2. Navigate between main pages and `/settings` — the override applies everywhere its slot renders.
|
|
91
|
-
3. Remove the `components` entry and restart — the built-in components return. The originals were never modified.
|
|
92
|
-
|
|
93
|
-
## FAQ
|
|
94
|
-
|
|
95
|
-
<AccordionGroup>
|
|
96
|
-
<Accordion title="If I replace MainSidebar, do my custom routes still show up in the menu?">
|
|
97
|
-
Only if your sidebar renders them. A `MainSidebar` override replaces the **entire** navigation sidebar, including the auto-generated menu items from route `config` exports — read them from `virtual:mercur/menu-items` if you want to keep automatic navigation. If you only want to add or reorder items, you don't need an override at all: the `config` export on route files (with `rank` and `nested`) covers that.
|
|
98
|
-
</Accordion>
|
|
99
|
-
<Accordion title="Can I override other components, like the login page or a form?">
|
|
100
|
-
No — these four slots are the complete list. The login flow, forms, and everything page-level are customized through drop-in routes and [compound re-composition](/rc/resources/tutorials/recompose-a-page).
|
|
101
|
-
</Accordion>
|
|
102
|
-
<Accordion title="Do overrides apply to both panels?">
|
|
103
|
-
Each panel app has its own `vite.config.ts`, so overrides are configured per panel. `StoreSetup` only exists in the vendor portal; the other three slots exist in both.
|
|
104
|
-
</Accordion>
|
|
105
|
-
</AccordionGroup>
|
|
106
|
-
|
|
107
|
-
## Next steps
|
|
108
|
-
|
|
109
|
-
<CardGroup cols={2}>
|
|
110
|
-
<Card title="Re-compose a built-in page" href="/rc/resources/tutorials/recompose-a-page">
|
|
111
|
-
Change one page's composition instead of global chrome.
|
|
112
|
-
</Card>
|
|
113
|
-
<Card title="Extending Panels" href="/rc/resources/customization/extending-panels">
|
|
114
|
-
The full reference for routes, navigation, branding, and overrides.
|
|
115
|
-
</Card>
|
|
116
|
-
</CardGroup>
|