@mercurjs/docs 2.3.6-canary.8 → 2.3.6
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/platform/order-group/guides/list-order-groups.mdx +4 -0
- package/content/references/api/vendor/sellers/select-seller.mdx +7 -3
- package/content/references/configuration.mdx +1 -0
- package/content/references/panel-extensions/create-page.mdx +45 -1
- package/content/references/panel-extensions/custom-fields.mdx +20 -1
- package/content/references/panel-extensions/widgets.mdx +86 -2
- package/content/resources/customization/extending-panels.mdx +2 -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 |
|
|
@@ -42,6 +42,10 @@ The workflow returns `{ rows, metadata }`, where `metadata` carries the `count`,
|
|
|
42
42
|
Filters go inside `variables`: the group repository understands `id`,
|
|
43
43
|
`customer_id`, `seller_id`, `status`, `sales_channel_id`, `created_at`,
|
|
44
44
|
`updated_at`, and a free-text `q` (matched against group id and customer id).
|
|
45
|
+
Order-level filters (`seller_id`, `status`, `sales_channel_id`) only decide
|
|
46
|
+
which groups match. A matching group's `seller_count` and `total` still
|
|
47
|
+
describe every order in the group, as covered in
|
|
48
|
+
[Computed totals](/platform/order-group/concepts/computed-totals).
|
|
45
49
|
</Note>
|
|
46
50
|
|
|
47
51
|
## Scope to a seller
|
|
@@ -4,7 +4,7 @@ api: "POST /vendor/sellers/select"
|
|
|
4
4
|
description: "Set the active seller for the member's session."
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Verifies the member belongs to the chosen seller. For session-authenticated requests the seller is also stored in the session.
|
|
8
8
|
|
|
9
9
|
<Note>
|
|
10
10
|
Requires only the `Authorization` header. Session-based clients get the seller stored server-side; token-based clients should send `x-seller-id` on subsequent requests instead.
|
|
@@ -22,6 +22,10 @@ Stores the chosen seller in the session after verifying the member belongs to it
|
|
|
22
22
|
Whether the seller was selected.
|
|
23
23
|
</ResponseField>
|
|
24
24
|
|
|
25
|
+
<ResponseField name="seller_id" type="string">
|
|
26
|
+
The selected seller's ID. Token-based clients send it as `x-seller-id` on subsequent requests.
|
|
27
|
+
</ResponseField>
|
|
28
|
+
|
|
25
29
|
<RequestExample>
|
|
26
30
|
```bash cURL
|
|
27
31
|
curl -X POST 'http://localhost:9000/vendor/sellers/select' \
|
|
@@ -31,7 +35,7 @@ curl -X POST 'http://localhost:9000/vendor/sellers/select' \
|
|
|
31
35
|
```
|
|
32
36
|
|
|
33
37
|
```ts JS Client
|
|
34
|
-
const {
|
|
38
|
+
const { seller_id } = await client.vendor.sellers.select.mutate({
|
|
35
39
|
seller_id: "sel_01HXYZ",
|
|
36
40
|
})
|
|
37
41
|
```
|
|
@@ -39,6 +43,6 @@ const { success } = await client.vendor.sellers.select.mutate({
|
|
|
39
43
|
|
|
40
44
|
<ResponseExample>
|
|
41
45
|
```json 200
|
|
42
|
-
{ "success": true }
|
|
46
|
+
{ "success": true, "seller_id": "sel_01HXYZ" }
|
|
43
47
|
```
|
|
44
48
|
</ResponseExample>
|
|
@@ -70,6 +70,7 @@ The starter (`apps/api` and projects created with `create-mercur-app`) reads:
|
|
|
70
70
|
| `COOKIE_SECRET` | Cookie signing secret | `supersecret` (change in production) |
|
|
71
71
|
| `FILE_BACKEND_URL` | Public origin baked into uploaded file URLs | `http://localhost:9000/static` |
|
|
72
72
|
| `MERCUR_VENDOR_URL` | Vendor dashboard base URL used in member invite links | `""`; can also be set via the seller module's `vendor_url` option |
|
|
73
|
+
| `MERCUR_DEV_CODEGEN` | Set to `"false"` to skip automatic type generation on dev startup | `"true"` |
|
|
73
74
|
| `NODE_ENV` | Environment selection | `development` |
|
|
74
75
|
|
|
75
76
|
## Dashboard modules
|
|
@@ -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.
|
|
@@ -191,7 +191,11 @@ list: {
|
|
|
191
191
|
{ id: "brand_name", header: "Brand", component: ({ row }) => row.brand?.name }, // add from link
|
|
192
192
|
],
|
|
193
193
|
bulkActions: [{ rank: 0, component: ArchiveBulkAction }],
|
|
194
|
-
filters: [
|
|
194
|
+
filters: [
|
|
195
|
+
{ key: "customer_id", label: "Buyer email", type: "select", options: buyerEmails, searchable: true }, // replace a built-in filter
|
|
196
|
+
{ key: "status", remove: true }, // remove a built-in filter
|
|
197
|
+
{ key: "erp_synced", label: "Synced to ERP", type: "select", options: yesNo }, // add a new one
|
|
198
|
+
],
|
|
195
199
|
viewDefaults: {
|
|
196
200
|
columnVisibility: { created_at: false },
|
|
197
201
|
columnOrder: ["title", "sku", "erp_id"],
|
|
@@ -207,6 +211,21 @@ list: {
|
|
|
207
211
|
| `header` | `string` (optional) | Column header text, for added columns. |
|
|
208
212
|
| `component` | `ComponentType<{ row?, value? }>` (optional) | Cell renderer. Receives the full `row` and the cell `value`. |
|
|
209
213
|
|
|
214
|
+
`filters[]` is `CustomListFilter`. The `key` is the query param the filter
|
|
215
|
+
writes, so it matches a built-in filter by its param name (`customer_id`,
|
|
216
|
+
`status`, `created_at`, ...).
|
|
217
|
+
|
|
218
|
+
| Field | Type | Description |
|
|
219
|
+
| --------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------------- |
|
|
220
|
+
| `key` | `string` | Built-in filter key to replace or remove. Any other string adds a new filter. |
|
|
221
|
+
| `remove` | `true` (optional) | Drop the built-in filter with this `key`. No other fields are needed. |
|
|
222
|
+
| `label` | `string` | Label shown in the "Add filter" menu. |
|
|
223
|
+
| `type` | `"select" \| "string" \| "number" \| "date"` | Filter control. |
|
|
224
|
+
| `options` | `{ label: string; value: unknown }[]` | Choices for a `select` filter. `multiple` and `searchable` are also accepted. |
|
|
225
|
+
|
|
226
|
+
A replacement keeps the built-in filter's position; added filters go after the
|
|
227
|
+
built-in ones.
|
|
228
|
+
|
|
210
229
|
<Note>
|
|
211
230
|
**Bulk-action rendering is deferred in the MVP.** `bulkActions` are accepted and
|
|
212
231
|
surfaced by the config, but not yet mounted into the list toolbar.
|
|
@@ -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
|
|
|
@@ -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.
|