@mercurjs/docs 2.3.6-canary.1 → 2.3.6-canary.11
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/migration-to-2-0.mdx +1 -1
- package/content/learn/order-groups.mdx +29 -6
- package/content/learn/payouts.mdx +2 -1
- package/content/references/api/vendor/payout-accounts/create-onboarding.mdx +4 -5
- package/content/references/api/vendor/payout-accounts/create-payout-account.mdx +5 -5
- package/content/references/panel-extensions/create-page.mdx +45 -1
- package/content/references/panel-extensions/widgets.mdx +86 -2
- package/content/resources/customization/extending-panels.mdx +47 -0
- package/content/resources/tutorials/customize-navigation.mdx +80 -2
- package/llms.txt +1 -1
- package/package.json +1 -1
|
@@ -116,7 +116,7 @@ The 1.x `OrderSet` entity is renamed to `OrderGroup` in 2.x. This is a breaking
|
|
|
116
116
|
|
|
117
117
|
Two fields were removed from the group:
|
|
118
118
|
|
|
119
|
-
- **`payment_collection_id`**:
|
|
119
|
+
- **`payment_collection_id`**: orders share the cart's payment collection until the payment is captured, then each gets its own. Read `payment_collections` on the orders (see [Order Groups](/learn/order-groups)).
|
|
120
120
|
- **`sales_channel_id`**: the sales channel is stored on each individual order.
|
|
121
121
|
|
|
122
122
|
Two fields are now computed at query time rather than stored: **`seller_count`** (distinct sellers across linked orders) and **`total`** (sum of order totals).
|
|
@@ -54,16 +54,39 @@ Order groups are created automatically during checkout by the `completeCartWithS
|
|
|
54
54
|
- Each seller and the customer (if new relationship)
|
|
55
55
|
6. **Finalize** — Inventory is reserved, payment is authorized, and the `order_group.created` event is emitted
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
## Payments
|
|
58
|
+
|
|
59
|
+
The customer pays for the cart once: one payment collection on the cart, one payment at the payment provider. Medusa links a payment collection to a single order, so the orders of a cart share that payment through the cart until it is captured:
|
|
60
|
+
|
|
61
|
+
```text
|
|
62
|
+
order → cart → payment_collection → payment
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
When the cart payment is captured, it is split. Every order gets a payment collection of its own, linked to the order, with a captured payment for the order's share. That payment is on the same provider as the cart payment and points at the same charge. Shares are proportional to the order totals, rounded to the currency precision, and sum exactly to the captured amount.
|
|
59
66
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
67
|
+
```text
|
|
68
|
+
order → payment_collections → payment
|
|
69
|
+
```
|
|
63
70
|
|
|
64
|
-
|
|
71
|
+
From then on Medusa's order and payment workflows and the stock admin and vendor routes work on the order unchanged.
|
|
72
|
+
|
|
73
|
+
| Moment | What happens |
|
|
74
|
+
|--------|--------------|
|
|
75
|
+
| Cancel an order before capture | No payment is touched: the authorization is still needed by the cart's other orders. Once every order of the cart is canceled, the authorization is voided. |
|
|
76
|
+
| Capture | The cart payment is captured as a whole, by the provider webhook or from the dashboard, and split between the orders. What was charged for orders canceled earlier is refunded. |
|
|
77
|
+
| Refund an order's payment | The amount is refunded at the provider against the customer's charge and recorded on that order. It cannot exceed the order's share. |
|
|
78
|
+
| Cancel an order after capture | The order's share is refunded. |
|
|
79
|
+
|
|
80
|
+
Order reads expose whichever applies under `payment_collections`: the cart's collection before the capture, the order's own afterwards.
|
|
81
|
+
|
|
82
|
+
<Info>
|
|
83
|
+
The split, and voiding the authorization after the last cancel, run in subscribers to `payment.captured` and `order.canceled`, shortly after the request returns. A provider that captures on authorization is split during checkout.
|
|
65
84
|
</Info>
|
|
66
85
|
|
|
86
|
+
<Note>
|
|
87
|
+
Orders whose payment was captured before this model was introduced are not split retroactively and keep the cart's shared collection.
|
|
88
|
+
</Note>
|
|
89
|
+
|
|
67
90
|
## Events
|
|
68
91
|
|
|
69
92
|
| Event | Payload | When |
|
|
@@ -155,7 +155,8 @@ curl -X POST http://localhost:9000/vendor/payout-accounts/<account-id>/onboardin
|
|
|
155
155
|
-H "Authorization: Bearer <seller-token>" \
|
|
156
156
|
-H "Content-Type: application/json" \
|
|
157
157
|
-d '{
|
|
158
|
-
"
|
|
158
|
+
"data": {
|
|
159
|
+
"refresh_url": "https://my-store.com/settings/payouts",
|
|
159
160
|
"return_url": "https://my-store.com/settings/payouts"
|
|
160
161
|
}
|
|
161
162
|
}'
|
|
@@ -15,11 +15,11 @@ Creates an onboarding session with the payout provider. For Stripe Connect, the
|
|
|
15
15
|
## Body parameters
|
|
16
16
|
|
|
17
17
|
<ParamField body="data" type="object">
|
|
18
|
-
Provider-specific data forwarded when creating the onboarding.
|
|
18
|
+
Provider-specific data forwarded when creating the onboarding (e.g. `refresh_url` and `return_url` for Stripe Connect). Merged over the payout account's stored `data`.
|
|
19
19
|
</ParamField>
|
|
20
20
|
|
|
21
21
|
<ParamField body="context" type="object">
|
|
22
|
-
|
|
22
|
+
Additional context forwarded to the provider, such as an `idempotency_key`.
|
|
23
23
|
</ParamField>
|
|
24
24
|
|
|
25
25
|
## Response
|
|
@@ -40,13 +40,13 @@ curl -X POST 'http://localhost:9000/vendor/payout-accounts/pacc_01HXYZ/onboardin
|
|
|
40
40
|
-H 'Authorization: Bearer <token>' \
|
|
41
41
|
-H 'x-seller-id: <seller_id>' \
|
|
42
42
|
-H 'Content-Type: application/json' \
|
|
43
|
-
-d '{"
|
|
43
|
+
-d '{"data": {"refresh_url": "https://vendor.example.com/payouts", "return_url": "https://vendor.example.com/payouts"}}'
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
```ts JS Client
|
|
47
47
|
const { onboarding } = await client.vendor.payoutAccounts.$id.onboarding.mutate({
|
|
48
48
|
$id: "pacc_01HXYZ",
|
|
49
|
-
|
|
49
|
+
data: {
|
|
50
50
|
refresh_url: "https://vendor.example.com/payouts",
|
|
51
51
|
return_url: "https://vendor.example.com/payouts",
|
|
52
52
|
},
|
|
@@ -60,7 +60,6 @@ const { onboarding } = await client.vendor.payoutAccounts.$id.onboarding.mutate(
|
|
|
60
60
|
"onboarding": {
|
|
61
61
|
"id": "onb_01HXYZ",
|
|
62
62
|
"data": { "url": "https://connect.stripe.com/setup/s/..." },
|
|
63
|
-
"context": { "return_url": "https://vendor.example.com/payouts" },
|
|
64
63
|
"created_at": "2026-01-15T10:00:00.000Z"
|
|
65
64
|
}
|
|
66
65
|
}
|
|
@@ -9,11 +9,11 @@ Creates a payout account for the current seller through the configured payout pr
|
|
|
9
9
|
## Body parameters
|
|
10
10
|
|
|
11
11
|
<ParamField body="data" type="object">
|
|
12
|
-
Provider-specific data forwarded when creating the account.
|
|
12
|
+
Provider-specific data forwarded when creating the account (e.g. `{ "country": "US" }` for Stripe Connect). Defaults to `{}`.
|
|
13
13
|
</ParamField>
|
|
14
14
|
|
|
15
15
|
<ParamField body="context" type="object">
|
|
16
|
-
|
|
16
|
+
Additional context forwarded to the provider, such as an `idempotency_key`.
|
|
17
17
|
</ParamField>
|
|
18
18
|
|
|
19
19
|
## Response
|
|
@@ -35,12 +35,12 @@ curl -X POST 'http://localhost:9000/vendor/payout-accounts' \
|
|
|
35
35
|
-H 'Authorization: Bearer <token>' \
|
|
36
36
|
-H 'x-seller-id: <seller_id>' \
|
|
37
37
|
-H 'Content-Type: application/json' \
|
|
38
|
-
-d '{"
|
|
38
|
+
-d '{"data": {"country": "US"}}'
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
```ts JS Client
|
|
42
42
|
const { payout_account } = await client.vendor.payoutAccounts.mutate({
|
|
43
|
-
|
|
43
|
+
data: { country: "US" },
|
|
44
44
|
})
|
|
45
45
|
```
|
|
46
46
|
</RequestExample>
|
|
@@ -51,7 +51,7 @@ const { payout_account } = await client.vendor.payoutAccounts.mutate({
|
|
|
51
51
|
"payout_account": {
|
|
52
52
|
"id": "pacc_01HXYZ",
|
|
53
53
|
"status": "pending",
|
|
54
|
-
"
|
|
54
|
+
"data": { "id": "acct_1Q2w3E4r5T6y7U8i" },
|
|
55
55
|
"created_at": "2026-01-15T10:00:00.000Z"
|
|
56
56
|
}
|
|
57
57
|
}
|
|
@@ -101,6 +101,7 @@ The `config` object controls the page's sidebar entry.
|
|
|
101
101
|
| `icon` | `ComponentType` (optional) | Sidebar icon, from `@medusajs/icons`. |
|
|
102
102
|
| `rank` | `number` (optional) | Order among sibling items. Lower ranks first. |
|
|
103
103
|
| `nested` | `string` (optional) | Parent item id to nest this entry under. |
|
|
104
|
+
| `group` | `string` (optional) | Sidebar group to list the entry under, as a string literal. See [Groups](#groups). |
|
|
104
105
|
| `translationNs` | `string` (optional) | i18n namespace used to resolve `label` as a translation key. |
|
|
105
106
|
| `public` | `boolean` (optional) | Render the route before authentication under the public layout. |
|
|
106
107
|
|
|
@@ -131,7 +132,7 @@ Read the loader's result with `useLoaderData()` inside the component.
|
|
|
131
132
|
|
|
132
133
|
## Reshape built-in navigation
|
|
133
134
|
|
|
134
|
-
Use `defineNavigationConfig` to reorder, hide, relabel,
|
|
135
|
+
Use `defineNavigationConfig` to reorder, hide, relabel, re-parent, or group built-in
|
|
135
136
|
sidebar items. It lives in a single host-owned file, `src/_navigation.ts`. Blocks
|
|
136
137
|
cannot contribute navigation overrides.
|
|
137
138
|
|
|
@@ -159,6 +160,49 @@ Each entry is a `NavItemOverride`.
|
|
|
159
160
|
| `label` | `string` (optional) | i18n key or literal replacing the item's label. |
|
|
160
161
|
| `icon` | `ComponentType` (optional) | Icon component replacing the item's icon, from `@medusajs/icons`. |
|
|
161
162
|
| `nested` | `NavParentId \| null` (optional) | Re-parent under a built-in parent id. `null` promotes a nested item to top level. |
|
|
163
|
+
| `group` | `NavGroupId` (optional) | List the item under a group declared in `groups`, or a built-in settings group. |
|
|
164
|
+
|
|
165
|
+
### Groups
|
|
166
|
+
|
|
167
|
+
`groups` declares group headings by keyword, and an item's `group` assigns it to
|
|
168
|
+
one. A group renders in whichever sidebar its items live in.
|
|
169
|
+
|
|
170
|
+
The main sidebar has no built-in groups. Top-level items assigned to a declared
|
|
171
|
+
group render below the ungrouped list, under the group's heading. The settings
|
|
172
|
+
sidebar ships `general`, `developer`, and `myAccount` in the admin panel and
|
|
173
|
+
`general` in the vendor portal, which `groups` can also override by id. Settings
|
|
174
|
+
items are targeted as `settings/<segment>`.
|
|
175
|
+
|
|
176
|
+
```ts apps/admin/src/_navigation.ts
|
|
177
|
+
import { defineNavigationConfig } from "@mercurjs/dashboard-sdk"
|
|
178
|
+
|
|
179
|
+
export default defineNavigationConfig({
|
|
180
|
+
groups: [
|
|
181
|
+
{ id: "catalog", label: "Catalog" },
|
|
182
|
+
{ id: "integrations", label: "Integrations", rank: 1 },
|
|
183
|
+
{ id: "developer", hidden: true },
|
|
184
|
+
],
|
|
185
|
+
items: [
|
|
186
|
+
{ id: "products", group: "catalog" },
|
|
187
|
+
{ id: "inventory", group: "catalog" },
|
|
188
|
+
{ id: "settings/tax-regions", group: "integrations" },
|
|
189
|
+
],
|
|
190
|
+
})
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Each entry is a `NavGroupConfig`.
|
|
194
|
+
|
|
195
|
+
| Field | Type | Description |
|
|
196
|
+
| --------------- | -------------------- | --------------------------------------------------------------------------- |
|
|
197
|
+
| `id` | `string` | A new keyword that declares a group, or a built-in settings group id to override. |
|
|
198
|
+
| `label` | `string` (optional) | Heading text. A literal, or an i18n key with `translationNs`. Defaults to the `id`. |
|
|
199
|
+
| `translationNs` | `string` (optional) | i18n namespace used to resolve `label`. |
|
|
200
|
+
| `rank` | `number` (optional) | Order among groups, lower first. Defaults to declaration order. Built-in settings groups rank `0`, `1`, `2`. |
|
|
201
|
+
| `hidden` | `boolean` (optional) | Remove the group and its items from the sidebar. |
|
|
202
|
+
|
|
203
|
+
A custom page joins a group through `config.group`. An undeclared group falls
|
|
204
|
+
back to the default placement (the ungrouped list, or the first settings group),
|
|
205
|
+
and a group with no visible items isn't rendered.
|
|
162
206
|
|
|
163
207
|
Navigation overrides reshape existing items only. To add a new item, register a
|
|
164
208
|
page with a `config.label` as shown above.
|
|
@@ -70,6 +70,7 @@ finds it at build time. There is no manifest to edit and no route to register.
|
|
|
70
70
|
| ------ | -------------------------------- | ----------------------------------------------------------------------- |
|
|
71
71
|
| `zone` | `WidgetZoneId \| WidgetZoneId[]` | The zone or zones to render in. Multiple widgets in one zone stack in registration order. |
|
|
72
72
|
| `id` | `string` (optional) | A stable id. Derived from the file path at build time when omitted. |
|
|
73
|
+
| `label` | `string` (optional) | The tab label, as an i18n key or a literal. Only used by [tab zones](#tabs). |
|
|
73
74
|
|
|
74
75
|
## Component props
|
|
75
76
|
|
|
@@ -93,6 +94,89 @@ const ProductDetailNote = ({ data }: { data?: HttpTypes.AdminProduct }) => (
|
|
|
93
94
|
authentication and receive no `data`.
|
|
94
95
|
</Note>
|
|
95
96
|
|
|
97
|
+
## Tabs
|
|
98
|
+
|
|
99
|
+
Some detail pages group their content into tabs. A tab zone lets a widget add a
|
|
100
|
+
tab of its own: the widget's `label` names the tab and its component renders as
|
|
101
|
+
the tab's panel. The placement decides whether the tab sits before or after the
|
|
102
|
+
built-in ones.
|
|
103
|
+
|
|
104
|
+
```tsx apps/admin-test/src/widgets/store-loyalty-tab.tsx
|
|
105
|
+
import { defineWidgetConfig } from "@mercurjs/dashboard-sdk"
|
|
106
|
+
import { Container, Heading } from "@medusajs/ui"
|
|
107
|
+
import type { SellerDTO } from "@mercurjs/types"
|
|
108
|
+
|
|
109
|
+
const StoreLoyaltyTab = ({ data }: { data?: SellerDTO }) => (
|
|
110
|
+
<Container>
|
|
111
|
+
<Heading level="h2">Loyalty for {data?.name}</Heading>
|
|
112
|
+
</Container>
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
export const config = defineWidgetConfig({
|
|
116
|
+
zone: "stores.detail.tabs.after",
|
|
117
|
+
id: "loyalty",
|
|
118
|
+
label: "Loyalty",
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
export default StoreLoyaltyTab
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
A widget that targets a tab zone without a `label` is not shown. `permission`
|
|
125
|
+
works as on any other widget and hides the tab from actors who lack it.
|
|
126
|
+
|
|
127
|
+
| Panel | Tab zone | Page |
|
|
128
|
+
| ----- | ------------------- | ------------------------------------------------------- |
|
|
129
|
+
| Admin | `stores.detail.tabs` | Store detail, next to Orders, Offers, Users and Time off. |
|
|
130
|
+
|
|
131
|
+
## List item rows
|
|
132
|
+
|
|
133
|
+
The locations list renders one card per stock location. The `locations.list.item`
|
|
134
|
+
zone sits inside each card, so a widget there adds a row to the card instead of a
|
|
135
|
+
separate block above or below the list. `locations.list.item.before` renders
|
|
136
|
+
between the card header and the built-in rows, and `locations.list.item.after`
|
|
137
|
+
renders after them. `data` is the stock location of that card.
|
|
138
|
+
|
|
139
|
+
```tsx apps/vendor/src/widgets/location-local-delivery-row.tsx
|
|
140
|
+
import { defineWidgetConfig } from "@mercurjs/dashboard-sdk"
|
|
141
|
+
import { StatusBadge, Text } from "@medusajs/ui"
|
|
142
|
+
import type { HttpTypes } from "@mercurjs/types"
|
|
143
|
+
|
|
144
|
+
type StockLocation = HttpTypes.VendorStockLocationResponse["stock_location"]
|
|
145
|
+
|
|
146
|
+
const LocationLocalDeliveryRow = ({ data }: { data?: StockLocation }) => {
|
|
147
|
+
const enabled = data?.fulfillment_sets?.some(
|
|
148
|
+
(set) => set.type === "local_delivery"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<div className="flex items-center justify-between px-6 py-4">
|
|
153
|
+
<Text size="small" weight="plus" className="text-ui-fg-subtle flex-1">
|
|
154
|
+
Local delivery
|
|
155
|
+
</Text>
|
|
156
|
+
<div className="flex-1">
|
|
157
|
+
<StatusBadge color={enabled ? "green" : "grey"}>
|
|
158
|
+
{enabled ? "Enabled" : "Disabled"}
|
|
159
|
+
</StatusBadge>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export const config = defineWidgetConfig({
|
|
166
|
+
zone: "locations.list.item.after",
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
export default LocationLocalDeliveryRow
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Return a single row element, without a `Container` of its own. The card draws the
|
|
173
|
+
divider between rows.
|
|
174
|
+
|
|
175
|
+
| Panel | Zone | Page |
|
|
176
|
+
| ------ | --------------------- | -------------------------------------- |
|
|
177
|
+
| Vendor | `locations.list.item` | Settings → Locations, each location card. |
|
|
178
|
+
| Admin | `locations.list.item` | Settings → Locations, each location card. |
|
|
179
|
+
|
|
96
180
|
## Zone ids
|
|
97
181
|
|
|
98
182
|
A zone id reads `<domain>.<view>.<placement>`.
|
|
@@ -120,7 +204,7 @@ expands to a `.before` and an `.after` zone.
|
|
|
120
204
|
| `customer-groups` | ✓ | ✓ | | |
|
|
121
205
|
| `customers` | ✓ | ✓ | ✓ | |
|
|
122
206
|
| `inventory` | ✓ | ✓ | ✓ | |
|
|
123
|
-
| `locations` | ✓ | ✓ | ✓ |
|
|
207
|
+
| `locations` | ✓ | ✓ | ✓ | `list.item` |
|
|
124
208
|
| `offer-variants` | | ✓ | ✓ | |
|
|
125
209
|
| `offers` | ✓ | ✓ | ✓ | |
|
|
126
210
|
| `orders` | ✓ | ✓ | ✓ | `detail.summary` |
|
|
@@ -167,7 +251,7 @@ cell expands to a `.before` and an `.after` zone.
|
|
|
167
251
|
| `customer-groups` | ✓ | ✓ | | |
|
|
168
252
|
| `customers` | ✓ | ✓ | ✓ | |
|
|
169
253
|
| `inventory` | ✓ | ✓ | ✓ | |
|
|
170
|
-
| `locations` | ✓ | ✓ | ✓ |
|
|
254
|
+
| `locations` | ✓ | ✓ | ✓ | `list.item` |
|
|
171
255
|
| `marketplace` | | ✓ | | |
|
|
172
256
|
| `offer-variants` | | ✓ | ✓ | |
|
|
173
257
|
| `offers` | ✓ | ✓ | ✓ | |
|
|
@@ -151,6 +151,7 @@ export default defineNavigationConfig({
|
|
|
151
151
|
|
|
152
152
|
- `id` targets any built-in item, top-level routes or nested children, by its stable id, typed as `NavItemId`.
|
|
153
153
|
- `rank` orders an item within its parent, `hidden` removes it from the sidebar, `label` and `icon` relabel it, and `nested` re-parents it (`nested: null` promotes to top level, typed against `NavParentId`).
|
|
154
|
+
- `groups` declares group headings by keyword, and an item's `group` lists it under one, in the main sidebar or the settings sidebar (settings items are targeted as `settings/<segment>`). The built-in settings groups can be reshaped by id. A custom page picks its group with `group` in its route config.
|
|
154
155
|
- 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`.
|
|
155
156
|
|
|
156
157
|
Walk through it in [Customize navigation](/rc/resources/tutorials/customize-navigation).
|
|
@@ -242,6 +243,7 @@ For the full walkthrough (forms, sections, and list columns), see [Extend forms
|
|
|
242
243
|
| `icon` | `ComponentType` | Icon component (for example from `@medusajs/icons`) |
|
|
243
244
|
| `rank` | `number` | Sort order (lower numbers appear first) |
|
|
244
245
|
| `nested` | `string` | Parent path for nested menu items |
|
|
246
|
+
| `group` | `string` | Sidebar group declared in `_navigation.ts` to list the item under |
|
|
245
247
|
| `translationNs` | `string` | i18n namespace for the label |
|
|
246
248
|
| `public` | `boolean` | If `true`, the route is accessible without authentication |
|
|
247
249
|
|
|
@@ -270,6 +272,51 @@ File paths map to URL routes automatically.
|
|
|
270
272
|
| `src/routes/(settings)/page.tsx` | Route grouping | Groups routes without adding a URL segment |
|
|
271
273
|
| `src/routes/dashboard/@sidebar/page.tsx` | Parallel route | Renders alongside parent |
|
|
272
274
|
|
|
275
|
+
### Adding permissions from a plugin
|
|
276
|
+
|
|
277
|
+
A plugin that ships its own pages usually needs its own permission keys. Declare them on the server, then register the same keys with the dashboard SDK so pages can be guarded and hidden from navigation.
|
|
278
|
+
|
|
279
|
+
**1. Define the keys on the server.** Import the definitions file once (for example from your module's `index.ts`) so the keys join the catalog:
|
|
280
|
+
|
|
281
|
+
```typescript src/permissions.ts
|
|
282
|
+
import { defineMercurPermissions } from "@mercurjs/core/permissions/registry"
|
|
283
|
+
|
|
284
|
+
export const messagingPermissions = defineMercurPermissions([
|
|
285
|
+
{ key: "messaging", group: "messaging", surface: ["vendor"], rights: ["view", "edit", "manage"] },
|
|
286
|
+
])
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Guard the plugin's API routes with the same key (`requirePermission("messaging", "view")`).
|
|
290
|
+
|
|
291
|
+
**2. Register the keys with the dashboard SDK.** `PermissionKey` is built from the `PermissionKeys` interface, so augment it:
|
|
292
|
+
|
|
293
|
+
```typescript src/permissions.d.ts
|
|
294
|
+
declare module "@mercurjs/dashboard-sdk" {
|
|
295
|
+
interface PermissionKeys {
|
|
296
|
+
messaging: true
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export {}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
The key now type-checks everywhere the SDK accepts a permission:
|
|
304
|
+
|
|
305
|
+
```tsx src/routes/messages/page.tsx
|
|
306
|
+
import type { RouteConfig, RouteHandle } from "@mercurjs/dashboard-sdk"
|
|
307
|
+
|
|
308
|
+
export const config: RouteConfig = {
|
|
309
|
+
label: "Messages",
|
|
310
|
+
permissions: ["messaging:view"],
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export const handle: RouteHandle = {
|
|
314
|
+
permissions: "messaging:view",
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
`can("messaging", "edit")` and `buildPermission("messaging", "manage")` accept it too. This only affects types: the dashboard reads the member's effective rights from the API at runtime.
|
|
319
|
+
|
|
273
320
|
## Branding
|
|
274
321
|
|
|
275
322
|
Set `name` and `logo` in the plugin options to customize the sidebar header.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: "How to Customize Navigation"
|
|
3
3
|
sidebarTitle: "Customize Navigation"
|
|
4
|
-
description: "Reorder, hide, relabel,
|
|
4
|
+
description: "Reorder, hide, relabel, re-parent, and group the panel's built-in sidebar items from a single _navigation.ts file."
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
Reshape the built-in sidebar without replacing it. You author one host-owned file, `src/_navigation.ts`, and it reorders, hides, relabels, and re-parents the built-in items.
|
|
@@ -55,8 +55,9 @@ With it present, `id` and `nested` autocomplete and an unknown id fails `tsc`.
|
|
|
55
55
|
| `label` | `string` | Relabel with an i18n key or literal. |
|
|
56
56
|
| `icon` | `ComponentType` | Replace its icon. |
|
|
57
57
|
| `nested` | `NavParentId \| null` | Re-parent under another top-level item. `null` promotes a nested item to top level. |
|
|
58
|
+
| `group` | `NavGroupId` | List the item under a [group heading](#group-items-under-headings). |
|
|
58
59
|
|
|
59
|
-
|
|
60
|
+
`id`, `nested`, and `group` are checked against the panel's generated `NavItemRegistry`, `NavParentRegistry`, and `NavGroupRegistry`.
|
|
60
61
|
</Step>
|
|
61
62
|
<Step title="Reload the panel">
|
|
62
63
|
Open the vendor portal. Orders sits at the top, Price Lists is gone from the menu, and Campaigns now appears under Orders.
|
|
@@ -80,6 +81,80 @@ export default defineNavigationConfig({
|
|
|
80
81
|
})
|
|
81
82
|
```
|
|
82
83
|
|
|
84
|
+
## Group items under headings
|
|
85
|
+
|
|
86
|
+
Declare groups by keyword in `groups`, then assign items to them with `group`. A group renders in whichever sidebar its items live in, and a group with no visible items isn't rendered.
|
|
87
|
+
|
|
88
|
+
### Main sidebar
|
|
89
|
+
|
|
90
|
+
The main sidebar ships without groups. Items you assign to a declared group move out of the default list and render below it, under the group's heading.
|
|
91
|
+
|
|
92
|
+
```ts apps/vendor/src/_navigation.ts
|
|
93
|
+
import { defineNavigationConfig } from "@mercurjs/dashboard-sdk"
|
|
94
|
+
|
|
95
|
+
export default defineNavigationConfig({
|
|
96
|
+
groups: [
|
|
97
|
+
{ id: "catalog", label: "Catalog" },
|
|
98
|
+
{ id: "finance", label: "Finance" },
|
|
99
|
+
],
|
|
100
|
+
items: [
|
|
101
|
+
{ id: "products", group: "catalog" },
|
|
102
|
+
{ id: "inventory", group: "catalog" },
|
|
103
|
+
{ id: "payouts", group: "finance" },
|
|
104
|
+
],
|
|
105
|
+
})
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Only top-level items can be grouped. Nested children follow their parent.
|
|
109
|
+
|
|
110
|
+
### Settings sidebar
|
|
111
|
+
|
|
112
|
+
The settings sidebar already lists its items under groups. The admin panel ships `general`, `developer`, and `myAccount`. The vendor portal ships `general`. You can reshape these built-in groups, add your own, and move items between them. Settings items are targeted by their path without the leading slash, such as `settings/users`.
|
|
113
|
+
|
|
114
|
+
```ts apps/admin/src/_navigation.ts
|
|
115
|
+
import { defineNavigationConfig } from "@mercurjs/dashboard-sdk"
|
|
116
|
+
|
|
117
|
+
export default defineNavigationConfig({
|
|
118
|
+
groups: [
|
|
119
|
+
{ id: "integrations", label: "Integrations", rank: 1 }, // new group, after General
|
|
120
|
+
{ id: "developer", label: "API" }, // relabel a built-in group
|
|
121
|
+
],
|
|
122
|
+
items: [
|
|
123
|
+
{ id: "settings/secret-api-keys", group: "integrations" },
|
|
124
|
+
{ id: "settings/tax-regions", group: "integrations", rank: 0 },
|
|
125
|
+
],
|
|
126
|
+
})
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Group fields
|
|
130
|
+
|
|
131
|
+
| Field | Type | Effect |
|
|
132
|
+
|-------|------|--------|
|
|
133
|
+
| `id` | `string` | **Required.** A new keyword to declare a group, or a built-in settings group id to override. |
|
|
134
|
+
| `label` | `string` | Heading text. A literal, or an i18n key when `translationNs` is set. Defaults to the `id`. |
|
|
135
|
+
| `translationNs` | `string` | i18n namespace used to resolve `label`. |
|
|
136
|
+
| `rank` | `number` | Order among groups, lower first. Defaults to declaration order. Built-in settings groups rank `0`, `1`, `2`. |
|
|
137
|
+
| `hidden` | `boolean` | Remove the group and its items from the sidebar. |
|
|
138
|
+
|
|
139
|
+
An item's `group` only accepts a built-in group id or an id declared in the same file's `groups`, so a typo fails `tsc`.
|
|
140
|
+
|
|
141
|
+
### Group a custom page
|
|
142
|
+
|
|
143
|
+
A custom page picks its group in its own route config with the same keyword.
|
|
144
|
+
|
|
145
|
+
```tsx apps/admin/src/routes/settings/webhooks/page.tsx
|
|
146
|
+
import type { RouteConfig } from "@mercurjs/dashboard-sdk"
|
|
147
|
+
|
|
148
|
+
export const config: RouteConfig = {
|
|
149
|
+
label: "Webhooks",
|
|
150
|
+
group: "integrations",
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
<Note>
|
|
155
|
+
A page whose group isn't declared falls back to the default placement: the ungrouped list in the main sidebar, or the first group in the settings sidebar.
|
|
156
|
+
</Note>
|
|
157
|
+
|
|
83
158
|
## Verify
|
|
84
159
|
|
|
85
160
|
1. The top-level order reflects your `rank` values, with `orders` first.
|
|
@@ -94,6 +169,9 @@ export default defineNavigationConfig({
|
|
|
94
169
|
<Accordion title="Can an installed block reorder the sidebar?">
|
|
95
170
|
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`.
|
|
96
171
|
</Accordion>
|
|
172
|
+
<Accordion title="Can a block put its page in a group?">
|
|
173
|
+
Yes. A block's page sets `group` in its route config like any other page. If the host hasn't declared that group, the page falls back to the default placement.
|
|
174
|
+
</Accordion>
|
|
97
175
|
<Accordion title="What ids can I target?">
|
|
98
176
|
Any built-in item, top-level or nested, by its own id, such as `orders`, `products`, `categories`, `collections`, `campaigns`, or `customer-groups`. Let your editor autocomplete `id:` against `NavItemId`. The full set is generated into your panel's `extension-targets.d.ts`.
|
|
99
177
|
</Accordion>
|
package/llms.txt
CHANGED
|
@@ -51,7 +51,7 @@ package (`node_modules/@mercurjs/docs/`).
|
|
|
51
51
|
- [How to Build a Block](content/resources/tutorials/build-a-block.mdx) — Author a reusable feature as a block covering backend, panel UI, and docs, then build it into a registry and install it into any Mercur project.
|
|
52
52
|
- [How to Add a Custom API Route](content/resources/tutorials/custom-api-route.mdx) — Add a backend endpoint, regenerate the route map, and call it from a panel page with full type safety.
|
|
53
53
|
- [How to Create a Custom Page](content/resources/tutorials/custom-panel-page.mdx) — Add a page to the vendor portal or admin panel with file-based routing and let the dashboard SDK wire it into the sidebar.
|
|
54
|
-
- [How to Customize Navigation](content/resources/tutorials/customize-navigation.mdx) — Reorder, hide, relabel,
|
|
54
|
+
- [How to Customize Navigation](content/resources/tutorials/customize-navigation.mdx) — Reorder, hide, relabel, re-parent, and group the panel's built-in sidebar items from a single _navigation.ts file.
|
|
55
55
|
- [How to Extend Forms and Tables](content/resources/tutorials/extend-forms-and-tables.mdx) — Add validated fields, detail-section displays, and list columns to a built-in model from a single file with defineCustomFieldsConfig.
|
|
56
56
|
- [How to Extend Onboarding](content/resources/tutorials/extend-onboarding.mdx) — Add a field to the vendor store-setup surface, carry it through additional_data, and persist it from a workflow hook without forking core.
|
|
57
57
|
- [How to Create Master Products and Offers](content/resources/tutorials/master-products-and-offers.mdx) — Publish one master product, add two competing seller offers, and read per-offer prices from the Store API.
|