@spree/dashboard-core 0.1.0
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/LICENSE +21 -0
- package/README.md +418 -0
- package/package.json +84 -0
- package/src/client.ts +25 -0
- package/src/components/address-form-dialog.tsx +376 -0
- package/src/components/app-sidebar.tsx +70 -0
- package/src/components/bulk-action-bar.tsx +504 -0
- package/src/components/can.tsx +30 -0
- package/src/components/country-combobox.tsx +238 -0
- package/src/components/country-state-fields.tsx +86 -0
- package/src/components/currency-select.tsx +181 -0
- package/src/components/export-button.tsx +156 -0
- package/src/components/file-upload-field.tsx +265 -0
- package/src/components/image-upload-field.tsx +53 -0
- package/src/components/import-button.tsx +202 -0
- package/src/components/locale-select.tsx +268 -0
- package/src/components/market-combobox.tsx +89 -0
- package/src/components/nav-main.tsx +186 -0
- package/src/components/page-header.tsx +286 -0
- package/src/components/page-tabs.tsx +78 -0
- package/src/components/preferences-form.tsx +404 -0
- package/src/components/resource-combobox.tsx +126 -0
- package/src/components/resource-multi-autocomplete.tsx +176 -0
- package/src/components/resource-picker-sheet.tsx +235 -0
- package/src/components/resource-table.tsx +652 -0
- package/src/components/settings-sidebar.tsx +139 -0
- package/src/components/slot.tsx +50 -0
- package/src/components/store-date-picker.tsx +18 -0
- package/src/components/store-switcher.tsx +70 -0
- package/src/components/table-toolbar.tsx +818 -0
- package/src/components/tag-combobox.tsx +99 -0
- package/src/components/top-bar.tsx +210 -0
- package/src/hooks/use-auth.ts +10 -0
- package/src/hooks/use-command-palette.tsx +34 -0
- package/src/hooks/use-countries.ts +15 -0
- package/src/hooks/use-custom-fields.ts +142 -0
- package/src/hooks/use-direct-upload.ts +104 -0
- package/src/hooks/use-display-name.ts +42 -0
- package/src/hooks/use-export.ts +75 -0
- package/src/hooks/use-global-search.ts +79 -0
- package/src/hooks/use-host-form.ts +34 -0
- package/src/hooks/use-import.ts +62 -0
- package/src/hooks/use-resource-mutation.ts +99 -0
- package/src/hooks/use-switch-admin-locale.ts +55 -0
- package/src/index.ts +97 -0
- package/src/lib/custom-field-components.ts +56 -0
- package/src/lib/download.ts +49 -0
- package/src/lib/filters-to-ransack.ts +47 -0
- package/src/lib/form-errors.ts +103 -0
- package/src/lib/form-fields-registry.ts +102 -0
- package/src/lib/form-mappers.ts +30 -0
- package/src/lib/formatters.ts +19 -0
- package/src/lib/i18n.ts +210 -0
- package/src/lib/nav-registry.ts +224 -0
- package/src/lib/permissions.ts +55 -0
- package/src/lib/query-client.ts +11 -0
- package/src/lib/query-keys.ts +69 -0
- package/src/lib/route-registry.ts +158 -0
- package/src/lib/search-registry.ts +129 -0
- package/src/lib/settings-nav-registry.ts +158 -0
- package/src/lib/slot-registry.ts +116 -0
- package/src/lib/table-registry.ts +274 -0
- package/src/lib/timestamp-columns.tsx +58 -0
- package/src/locales/ar.json +629 -0
- package/src/locales/de.json +629 -0
- package/src/locales/en.json +629 -0
- package/src/locales/fr.json +629 -0
- package/src/locales/pl.json +629 -0
- package/src/locales/zh-CN.json +629 -0
- package/src/plugin.ts +222 -0
- package/src/providers/auth-provider.tsx +188 -0
- package/src/providers/permission-provider.tsx +118 -0
- package/src/providers/store-provider.tsx +111 -0
- package/src/virtual.d.ts +5 -0
- package/src/vite/discover.ts +170 -0
- package/src/vite/index.ts +270 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vendo Connect Inc., Vendo Sp. z o.o.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
# @spree/dashboard-core
|
|
2
|
+
|
|
3
|
+
Spree Dashboard framework. Registries, providers, infra hooks, the admin SDK client, and the `defineDashboardPlugin` extension API.
|
|
4
|
+
|
|
5
|
+
This is the package plugin authors install. Use it to register nav entries, slot widgets, table column extensions, and settings pages from a separate npm package that gets imported into `@spree/dashboard` at boot.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add @spree/dashboard-core @spree/dashboard-ui
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Peer dependencies that the consuming app provides: `react`, `react-dom`, `@spree/admin-sdk`, `@spree/dashboard-ui`, `@tanstack/react-query`, `@tanstack/react-router`, `@tanstack/react-hotkeys`, `react-hook-form`, `react-i18next`, `i18next`, `sonner`.
|
|
14
|
+
|
|
15
|
+
**Why peer deps:** the registries (nav, slot, table, settings-nav, form fields, custom field components) are module singletons. If your plugin bundles its own copy of `@spree/dashboard-core`, `registerSlot` writes to a different Map than the app's `<Slot>` reads — silent no-op. Same applies to React, the query client, the router instance, and the i18n singleton. pnpm dedupes these to a single instance as long as they're peers.
|
|
16
|
+
|
|
17
|
+
## What's in the box
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
@spree/dashboard-core
|
|
21
|
+
├── adminClient # @spree/admin-sdk instance, baseUrl wired to VITE_SPREE_API_URL
|
|
22
|
+
│
|
|
23
|
+
├── Providers (mount in your app shell, in this order)
|
|
24
|
+
│ ├── AuthProvider # JWT + refresh token, /auth/refresh on cold load
|
|
25
|
+
│ ├── PermissionProvider # /api/v3/admin/me → CanCanCan rules
|
|
26
|
+
│ └── StoreProvider # current store context (multi-store)
|
|
27
|
+
│
|
|
28
|
+
├── Registries (the four extension points)
|
|
29
|
+
│ ├── nav # sidebar items
|
|
30
|
+
│ ├── settingsNav # settings sub-shell groups + items
|
|
31
|
+
│ ├── slot # named injection points (`product.form_sidebar`, ...)
|
|
32
|
+
│ └── tables # column add/remove/update per list view
|
|
33
|
+
│
|
|
34
|
+
├── Infra hooks
|
|
35
|
+
│ ├── useAuth # { token, user, login(), logout() }
|
|
36
|
+
│ ├── usePermissions # CanCanCan rules + <Can> helper
|
|
37
|
+
│ ├── useStore # current store
|
|
38
|
+
│ ├── useResourceMutation # useMutation + toast + form-error mapping
|
|
39
|
+
│ ├── useDirectUpload # Active Storage direct-upload
|
|
40
|
+
│ ├── useGlobalSearch # cross-resource search
|
|
41
|
+
│ ├── useCommandPalette # ⌘K palette state
|
|
42
|
+
│ ├── useCopyToClipboard
|
|
43
|
+
│ ├── useScrolled
|
|
44
|
+
│ ├── useCountries # admin/countries (geo data, cached)
|
|
45
|
+
│ ├── useExport # CSV export jobs (polled)
|
|
46
|
+
│ └── useCustomFields # metafield definitions + values
|
|
47
|
+
│
|
|
48
|
+
├── i18n (i18next + react-i18next)
|
|
49
|
+
│ ├── Base translation namespace (`admin.actions.*`, `admin.common.*`, `admin.validation.*`, …)
|
|
50
|
+
│ ├── Trans, useTranslation # re-exported from react-i18next
|
|
51
|
+
│ └── i18n # the i18next singleton (call addResourceBundle to extend)
|
|
52
|
+
│
|
|
53
|
+
├── Helpers
|
|
54
|
+
│ ├── filtersToRansack # UI filter shape → Ransack params
|
|
55
|
+
│ ├── mapSpreeErrorsToForm # 422 → RHF setError
|
|
56
|
+
│ ├── formatPrice / getInitials
|
|
57
|
+
│ ├── Subject, Action # CanCanCan subject/action constants
|
|
58
|
+
│ └── queryClient # TanStack Query client singleton
|
|
59
|
+
│
|
|
60
|
+
└── defineDashboardPlugin # the one-call extension facade
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Additional entry point:
|
|
64
|
+
|
|
65
|
+
- `@spree/dashboard-core/vite` — Vite plugin that wires Tailwind v4. See [Vite integration](#vite-integration).
|
|
66
|
+
|
|
67
|
+
## Vite integration
|
|
68
|
+
|
|
69
|
+
The dashboard relies on Tailwind v4, which doesn't scan `node_modules` by default and only accepts filesystem paths in `@source` directives (not bare package specifiers). The `spreeDashboardPlugin` from `@spree/dashboard-core/vite` resolves each Spree dashboard package and each host-named plugin through Node module resolution and injects matching `@source` directives into your CSS entry at build time. It also bundles `@tailwindcss/vite` itself so plugin ordering is guaranteed — hosts must NOT register `@tailwindcss/vite` separately.
|
|
70
|
+
|
|
71
|
+
### Using the full `@spree/dashboard` app shell
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
// host vite.config.ts
|
|
75
|
+
import { spreeDashboardPlugin } from '@spree/dashboard-core/vite'
|
|
76
|
+
import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
|
|
77
|
+
import react from '@vitejs/plugin-react'
|
|
78
|
+
import { defineConfig } from 'vite'
|
|
79
|
+
|
|
80
|
+
export default defineConfig({
|
|
81
|
+
plugins: [
|
|
82
|
+
spreeDashboardPlugin({
|
|
83
|
+
plugins: [
|
|
84
|
+
'@my-store/orders-dashboard-plugin',
|
|
85
|
+
'@my-store/wishlists-dashboard-plugin',
|
|
86
|
+
],
|
|
87
|
+
}),
|
|
88
|
+
TanStackRouterVite(),
|
|
89
|
+
react(),
|
|
90
|
+
],
|
|
91
|
+
})
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
```css
|
|
95
|
+
/* host src/styles.css */
|
|
96
|
+
@import "@spree/dashboard/styles.css";
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Building a custom dashboard on `@spree/dashboard-core` + `@spree/dashboard-ui`
|
|
100
|
+
|
|
101
|
+
If you're not using `@spree/dashboard`'s app shell — e.g. a vendor portal or B2B buyer admin built directly on the registries and primitives — point `cssEntry` at your own CSS file:
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
// custom-dashboard vite.config.ts
|
|
105
|
+
import { spreeDashboardPlugin } from '@spree/dashboard-core/vite'
|
|
106
|
+
|
|
107
|
+
export default defineConfig({
|
|
108
|
+
plugins: [
|
|
109
|
+
spreeDashboardPlugin({
|
|
110
|
+
cssEntry: './src/admin.css',
|
|
111
|
+
plugins: ['@my-store/vendor-portal-plugin'],
|
|
112
|
+
}),
|
|
113
|
+
// host owns react, router, etc.
|
|
114
|
+
],
|
|
115
|
+
})
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
```css
|
|
119
|
+
/* custom-dashboard src/admin.css */
|
|
120
|
+
@import "@spree/dashboard-ui/styles.css";
|
|
121
|
+
/* Tailwind auto-scans the host's own src/, so host-authored classes
|
|
122
|
+
work without configuration. The Vite plugin injects @source for
|
|
123
|
+
@spree/dashboard-core, @spree/dashboard-ui, and any named plugins. */
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### `@spree/dashboard-ui` only (no `@spree/dashboard-core`)
|
|
127
|
+
|
|
128
|
+
If you only need design-system primitives and tokens, no registries, no providers — you don't need this Vite plugin at all. Install `@spree/dashboard-ui`, set up `@tailwindcss/vite` yourself, and import its stylesheet:
|
|
129
|
+
|
|
130
|
+
```css
|
|
131
|
+
@import "@spree/dashboard-ui/styles.css";
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`dashboard-ui/styles.css` carries its own `@source` directives covering its components.
|
|
135
|
+
|
|
136
|
+
### Options
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
spreeDashboardPlugin({
|
|
140
|
+
/**
|
|
141
|
+
* Path to the host's CSS entry file, relative to the host's project root.
|
|
142
|
+
* The plugin injects @source directives into this file. Defaults to
|
|
143
|
+
* `./src/styles.css`.
|
|
144
|
+
*/
|
|
145
|
+
cssEntry: './src/styles.css',
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Names of installed dashboard plugin packages. Each must be a resolvable
|
|
149
|
+
* npm specifier. The plugin resolves each via Node module resolution and
|
|
150
|
+
* tells Tailwind to scan its source files.
|
|
151
|
+
*/
|
|
152
|
+
plugins: ['@my-store/orders-plugin'],
|
|
153
|
+
})
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### What plugin authors must do
|
|
157
|
+
|
|
158
|
+
Ship `src/` in their published package so Tailwind has source files to scan:
|
|
159
|
+
|
|
160
|
+
```json
|
|
161
|
+
// plugin's package.json
|
|
162
|
+
{
|
|
163
|
+
"name": "@my-store/orders-dashboard-plugin",
|
|
164
|
+
"main": "./src/index.ts",
|
|
165
|
+
"types": "./src/index.ts",
|
|
166
|
+
"files": ["src", "README.md"]
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Plugin authors do NOT need to ship pre-built CSS, configure Tailwind, or care about the host's package manager — adding the plugin name to the host's `plugins: [...]` is enough.
|
|
171
|
+
|
|
172
|
+
## Plugin authoring — the basics
|
|
173
|
+
|
|
174
|
+
Your plugin is an npm package. Its entry module calls `defineDashboardPlugin({...})` at import time. The dashboard app imports your entry module before first render — every registry handles late registration via `useSyncExternalStore`, so even if your plugin loads asynchronously the sidebar/slots re-render.
|
|
175
|
+
|
|
176
|
+
```tsx
|
|
177
|
+
// my-spree-plugin/src/index.ts
|
|
178
|
+
import { defineDashboardPlugin, usePermissions } from '@spree/dashboard-core'
|
|
179
|
+
import { Card, CardHeader, CardTitle, CardContent } from '@spree/dashboard-ui'
|
|
180
|
+
import { BarChartIcon } from 'lucide-react'
|
|
181
|
+
|
|
182
|
+
function WishlistsWidget({ product }: { product: { id: string; wishlist_count: number } }) {
|
|
183
|
+
return (
|
|
184
|
+
<Card>
|
|
185
|
+
<CardHeader><CardTitle>Wishlists</CardTitle></CardHeader>
|
|
186
|
+
<CardContent>{product.wishlist_count} customers</CardContent>
|
|
187
|
+
</Card>
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
defineDashboardPlugin({
|
|
192
|
+
nav: [
|
|
193
|
+
{ key: 'wishlists', label: 'Wishlists', path: '/wishlists', icon: BarChartIcon, position: 50 },
|
|
194
|
+
],
|
|
195
|
+
slots: {
|
|
196
|
+
'product.form_sidebar': [
|
|
197
|
+
{ id: 'wishlist-count', component: WishlistsWidget, position: 50 },
|
|
198
|
+
],
|
|
199
|
+
},
|
|
200
|
+
tables: {
|
|
201
|
+
products: {
|
|
202
|
+
add: [{ key: 'wishlist_count', label: 'Wishlists', sortable: true }],
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
})
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Then in the consuming app's entry point:
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
// my-spree-app/src/main.tsx
|
|
212
|
+
import '@spree/dashboard-core/lib/i18n' // base translations
|
|
213
|
+
import 'my-spree-plugin' // your plugin registers extensions
|
|
214
|
+
// ...rest of normal dashboard bootstrap
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Translations — extending the base namespace
|
|
218
|
+
|
|
219
|
+
Plugin authors **inherit the framework's base translation namespace** (`admin.actions.save`, `admin.common.no_results`, `admin.validation.required`, `admin.fields.<simple>.label`, …) for free. To add your own keys, call `i18n.addResourceBundle` after the side-effect import.
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
// my-spree-plugin/src/index.ts
|
|
223
|
+
import { i18n } from '@spree/dashboard-core'
|
|
224
|
+
import en from './locales/en.json'
|
|
225
|
+
//
|
|
226
|
+
// {
|
|
227
|
+
// "admin": {
|
|
228
|
+
// "wishlists": {
|
|
229
|
+
// "title": "Wishlists",
|
|
230
|
+
// "empty_state": "No wishlists yet"
|
|
231
|
+
// }
|
|
232
|
+
// }
|
|
233
|
+
// }
|
|
234
|
+
|
|
235
|
+
i18n.addResourceBundle('en', 'translation', en, true, true)
|
|
236
|
+
// `deep: true` + `overwrite: true` merge nested keys into the base namespace
|
|
237
|
+
// without dropping anything the framework provided.
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Now anywhere in your plugin you can mix base keys and your own:
|
|
241
|
+
|
|
242
|
+
```tsx
|
|
243
|
+
import { useTranslation } from '@spree/dashboard-core'
|
|
244
|
+
|
|
245
|
+
function WishlistsHeader() {
|
|
246
|
+
const { t } = useTranslation()
|
|
247
|
+
return (
|
|
248
|
+
<header>
|
|
249
|
+
<h1>{t('admin.wishlists.title')}</h1>
|
|
250
|
+
<button>{t('admin.actions.create')}</button>{/* base */}
|
|
251
|
+
</header>
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### When plugins should pick their own sub-namespace
|
|
257
|
+
|
|
258
|
+
`addResourceBundle` deep-merges into `admin.*`, so `admin.actions.save` from a plugin would **overwrite** the framework's `admin.actions.save`. To avoid collisions, put plugin-specific text under a vendor-prefixed key:
|
|
259
|
+
|
|
260
|
+
```jsonc
|
|
261
|
+
{
|
|
262
|
+
"admin": {
|
|
263
|
+
"wishlists": { ... } // ok — fresh namespace under admin
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Not:
|
|
269
|
+
|
|
270
|
+
```jsonc
|
|
271
|
+
{
|
|
272
|
+
"admin": {
|
|
273
|
+
"actions": {
|
|
274
|
+
"share": "Share" // collides if the framework ever adds `admin.actions.share`
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Prefer `admin.<plugin>.*` for everything you own. Use base keys (`admin.actions.*`, `admin.common.*`, `admin.fields.<simple>.*`) by *reading* them, not by extending them.
|
|
281
|
+
|
|
282
|
+
### Multi-locale plugins
|
|
283
|
+
|
|
284
|
+
Same pattern, once per locale:
|
|
285
|
+
|
|
286
|
+
```ts
|
|
287
|
+
import { i18n } from '@spree/dashboard-core'
|
|
288
|
+
import en from './locales/en.json'
|
|
289
|
+
import fr from './locales/fr.json'
|
|
290
|
+
|
|
291
|
+
i18n.addResourceBundle('en', 'translation', en, true, true)
|
|
292
|
+
i18n.addResourceBundle('fr', 'translation', fr, true, true)
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
The framework currently ships English only; once Spree's locale list grows the same `addResourceBundle` pattern applies.
|
|
296
|
+
|
|
297
|
+
## Extension surface — the registries
|
|
298
|
+
|
|
299
|
+
### `nav` — main sidebar items
|
|
300
|
+
|
|
301
|
+
```ts
|
|
302
|
+
import { nav } from '@spree/dashboard-core'
|
|
303
|
+
import { PackageIcon } from 'lucide-react'
|
|
304
|
+
|
|
305
|
+
nav.add({
|
|
306
|
+
key: 'wishlists',
|
|
307
|
+
label: 'Wishlists',
|
|
308
|
+
path: '/wishlists', // prefixed with /$storeId at render time
|
|
309
|
+
icon: PackageIcon,
|
|
310
|
+
position: 50, // built-ins use 100/200/300 — slot in between
|
|
311
|
+
subject: 'Spree::Wishlist', // CanCanCan subject — hides item without permission
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
// Or: insert relative to an existing entry
|
|
315
|
+
nav.insertAfter('orders', { key: 'wishlists', label: 'Wishlists', path: '/wishlists' })
|
|
316
|
+
nav.update('orders', { label: 'All Orders' })
|
|
317
|
+
nav.remove('legacy-thing')
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### `settingsNav` — settings sub-shell
|
|
321
|
+
|
|
322
|
+
Settings entries cluster under groups:
|
|
323
|
+
|
|
324
|
+
```ts
|
|
325
|
+
import { settingsNav } from '@spree/dashboard-core'
|
|
326
|
+
|
|
327
|
+
settingsNav.addGroup({ key: 'integrations', label: 'Integrations', position: 500 })
|
|
328
|
+
settingsNav.add({
|
|
329
|
+
key: 'wishlist-settings',
|
|
330
|
+
label: 'Wishlists',
|
|
331
|
+
path: '/wishlists', // prefixed with /$storeId/settings at render time
|
|
332
|
+
group: 'integrations',
|
|
333
|
+
position: 100,
|
|
334
|
+
})
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### `registerSlot` — named injection points
|
|
338
|
+
|
|
339
|
+
Slots are typed by their context. A `product.form_sidebar` slot receives the product, ambient permissions, store, and user:
|
|
340
|
+
|
|
341
|
+
```tsx
|
|
342
|
+
import { registerSlot } from '@spree/dashboard-core'
|
|
343
|
+
import { Card } from '@spree/dashboard-ui'
|
|
344
|
+
|
|
345
|
+
registerSlot<{ product: { id: string; wishlist_count: number } }>('product.form_sidebar', {
|
|
346
|
+
id: 'wishlist-count',
|
|
347
|
+
component: ({ product, permissions }) => {
|
|
348
|
+
if (!permissions?.can('manage', 'Spree::Wishlist')) return null
|
|
349
|
+
return <Card>Wishlists: {product.wishlist_count}</Card>
|
|
350
|
+
},
|
|
351
|
+
position: 50,
|
|
352
|
+
// Optional visibility predicate — receives the merged context
|
|
353
|
+
if: (ctx) => ctx.product.wishlist_count > 0,
|
|
354
|
+
})
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
Built-in slot names follow a `resource.location` convention (e.g. `product.form_sidebar`, `order.page_body`, `product.actions`). The full list lives in the [slots catalog](https://spreecommerce.org/docs/developer/dashboard/slots-catalog).
|
|
358
|
+
|
|
359
|
+
### `tables` — list-view column extensions
|
|
360
|
+
|
|
361
|
+
```ts
|
|
362
|
+
import { tables } from '@spree/dashboard-core'
|
|
363
|
+
|
|
364
|
+
tables.products.addColumn({
|
|
365
|
+
key: 'wishlist_count',
|
|
366
|
+
label: 'Wishlists',
|
|
367
|
+
sortable: true,
|
|
368
|
+
render: (product) => product.wishlist_count,
|
|
369
|
+
})
|
|
370
|
+
tables.products.removeColumn('legacy_column')
|
|
371
|
+
tables.products.updateColumn('total', { label: 'Net total' })
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
**Ordering:** built-in tables register lazily via side-effect imports in route files (`import '@/tables/products'` inside the products route), so calling `tables.products.addColumn(...)` from your plugin's entry module — which loads before any route mounts — would otherwise race the registration. The mutator handles this transparently: mutations on a not-yet-registered table are queued and replayed when `defineTable('products', ...)` runs. Mutations on tables that never register stay pending and never fire, which is the right behavior when your plugin extends an optional feature.
|
|
375
|
+
|
|
376
|
+
## `defineDashboardPlugin` — one-call facade
|
|
377
|
+
|
|
378
|
+
Every registry in a single declarative config:
|
|
379
|
+
|
|
380
|
+
```ts
|
|
381
|
+
import { defineDashboardPlugin } from '@spree/dashboard-core/plugin'
|
|
382
|
+
|
|
383
|
+
defineDashboardPlugin({
|
|
384
|
+
nav: [...],
|
|
385
|
+
settingsNavGroups: [...],
|
|
386
|
+
settingsNav: [...],
|
|
387
|
+
slots: { 'product.form_sidebar': [...] },
|
|
388
|
+
tables: {
|
|
389
|
+
products: {
|
|
390
|
+
add: [...],
|
|
391
|
+
remove: ['legacy_column'],
|
|
392
|
+
update: { total: { label: 'Net total' } },
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
// Extension fields on built-in forms — hydrate from the fetched resource,
|
|
396
|
+
// persist through the form's own Save. Render the input from a slot widget
|
|
397
|
+
// bound via useHostForm().
|
|
398
|
+
formFields: {
|
|
399
|
+
product: [{ name: 'tech_specs', from: (product) => product?.tech_specs ?? '' }],
|
|
400
|
+
},
|
|
401
|
+
// Replace the input widget for a specific custom field definition.
|
|
402
|
+
customFieldComponents: { 'specs.color': ColorPicker },
|
|
403
|
+
})
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
Equivalent to calling the underlying registry methods directly. Use whichever feels right — the facade is purely ergonomic.
|
|
407
|
+
|
|
408
|
+
## What's NOT here
|
|
409
|
+
|
|
410
|
+
Resource-specific hooks (`use-orders`, `use-products`, `use-customers`, …), Zod schemas, feature pages, route definitions, and per-feature locale strings live in `@spree/dashboard` (the app). We do not publish feature internals. Plugin authors compose new pages out of `@spree/dashboard-ui` primitives + `@spree/dashboard-core` infra, not out of forked feature internals.
|
|
411
|
+
|
|
412
|
+
Theme and toast UI live in `@spree/dashboard-ui` (they're rendering concerns, no Spree backend coupling): `ThemeProvider`, `useTheme`, `Toaster`.
|
|
413
|
+
|
|
414
|
+
## Architectural notes
|
|
415
|
+
|
|
416
|
+
- **Client injection**: `adminClient` reads `import.meta.env.VITE_SPREE_API_URL` at build time. Three deployment topologies (dev proxy, prod single-origin, prod cross-origin) all share one code path. See `src/client.ts` for details.
|
|
417
|
+
- **i18n bootstrap**: `import '@spree/dashboard-core/lib/i18n'` initializes i18next with the base namespace. Must run before any component calls `useTranslation`. The dashboard's `i18n-setup.ts` does this + adds the app's resources via `addResourceBundle`. Plugins follow the same pattern.
|
|
418
|
+
- **Registries are module singletons**: see "Peer dependencies" above. Build your plugin with `@spree/dashboard-core` listed as a peer dep, not a regular dep.
|
package/package.json
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@spree/dashboard-core",
|
|
3
|
+
"license": "MIT",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "Spree Dashboard framework — registries (table, nav, slot, settings-nav), providers (auth, permission, store), admin SDK client, generic infra hooks, and the defineDashboardPlugin extension API. Developer Preview.",
|
|
6
|
+
"author": "Vendo Connect Inc.",
|
|
7
|
+
"homepage": "https://spreecommerce.org",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/spree/spree.git",
|
|
11
|
+
"directory": "packages/dashboard-core"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"spree",
|
|
15
|
+
"commerce",
|
|
16
|
+
"ecommerce",
|
|
17
|
+
"admin",
|
|
18
|
+
"dashboard",
|
|
19
|
+
"react",
|
|
20
|
+
"plugin"
|
|
21
|
+
],
|
|
22
|
+
"type": "module",
|
|
23
|
+
"main": "./src/index.ts",
|
|
24
|
+
"module": "./src/index.ts",
|
|
25
|
+
"types": "./src/index.ts",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": "./src/index.ts",
|
|
28
|
+
"./plugin": "./src/plugin.ts",
|
|
29
|
+
"./vite": "./src/vite/index.ts",
|
|
30
|
+
"./vite/discover": "./src/vite/discover.ts",
|
|
31
|
+
"./providers/*": "./src/providers/*.tsx",
|
|
32
|
+
"./hooks/*": "./src/hooks/*.ts",
|
|
33
|
+
"./hooks/use-command-palette": "./src/hooks/use-command-palette.tsx",
|
|
34
|
+
"./lib/*": "./src/lib/*.ts",
|
|
35
|
+
"./locales/en.json": "./src/locales/en.json"
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"src",
|
|
39
|
+
"README.md",
|
|
40
|
+
"LICENSE"
|
|
41
|
+
],
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"@hookform/resolvers": "^5.2.2",
|
|
44
|
+
"@tanstack/react-hotkeys": "^0.10.0",
|
|
45
|
+
"@tanstack/react-query": "^5.100.9",
|
|
46
|
+
"@tanstack/react-router": "^1.169.2",
|
|
47
|
+
"i18next": "^26.2.0",
|
|
48
|
+
"react": "^19.2.6",
|
|
49
|
+
"react-dom": "^19.2.6",
|
|
50
|
+
"react-hook-form": "^7.75.0",
|
|
51
|
+
"react-i18next": "^17.0.8",
|
|
52
|
+
"sonner": "^2.0.7",
|
|
53
|
+
"@spree/admin-sdk": "^0.5.0",
|
|
54
|
+
"@spree/dashboard-ui": "^0.1.0"
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"@dnd-kit/core": "^6.3.1",
|
|
58
|
+
"@dnd-kit/sortable": "^10.0.0",
|
|
59
|
+
"@dnd-kit/utilities": "^3.2.2",
|
|
60
|
+
"@tailwindcss/vite": "^4.2.4",
|
|
61
|
+
"date-fns": "^4.1.0",
|
|
62
|
+
"date-fns-tz": "^3.2.0",
|
|
63
|
+
"lucide-react": "^1.14.0",
|
|
64
|
+
"spark-md5": "^3.0.2",
|
|
65
|
+
"zod": "^4.4.3"
|
|
66
|
+
},
|
|
67
|
+
"devDependencies": {
|
|
68
|
+
"@types/react": "^19.2.14",
|
|
69
|
+
"@types/react-dom": "^19.2.3",
|
|
70
|
+
"@types/spark-md5": "^3.0.5",
|
|
71
|
+
"typescript": "~6.0.3",
|
|
72
|
+
"vite": "^8.0.16",
|
|
73
|
+
"vitest": "^4.1.5",
|
|
74
|
+
"@spree/dashboard-ui": "^0.1.0",
|
|
75
|
+
"@spree/admin-sdk": "^0.5.0"
|
|
76
|
+
},
|
|
77
|
+
"scripts": {
|
|
78
|
+
"lint": "biome check src",
|
|
79
|
+
"lint:fix": "biome check --fix src",
|
|
80
|
+
"format": "biome format --write src",
|
|
81
|
+
"typecheck": "tsc --noEmit",
|
|
82
|
+
"test": "vitest run"
|
|
83
|
+
}
|
|
84
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createAdminClient } from '@spree/admin-sdk'
|
|
2
|
+
|
|
3
|
+
// `VITE_SPREE_API_URL` controls whether the SDK builds relative or absolute URLs.
|
|
4
|
+
// Three deployment topologies, one code path:
|
|
5
|
+
//
|
|
6
|
+
// 1. Dev (default, env unset) — Vite dev proxy forwards `/api/*` to
|
|
7
|
+
// Rails. Browser sees same-origin → no CORS, no preflight, the refresh-token
|
|
8
|
+
// cookie rides under `SameSite=Lax` without HTTPS.
|
|
9
|
+
//
|
|
10
|
+
// 2. Prod single-origin (env unset) — Static-host rewrite (Render/nginx/
|
|
11
|
+
// Cloudflare Worker) forwards `/api/*` to Rails. Same code path as dev,
|
|
12
|
+
// same security posture, no CORS config to maintain. This is the
|
|
13
|
+
// recommended topology for self-hosters.
|
|
14
|
+
//
|
|
15
|
+
// 3. Prod cross-origin (env set) — `VITE_SPREE_API_URL=https://api.shop.com`
|
|
16
|
+
// makes the SDK emit absolute URLs. Backend then needs the origin in
|
|
17
|
+
// `Spree::AllowedOrigin`; cookies switch to `SameSite=None; Secure`
|
|
18
|
+
// (handled server-side by Rails.env). Use when the admin lives on a CDN
|
|
19
|
+
// and you don't want an edge proxy.
|
|
20
|
+
const baseUrl = import.meta.env.VITE_SPREE_API_URL || ''
|
|
21
|
+
|
|
22
|
+
export const adminClient = createAdminClient({
|
|
23
|
+
baseUrl,
|
|
24
|
+
// No initial token — auth-provider bootstraps via /auth/refresh on mount.
|
|
25
|
+
})
|