@spree/cli 3.0.1 → 3.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/README.md +1 -0
- package/dist/index.js +235 -204
- package/dist/templates/dashboard-starter/AGENTS.md +71 -0
- package/dist/templates/dashboard-starter/README.md +112 -1
- package/dist/templates/dashboard-starter/package.json +10 -3
- package/dist/templates/dashboard-starter/playwright.config.ts +37 -0
- package/dist/templates/dashboard-starter/src/hooks/.gitkeep +0 -0
- package/dist/templates/dashboard-starter/src/locales/en.json +3 -0
- package/dist/templates/dashboard-starter/src/pages/.gitkeep +0 -0
- package/dist/templates/dashboard-starter/src/plugins.ts +16 -6
- package/dist/templates/dashboard-starter/src/routeTree.gen.ts +21 -0
- package/dist/templates/dashboard-starter/src/schemas/.gitkeep +0 -0
- package/dist/templates/dashboard-starter/src/slots/.gitkeep +0 -0
- package/dist/templates/dashboard-starter/src/tables/.gitkeep +0 -0
- package/dist/templates/dashboard-starter/tsconfig.json +1 -0
- package/dist/templates/dashboard-starter/vitest.config.ts +14 -0
- package/dist/templates/plugin/README.md.tt +8 -15
- package/dist/templates/plugin/packages/dashboard/package.json.tt +7 -3
- package/dist/templates/seller-dashboard-starter/AGENTS.md +75 -0
- package/dist/templates/seller-dashboard-starter/package.json +10 -3
- package/dist/templates/seller-dashboard-starter/playwright.config.ts +37 -0
- package/dist/templates/seller-dashboard-starter/src/hooks/.gitkeep +0 -0
- package/dist/templates/seller-dashboard-starter/src/locales/en.json +3 -0
- package/dist/templates/seller-dashboard-starter/src/pages/.gitkeep +0 -0
- package/dist/templates/seller-dashboard-starter/src/plugins.ts +7 -3
- package/dist/templates/seller-dashboard-starter/src/schemas/.gitkeep +0 -0
- package/dist/templates/seller-dashboard-starter/src/slots/.gitkeep +0 -0
- package/dist/templates/seller-dashboard-starter/src/tables/.gitkeep +0 -0
- package/dist/templates/seller-dashboard-starter/tsconfig.json +1 -0
- package/dist/templates/seller-dashboard-starter/vitest.config.ts +14 -0
- package/package.json +2 -2
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Agent Instructions
|
|
2
|
+
|
|
3
|
+
This is the Spree admin dashboard for this project — a Vite + React app. The
|
|
4
|
+
shell (routes, chrome, resource pages) ships in `@spree/dashboard`; this
|
|
5
|
+
project holds your customizations.
|
|
6
|
+
|
|
7
|
+
Full docs: https://spreecommerce.org/docs/developer/dashboard/overview
|
|
8
|
+
|
|
9
|
+
## Imports
|
|
10
|
+
|
|
11
|
+
Import from `@spree/dashboard` — it re-exports the framework and the design
|
|
12
|
+
system, so there is no need to work out which package an export lives in.
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { defineDashboardPlugin, useStore, Button, ResourceTable } from '@spree/dashboard'
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`@spree/dashboard-core` and `@spree/dashboard-ui` stay importable for the few
|
|
19
|
+
components that exist in both: the design system's takes data as props, the
|
|
20
|
+
framework's fetches its own. The facade gives you the framework's.
|
|
21
|
+
|
|
22
|
+
## Where code goes
|
|
23
|
+
|
|
24
|
+
Organized by kind, one file per resource — the same layout `@spree/dashboard`
|
|
25
|
+
uses internally.
|
|
26
|
+
|
|
27
|
+
| Directory | Holds | Naming |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| `src/plugins.ts` | Registrations only: nav, routes, slots, tables | one file |
|
|
30
|
+
| `src/pages/` | One screen each | `brands.tsx` |
|
|
31
|
+
| `src/hooks/` | Admin API wrappers | `use-brands.ts` |
|
|
32
|
+
| `src/tables/` | `defineTable` calls | `brands.tsx` |
|
|
33
|
+
| `src/schemas/` | Zod schema + `FormValues` | `brand.ts` |
|
|
34
|
+
| `src/slots/` | Widgets injected into built-in pages | `product-brand-card.tsx` |
|
|
35
|
+
| `src/locales/` | Your `admin.*` translation keys | `en.json` |
|
|
36
|
+
|
|
37
|
+
Adding a resource touches one file in each directory. Keep `plugins.ts` a map
|
|
38
|
+
of what exists, not an implementation.
|
|
39
|
+
|
|
40
|
+
## Rules
|
|
41
|
+
|
|
42
|
+
- **The Admin API is the only data source.** Never reach into the Rails app or
|
|
43
|
+
import server-rendered HTML. A missing endpoint is added to the API first.
|
|
44
|
+
- **Wrap API calls in a hook.** Components never call `adminClient` directly.
|
|
45
|
+
- **Query keys are store-scoped.** Build them with `useResourceKey`; a bare
|
|
46
|
+
`['brands']` key leaks one store's data into another after a store switch.
|
|
47
|
+
- **Writes go through `useResourceMutation`**, which maps 422 responses onto
|
|
48
|
+
form fields instead of firing a toast.
|
|
49
|
+
- **Every visible string goes through i18next** — page titles, column labels,
|
|
50
|
+
buttons, empty states, toasts. Never hardcode English in JSX or in a table
|
|
51
|
+
definition. Add each new key to every locale file the project ships.
|
|
52
|
+
- **Schemas hold canonical values, never labels.** Build `{ value, label }`
|
|
53
|
+
pairs at render time so labels stay translatable.
|
|
54
|
+
- **Never embed an SDK entity type in a form-values type.** React Hook Form
|
|
55
|
+
walks every nested key and the SDK's object graph overflows the TypeScript
|
|
56
|
+
compiler.
|
|
57
|
+
- **A sortable or filterable column must be allowlisted for Ransack** on the
|
|
58
|
+
model, or the request fails at runtime.
|
|
59
|
+
- **Destructive actions that fire straight from a click need a confirm** —
|
|
60
|
+
`useConfirm()` with `variant: 'destructive'`.
|
|
61
|
+
|
|
62
|
+
## Checks
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pnpm typecheck
|
|
66
|
+
pnpm lint
|
|
67
|
+
pnpm test # unit tests (Vitest, Node environment)
|
|
68
|
+
pnpm build
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Test the logic between the UI and the API — query keys, payload mapping, permission predicates. Do not write tests that render a component to assert its markup; that tests React. Use `pnpm test:e2e` when a browser is genuinely needed.
|
|
@@ -12,7 +12,7 @@ cp .env.example .env.local # optional — VITE_API_PROXY_TARGET if your API is
|
|
|
12
12
|
pnpm dev # http://localhost:5173
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
The dev server proxies `/api` to your
|
|
15
|
+
The dev server proxies `/api` to your server API so the SPA stays same-origin — don't set `VITE_SPREE_API_URL` in dev (it switches the SDK to absolute cross-origin URLs, which breaks on CORS and the auth cookie).
|
|
16
16
|
|
|
17
17
|
Sign in with an admin account — authentication is interactive (JWT + refresh cookie). No API keys belong in `.env.local`: every `VITE_`-prefixed value is compiled into the client bundle.
|
|
18
18
|
|
|
@@ -29,8 +29,119 @@ That's the whole install. `spreeDashboardPlugin()` (see `vite.config.ts`) discov
|
|
|
29
29
|
|
|
30
30
|
`src/plugins.ts` is yours — register nav entries, routes, slot widgets, and table columns with `defineDashboardPlugin` from `@spree/dashboard-core`. Same API the distributed plugins use; see the [customization quickstart](https://spreecommerce.org/docs/developer/dashboard/customization/quickstart).
|
|
31
31
|
|
|
32
|
+
**Import everything from `@spree/dashboard`.** It re-exports the framework
|
|
33
|
+
(registries, providers, hooks, the Admin API client) and the design system, so
|
|
34
|
+
you never have to remember which package a given export lives in:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { defineDashboardPlugin, useStore, Button, ResourceTable } from '@spree/dashboard'
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`@spree/dashboard-core` and `@spree/dashboard-ui` remain installed and
|
|
41
|
+
importable when you want them directly — a handful of components exist in
|
|
42
|
+
both, where the design system's version takes its data as props and the
|
|
43
|
+
framework's fetches its own. The facade gives you the framework's.
|
|
44
|
+
|
|
45
|
+
Brands will not be your only customization, so this starter ships the same layout `@spree/dashboard` uses internally: organized by kind, one file per resource.
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
src/
|
|
49
|
+
├── plugins.ts registrations only — the map of what you added
|
|
50
|
+
├── pages/ one file per screen brands.tsx, brand-detail.tsx
|
|
51
|
+
├── hooks/ one file per resource use-brands.ts
|
|
52
|
+
├── tables/ one file per resource brands.tsx
|
|
53
|
+
├── schemas/ one file per resource brand.ts
|
|
54
|
+
├── slots/ widgets injected into product-brand-card.tsx
|
|
55
|
+
│ built-in pages
|
|
56
|
+
└── locales/ your translations en.json
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Adding a resource touches one file in each directory, which is why the split
|
|
60
|
+
is by kind rather than by feature — you always know where a thing lives.
|
|
61
|
+
|
|
62
|
+
**`pages/`** — a screen composes `ResourceLayout` with a `PageHeader` and
|
|
63
|
+
renders a `ResourceTable` or a form. It does not fetch data itself. Register
|
|
64
|
+
it as a route in `plugins.ts`:
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
import { BrandsPage } from './pages/brands'
|
|
68
|
+
|
|
69
|
+
defineDashboardPlugin({
|
|
70
|
+
routes: [{ key: 'brands', path: '/brands', component: BrandsPage }],
|
|
71
|
+
})
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**`hooks/`** — named `use-<resource>.ts`, each wrapping the Admin API so
|
|
75
|
+
components never call `adminClient` directly. Build query keys with
|
|
76
|
+
`useResourceKey`, which scopes them to the current store; a bare `['brands']`
|
|
77
|
+
key leaks one store's data into another after a store switch. Use
|
|
78
|
+
`useResourceMutation` for writes — it maps 422 responses onto form fields
|
|
79
|
+
instead of firing a toast.
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import { adminClient, useResourceKey } from '@spree/dashboard-core'
|
|
83
|
+
import { useQuery } from '@tanstack/react-query'
|
|
84
|
+
|
|
85
|
+
export function useBrands() {
|
|
86
|
+
return useQuery({
|
|
87
|
+
queryKey: useResourceKey('brands'),
|
|
88
|
+
queryFn: () => adminClient.request('GET', '/brands'),
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**`tables/`** — one `defineTable` call per resource, at module load, imported
|
|
94
|
+
from `plugins.ts` so it registers before the router mounts. Column changes to
|
|
95
|
+
built-in tables (products, orders, customers) live here too. A column marked
|
|
96
|
+
`sortable` or `filterable` must be allowlisted for Ransack on the model, or
|
|
97
|
+
the request fails.
|
|
98
|
+
|
|
99
|
+
**`schemas/`** — the Zod schema, its inferred `FormValues` type and defaults.
|
|
100
|
+
Hold canonical values only, never label strings: build `{ value, label }`
|
|
101
|
+
pairs at render time so labels stay translatable. Never embed an SDK entity
|
|
102
|
+
type (`Product`, `Order`) in a form-values type — React Hook Form walks every
|
|
103
|
+
nested key and the SDK's object graph overflows the TypeScript compiler.
|
|
104
|
+
Inline a short single-file schema instead.
|
|
105
|
+
|
|
106
|
+
**`slots/`** — components you inject into pages the dashboard already
|
|
107
|
+
renders, one file per widget. A slot widget takes its data from the slot's
|
|
108
|
+
context and, on a page with a host form, binds its inputs to that form with
|
|
109
|
+
`useHostForm()` rather than saving anything itself.
|
|
110
|
+
|
|
111
|
+
**`locales/`** — every visible string goes through i18next, including column
|
|
112
|
+
labels and button text. Keys live under a top-level `admin` object. Load the
|
|
113
|
+
bundle once at the top of `plugins.ts`:
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
import { i18n } from '@spree/dashboard-core'
|
|
117
|
+
import en from './locales/en.json'
|
|
118
|
+
|
|
119
|
+
i18n.addResourceBundle('en', 'translation', en, true, true)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Test
|
|
123
|
+
|
|
124
|
+
Vitest and Playwright are configured. Playwright needs its browser once:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
pnpm exec playwright install chromium
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
pnpm test # unit tests: src/**/*.test.ts
|
|
132
|
+
pnpm test:e2e # end-to-end, through a browser
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Unit tests run in Node rather than a DOM, because what is worth testing here is the logic between your UI and the API: query keys, payload mapping, permission predicates. Rendering a component to assert its markup tests React, not your feature — use an end-to-end test when you need a browser.
|
|
136
|
+
|
|
137
|
+
`pnpm test:e2e` starts the dashboard itself and reuses one you already have running, but expects your API to be up (`spree dev`). Point it elsewhere with `E2E_BASE_URL`.
|
|
138
|
+
|
|
32
139
|
## Build & deploy
|
|
33
140
|
|
|
141
|
+
The app root `Dockerfile` already builds this package and bakes it into the production image, available at `https://yourstore.com/dashboard` URL.
|
|
142
|
+
|
|
143
|
+
If you want to host it elsewhere, eg. on CDN you can build it manually.
|
|
144
|
+
|
|
34
145
|
```bash
|
|
35
146
|
pnpm build # static assets in dist/
|
|
36
147
|
```
|
|
@@ -8,13 +8,18 @@
|
|
|
8
8
|
"dev": "vite",
|
|
9
9
|
"build": "vite build",
|
|
10
10
|
"preview": "vite preview",
|
|
11
|
+
"test": "vitest run --passWithNoTests",
|
|
12
|
+
"test:watch": "vitest",
|
|
13
|
+
"test:e2e": "playwright test",
|
|
11
14
|
"typecheck": "tsc --noEmit",
|
|
12
15
|
"lint": "biome check .",
|
|
13
16
|
"lint:fix": "biome check --fix ."
|
|
14
17
|
},
|
|
15
18
|
"dependencies": {
|
|
16
|
-
"@spree/admin-sdk": "^1.0.0-beta.
|
|
17
|
-
"@spree/dashboard": "^1.0.0-beta.
|
|
19
|
+
"@spree/admin-sdk": "^1.0.0-beta.3",
|
|
20
|
+
"@spree/dashboard": "^1.0.0-beta.4",
|
|
21
|
+
"@spree/dashboard-core": "^1.0.0-beta.4",
|
|
22
|
+
"@spree/dashboard-ui": "^1.0.0-beta.4",
|
|
18
23
|
"@tanstack/react-query": "^5.100.9",
|
|
19
24
|
"@tanstack/react-router": "^1.169.2",
|
|
20
25
|
"react": "^19.2.6",
|
|
@@ -22,11 +27,13 @@
|
|
|
22
27
|
},
|
|
23
28
|
"devDependencies": {
|
|
24
29
|
"@biomejs/biome": "^2.4.14",
|
|
30
|
+
"@playwright/test": "^1.59.1",
|
|
25
31
|
"@types/react": "^19.2.14",
|
|
26
32
|
"@types/react-dom": "^19.2.3",
|
|
27
33
|
"@vitejs/plugin-react": "^6.0.1",
|
|
28
34
|
"typescript": "~6.0.3",
|
|
29
|
-
"vite": "^8.0.16"
|
|
35
|
+
"vite": "^8.0.16",
|
|
36
|
+
"vitest": "^4.1.5"
|
|
30
37
|
},
|
|
31
38
|
"packageManager": "pnpm@11.13.1"
|
|
32
39
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { defineConfig, devices } from '@playwright/test'
|
|
2
|
+
|
|
3
|
+
// End-to-end tests run against your own stack: `spree dev` for the API, and
|
|
4
|
+
// the dev server Playwright starts below. Point them somewhere else with
|
|
5
|
+
// E2E_BASE_URL — a preview build, or a deployed environment.
|
|
6
|
+
const BASE_URL = process.env.E2E_BASE_URL || 'http://localhost:5173'
|
|
7
|
+
|
|
8
|
+
export default defineConfig({
|
|
9
|
+
testDir: './e2e',
|
|
10
|
+
// One worker: specs share a database, so parallel runs interfere unless
|
|
11
|
+
// every spec creates its own records under a unique name.
|
|
12
|
+
fullyParallel: false,
|
|
13
|
+
workers: 1,
|
|
14
|
+
retries: process.env.CI ? 2 : 0,
|
|
15
|
+
timeout: 30_000,
|
|
16
|
+
expect: { timeout: 10_000 },
|
|
17
|
+
reporter: process.env.CI ? [['github'], ['html', { open: 'never' }]] : 'list',
|
|
18
|
+
|
|
19
|
+
use: {
|
|
20
|
+
baseURL: BASE_URL,
|
|
21
|
+
trace: 'on-first-retry',
|
|
22
|
+
screenshot: 'only-on-failure',
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
|
|
26
|
+
|
|
27
|
+
// Starts the dashboard for the run and reuses one you already have open.
|
|
28
|
+
// The API is expected to be running separately (`spree dev`).
|
|
29
|
+
webServer: process.env.E2E_BASE_URL
|
|
30
|
+
? undefined
|
|
31
|
+
: {
|
|
32
|
+
command: 'pnpm dev --port 5173 --strictPort',
|
|
33
|
+
url: BASE_URL,
|
|
34
|
+
reuseExistingServer: true,
|
|
35
|
+
timeout: 60_000,
|
|
36
|
+
},
|
|
37
|
+
})
|
|
File without changes
|
|
File without changes
|
|
@@ -4,20 +4,30 @@
|
|
|
4
4
|
// shared registries — the same API distributed plugins use. Imported once
|
|
5
5
|
// from main.tsx, before the dashboard renders.
|
|
6
6
|
//
|
|
7
|
+
// Keep this file a map of what you added: the registrations live here, the
|
|
8
|
+
// implementations live in pages/, hooks/, tables/ and schemas/. See the
|
|
9
|
+
// README for what belongs where.
|
|
10
|
+
//
|
|
7
11
|
// Example:
|
|
8
12
|
//
|
|
9
13
|
// import { defineDashboardPlugin } from '@spree/dashboard'
|
|
10
14
|
// import { AnalyticsPage } from './pages/analytics'
|
|
11
15
|
//
|
|
12
16
|
// defineDashboardPlugin({
|
|
13
|
-
// nav: [{
|
|
17
|
+
// nav: [{
|
|
18
|
+
// key: 'analytics',
|
|
19
|
+
// label: i18n.t('admin.analytics.nav'),
|
|
20
|
+
// path: '/analytics',
|
|
21
|
+
// position: 650,
|
|
22
|
+
// }],
|
|
14
23
|
// routes: [{ key: 'analytics', path: '/analytics', component: AnalyticsPage }],
|
|
15
24
|
// })
|
|
16
25
|
//
|
|
17
|
-
// Building custom pages? Add the framework and design-system packages first —
|
|
18
|
-
// `pnpm add @spree/dashboard-core @spree/dashboard-ui` — then import hooks,
|
|
19
|
-
// providers, and UI primitives from those.
|
|
20
|
-
//
|
|
21
26
|
// Docs: https://spreecommerce.org/docs/developer/dashboard/customization/quickstart
|
|
22
27
|
|
|
23
|
-
|
|
28
|
+
import { i18n } from '@spree/dashboard'
|
|
29
|
+
import en from './locales/en.json'
|
|
30
|
+
|
|
31
|
+
// Merges your `admin.*` keys into the dashboard's namespace without dropping
|
|
32
|
+
// its own. Add a bundle per locale you ship.
|
|
33
|
+
i18n.addResourceBundle('en', 'translation', en, true, true)
|
|
@@ -52,6 +52,7 @@ import { Route as SettingsPayoutsRouteImport } from './../../dashboard/src/route
|
|
|
52
52
|
import { Route as SettingsPaymentMethodsRouteImport } from './../../dashboard/src/routes/_authenticated/$storeId/settings/payment-methods'
|
|
53
53
|
import { Route as SettingsPackageTypesRouteImport } from './../../dashboard/src/routes/_authenticated/$storeId/settings/package-types'
|
|
54
54
|
import { Route as SettingsMarketsRouteImport } from './../../dashboard/src/routes/_authenticated/$storeId/settings/markets'
|
|
55
|
+
import { Route as SettingsMarketplaceRouteImport } from './../../dashboard/src/routes/_authenticated/$storeId/settings/marketplace'
|
|
55
56
|
import { Route as SettingsIntegrationsRouteImport } from './../../dashboard/src/routes/_authenticated/$storeId/settings/integrations'
|
|
56
57
|
import { Route as SettingsEmailsRouteImport } from './../../dashboard/src/routes/_authenticated/$storeId/settings/emails'
|
|
57
58
|
import { Route as SettingsCustomFieldDefinitionsRouteImport } from './../../dashboard/src/routes/_authenticated/$storeId/settings/custom-field-definitions'
|
|
@@ -324,6 +325,11 @@ const SettingsMarketsRoute = SettingsMarketsRouteImport.update({
|
|
|
324
325
|
path: '/markets',
|
|
325
326
|
getParentRoute: () => SettingsRoute,
|
|
326
327
|
} as any)
|
|
328
|
+
const SettingsMarketplaceRoute = SettingsMarketplaceRouteImport.update({
|
|
329
|
+
id: '/marketplace',
|
|
330
|
+
path: '/marketplace',
|
|
331
|
+
getParentRoute: () => SettingsRoute,
|
|
332
|
+
} as any)
|
|
327
333
|
const SettingsIntegrationsRoute = SettingsIntegrationsRouteImport.update({
|
|
328
334
|
id: '/integrations',
|
|
329
335
|
path: '/integrations',
|
|
@@ -659,6 +665,7 @@ export interface FileRoutesByFullPath {
|
|
|
659
665
|
'/$storeId/settings/custom-field-definitions': typeof SettingsCustomFieldDefinitionsRoute
|
|
660
666
|
'/$storeId/settings/emails': typeof SettingsEmailsRoute
|
|
661
667
|
'/$storeId/settings/integrations': typeof SettingsIntegrationsRoute
|
|
668
|
+
'/$storeId/settings/marketplace': typeof SettingsMarketplaceRoute
|
|
662
669
|
'/$storeId/settings/markets': typeof SettingsMarketsRoute
|
|
663
670
|
'/$storeId/settings/package-types': typeof SettingsPackageTypesRoute
|
|
664
671
|
'/$storeId/settings/payment-methods': typeof SettingsPaymentMethodsRoute
|
|
@@ -756,6 +763,7 @@ export interface FileRoutesByTo {
|
|
|
756
763
|
'/$storeId/settings/custom-field-definitions': typeof SettingsCustomFieldDefinitionsRoute
|
|
757
764
|
'/$storeId/settings/emails': typeof SettingsEmailsRoute
|
|
758
765
|
'/$storeId/settings/integrations': typeof SettingsIntegrationsRoute
|
|
766
|
+
'/$storeId/settings/marketplace': typeof SettingsMarketplaceRoute
|
|
759
767
|
'/$storeId/settings/markets': typeof SettingsMarketsRoute
|
|
760
768
|
'/$storeId/settings/package-types': typeof SettingsPackageTypesRoute
|
|
761
769
|
'/$storeId/settings/payment-methods': typeof SettingsPaymentMethodsRoute
|
|
@@ -857,6 +865,7 @@ export interface FileRoutesById {
|
|
|
857
865
|
'/_authenticated/$storeId/settings/custom-field-definitions': typeof SettingsCustomFieldDefinitionsRoute
|
|
858
866
|
'/_authenticated/$storeId/settings/emails': typeof SettingsEmailsRoute
|
|
859
867
|
'/_authenticated/$storeId/settings/integrations': typeof SettingsIntegrationsRoute
|
|
868
|
+
'/_authenticated/$storeId/settings/marketplace': typeof SettingsMarketplaceRoute
|
|
860
869
|
'/_authenticated/$storeId/settings/markets': typeof SettingsMarketsRoute
|
|
861
870
|
'/_authenticated/$storeId/settings/package-types': typeof SettingsPackageTypesRoute
|
|
862
871
|
'/_authenticated/$storeId/settings/payment-methods': typeof SettingsPaymentMethodsRoute
|
|
@@ -958,6 +967,7 @@ export interface FileRouteTypes {
|
|
|
958
967
|
| '/$storeId/settings/custom-field-definitions'
|
|
959
968
|
| '/$storeId/settings/emails'
|
|
960
969
|
| '/$storeId/settings/integrations'
|
|
970
|
+
| '/$storeId/settings/marketplace'
|
|
961
971
|
| '/$storeId/settings/markets'
|
|
962
972
|
| '/$storeId/settings/package-types'
|
|
963
973
|
| '/$storeId/settings/payment-methods'
|
|
@@ -1055,6 +1065,7 @@ export interface FileRouteTypes {
|
|
|
1055
1065
|
| '/$storeId/settings/custom-field-definitions'
|
|
1056
1066
|
| '/$storeId/settings/emails'
|
|
1057
1067
|
| '/$storeId/settings/integrations'
|
|
1068
|
+
| '/$storeId/settings/marketplace'
|
|
1058
1069
|
| '/$storeId/settings/markets'
|
|
1059
1070
|
| '/$storeId/settings/package-types'
|
|
1060
1071
|
| '/$storeId/settings/payment-methods'
|
|
@@ -1155,6 +1166,7 @@ export interface FileRouteTypes {
|
|
|
1155
1166
|
| '/_authenticated/$storeId/settings/custom-field-definitions'
|
|
1156
1167
|
| '/_authenticated/$storeId/settings/emails'
|
|
1157
1168
|
| '/_authenticated/$storeId/settings/integrations'
|
|
1169
|
+
| '/_authenticated/$storeId/settings/marketplace'
|
|
1158
1170
|
| '/_authenticated/$storeId/settings/markets'
|
|
1159
1171
|
| '/_authenticated/$storeId/settings/package-types'
|
|
1160
1172
|
| '/_authenticated/$storeId/settings/payment-methods'
|
|
@@ -1522,6 +1534,13 @@ declare module '@tanstack/react-router' {
|
|
|
1522
1534
|
preLoaderRoute: typeof SettingsMarketsRouteImport
|
|
1523
1535
|
parentRoute: typeof SettingsRoute
|
|
1524
1536
|
}
|
|
1537
|
+
'/_authenticated/$storeId/settings/marketplace': {
|
|
1538
|
+
id: '/_authenticated/$storeId/settings/marketplace'
|
|
1539
|
+
path: '/marketplace'
|
|
1540
|
+
fullPath: '/$storeId/settings/marketplace'
|
|
1541
|
+
preLoaderRoute: typeof SettingsMarketplaceRouteImport
|
|
1542
|
+
parentRoute: typeof SettingsRoute
|
|
1543
|
+
}
|
|
1525
1544
|
'/_authenticated/$storeId/settings/integrations': {
|
|
1526
1545
|
id: '/_authenticated/$storeId/settings/integrations'
|
|
1527
1546
|
path: '/integrations'
|
|
@@ -1918,6 +1937,7 @@ interface SettingsRouteChildren {
|
|
|
1918
1937
|
SettingsCustomFieldDefinitionsRoute: typeof SettingsCustomFieldDefinitionsRoute
|
|
1919
1938
|
SettingsEmailsRoute: typeof SettingsEmailsRoute
|
|
1920
1939
|
SettingsIntegrationsRoute: typeof SettingsIntegrationsRoute
|
|
1940
|
+
SettingsMarketplaceRoute: typeof SettingsMarketplaceRoute
|
|
1921
1941
|
SettingsMarketsRoute: typeof SettingsMarketsRoute
|
|
1922
1942
|
SettingsPackageTypesRoute: typeof SettingsPackageTypesRoute
|
|
1923
1943
|
SettingsPaymentMethodsRoute: typeof SettingsPaymentMethodsRoute
|
|
@@ -1950,6 +1970,7 @@ const SettingsRouteChildren: SettingsRouteChildren = {
|
|
|
1950
1970
|
SettingsCustomFieldDefinitionsRoute: SettingsCustomFieldDefinitionsRoute,
|
|
1951
1971
|
SettingsEmailsRoute: SettingsEmailsRoute,
|
|
1952
1972
|
SettingsIntegrationsRoute: SettingsIntegrationsRoute,
|
|
1973
|
+
SettingsMarketplaceRoute: SettingsMarketplaceRoute,
|
|
1953
1974
|
SettingsMarketsRoute: SettingsMarketsRoute,
|
|
1954
1975
|
SettingsPackageTypesRoute: SettingsPackageTypesRoute,
|
|
1955
1976
|
SettingsPaymentMethodsRoute: SettingsPaymentMethodsRoute,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config'
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
globals: true,
|
|
6
|
+
// Node, not jsdom: what is worth unit-testing here is the logic between
|
|
7
|
+
// your UI and the API — query keys, payload mapping, permission
|
|
8
|
+
// predicates. Rendering a component to assert its markup tests React.
|
|
9
|
+
// Reach for a browser through Playwright instead (`pnpm test:e2e`).
|
|
10
|
+
environment: 'node',
|
|
11
|
+
include: ['src/**/*.{test,spec}.{ts,tsx}'],
|
|
12
|
+
exclude: ['node_modules/**', 'e2e/**', 'dist/**'],
|
|
13
|
+
},
|
|
14
|
+
})
|
|
@@ -2,32 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
A Spree plugin: **{{name}}**.
|
|
4
4
|
|
|
5
|
-
This
|
|
5
|
+
This monorepo holds the dashboard half of the plugin:
|
|
6
6
|
|
|
7
7
|
- `packages/dashboard/` — TypeScript dashboard plugin (extends the Spree 6.0 admin via [`@spree/dashboard-core`](https://www.npmjs.com/package/@spree/dashboard-core))
|
|
8
|
-
- `engine/` — Ruby Rails engine (Spree extension gem) that ships the Admin API endpoints and ActiveRecord models
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
The example pages call `/api/v3/admin/{{name}}` endpoints through the SDK's `request` method. Those endpoints don't exist yet — they belong in a Spree extension gem (a Rails engine) that you create separately, with its model, Admin API controller and permission scope, as described in [Creating an extension](https://spreecommerce.org/docs/developer/contributing/creating-an-extension). Then point your Spree host's Gemfile at it:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
gem '{{ruby_name}}', path: '../path/to/{{ruby_name}}'
|
|
13
|
+
```
|
|
11
14
|
|
|
12
15
|
## Development setup
|
|
13
16
|
|
|
14
17
|
```bash
|
|
15
|
-
# Install JavaScript dependencies
|
|
16
18
|
pnpm install
|
|
17
|
-
|
|
18
|
-
# Install Ruby dependencies for the engine
|
|
19
|
-
cd engine && bundle install && cd ..
|
|
19
|
+
pnpm build # type-checks the plugin (it ships TypeScript source)
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
## Running against a Spree host app
|
|
23
23
|
|
|
24
|
-
The engine needs to live alongside a Spree host (typically scaffolded via `create-spree-app`). Point your host's Gemfile at the local engine:
|
|
25
|
-
|
|
26
|
-
```ruby
|
|
27
|
-
# in your Spree host's Gemfile
|
|
28
|
-
gem '{{ruby_name}}', path: '../path/to/{{plugin_name}}/engine'
|
|
29
|
-
```
|
|
30
|
-
|
|
31
24
|
Activate the dashboard plugin with a side-effect import in the host's `main.tsx`, before the router mounts. This runs `defineDashboardPlugin` so the nav entry, route, slot widget, and table column register:
|
|
32
25
|
|
|
33
26
|
```tsx
|
|
@@ -45,7 +38,7 @@ If you'd rather opt out of auto-discovery and register manually, pass an explici
|
|
|
45
38
|
- [Plugin overview](https://spreecommerce.org/docs/developer/dashboard/plugins/overview) — packaging + publishing your plugin to npm
|
|
46
39
|
- [Slots catalog](https://spreecommerce.org/docs/developer/dashboard/slots-catalog) — every named slot the dashboard exposes
|
|
47
40
|
- [Custom Admin Endpoints](https://spreecommerce.org/docs/developer/sdk/admin/extending) — adding your own API endpoints
|
|
48
|
-
- [
|
|
41
|
+
- [Creating an extension](https://spreecommerce.org/docs/developer/contributing/creating-an-extension) — the Rails-engine side
|
|
49
42
|
|
|
50
43
|
## License
|
|
51
44
|
|
|
@@ -12,11 +12,15 @@
|
|
|
12
12
|
"exports": {
|
|
13
13
|
".": "./src/index.tsx"
|
|
14
14
|
},
|
|
15
|
+
"sideEffects": [
|
|
16
|
+
"./src/index.tsx"
|
|
17
|
+
],
|
|
15
18
|
"files": [
|
|
16
19
|
"src",
|
|
17
20
|
"README.md"
|
|
18
21
|
],
|
|
19
22
|
"scripts": {
|
|
23
|
+
"build": "tsc --noEmit",
|
|
20
24
|
"lint": "biome check src",
|
|
21
25
|
"lint:fix": "biome check --fix src",
|
|
22
26
|
"typecheck": "tsc --noEmit"
|
|
@@ -28,9 +32,9 @@
|
|
|
28
32
|
}
|
|
29
33
|
},
|
|
30
34
|
"peerDependencies": {
|
|
31
|
-
"@spree/admin-sdk": "
|
|
32
|
-
"@spree/dashboard-core": "
|
|
33
|
-
"@spree/dashboard-ui": "
|
|
35
|
+
"@spree/admin-sdk": "{{spree_admin_sdk_range}}",
|
|
36
|
+
"@spree/dashboard-core": "{{spree_dashboard_core_range}}",
|
|
37
|
+
"@spree/dashboard-ui": "{{spree_dashboard_ui_range}}",
|
|
34
38
|
"@tanstack/react-query": "^5",
|
|
35
39
|
"@tanstack/react-router": "^1",
|
|
36
40
|
"i18next": "^26",
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Agent Instructions
|
|
2
|
+
|
|
3
|
+
This is the Spree seller panel for this marketplace — a Vite + React app. The
|
|
4
|
+
shell ships in `@spree/seller-dashboard`; this project holds your
|
|
5
|
+
customizations. Sellers manage their own catalog and orders here.
|
|
6
|
+
|
|
7
|
+
Full docs: https://spreecommerce.org/docs/developer/dashboard/overview
|
|
8
|
+
|
|
9
|
+
The admin dashboard is the reference implementation: when a component is
|
|
10
|
+
useful to both, extract it rather than copying it.
|
|
11
|
+
|
|
12
|
+
## Imports
|
|
13
|
+
|
|
14
|
+
Import from `@spree/seller-dashboard` — it re-exports the framework and the design
|
|
15
|
+
system, so there is no need to work out which package an export lives in.
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { defineDashboardPlugin, useStore, Button, ResourceTable } from '@spree/seller-dashboard'
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`@spree/dashboard-core` and `@spree/dashboard-ui` stay importable for the few
|
|
22
|
+
components that exist in both: the design system's takes data as props, the
|
|
23
|
+
framework's fetches its own. The facade gives you the framework's.
|
|
24
|
+
|
|
25
|
+
## Where code goes
|
|
26
|
+
|
|
27
|
+
Organized by kind, one file per resource — the same layout the panel
|
|
28
|
+
uses internally.
|
|
29
|
+
|
|
30
|
+
| Directory | Holds | Naming |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| `src/plugins.ts` | Registrations only: nav, slots, tables | one file |
|
|
33
|
+
| `src/pages/` | One screen each | `brands.tsx` |
|
|
34
|
+
| `src/hooks/` | Admin API wrappers | `use-brands.ts` |
|
|
35
|
+
| `src/tables/` | `defineTable` calls | `brands.tsx` |
|
|
36
|
+
| `src/schemas/` | Zod schema + `FormValues` | `brand.ts` |
|
|
37
|
+
| `src/slots/` | Widgets injected into built-in pages | `seller-payout-card.tsx` |
|
|
38
|
+
| `src/locales/` | Your `admin.*` translation keys | `en.json` |
|
|
39
|
+
|
|
40
|
+
Adding a resource touches one file in each directory. Keep `plugins.ts` a map
|
|
41
|
+
of what exists, not an implementation.
|
|
42
|
+
|
|
43
|
+
## Rules
|
|
44
|
+
|
|
45
|
+
- **The Admin API is the only data source.** Never reach into the Rails app or
|
|
46
|
+
import server-rendered HTML. Seller endpoints are scoped to the signed-in
|
|
47
|
+
seller by the server, never by the client. A missing endpoint is added to the API first.
|
|
48
|
+
- **Wrap API calls in a hook.** Components never call `adminClient` directly.
|
|
49
|
+
- **Query keys are store-scoped.** Build them with `useResourceKey`; a bare
|
|
50
|
+
`['brands']` key leaks one store's data into another after a store switch.
|
|
51
|
+
- **Writes go through `useResourceMutation`**, which maps 422 responses onto
|
|
52
|
+
form fields instead of firing a toast.
|
|
53
|
+
- **Every visible string goes through i18next** — page titles, column labels,
|
|
54
|
+
buttons, empty states, toasts. Never hardcode English in JSX or in a table
|
|
55
|
+
definition. Add each new key to every locale file the project ships.
|
|
56
|
+
- **Schemas hold canonical values, never labels.** Build `{ value, label }`
|
|
57
|
+
pairs at render time so labels stay translatable.
|
|
58
|
+
- **Never embed an SDK entity type in a form-values type.** React Hook Form
|
|
59
|
+
walks every nested key and the SDK's object graph overflows the TypeScript
|
|
60
|
+
compiler.
|
|
61
|
+
- **A sortable or filterable column must be allowlisted for Ransack** on the
|
|
62
|
+
model, or the request fails at runtime.
|
|
63
|
+
- **Destructive actions that fire straight from a click need a confirm** —
|
|
64
|
+
`useConfirm()` with `variant: 'destructive'`.
|
|
65
|
+
|
|
66
|
+
## Checks
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pnpm typecheck
|
|
70
|
+
pnpm lint
|
|
71
|
+
pnpm test # unit tests (Vitest, Node environment)
|
|
72
|
+
pnpm build
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Test the logic between the UI and the API — query keys, payload mapping, permission predicates. Do not write tests that render a component to assert its markup; that tests React. Use `pnpm test:e2e` when a browser is genuinely needed.
|
|
@@ -8,13 +8,18 @@
|
|
|
8
8
|
"dev": "vite",
|
|
9
9
|
"build": "vite build",
|
|
10
10
|
"preview": "vite preview",
|
|
11
|
+
"test": "vitest run --passWithNoTests",
|
|
12
|
+
"test:watch": "vitest",
|
|
13
|
+
"test:e2e": "playwright test",
|
|
11
14
|
"typecheck": "tsc --noEmit",
|
|
12
15
|
"lint": "biome check .",
|
|
13
16
|
"lint:fix": "biome check --fix ."
|
|
14
17
|
},
|
|
15
18
|
"dependencies": {
|
|
16
|
-
"@spree/seller-dashboard": "^1.0.0-beta.
|
|
17
|
-
"@spree/
|
|
19
|
+
"@spree/seller-dashboard": "^1.0.0-beta.4",
|
|
20
|
+
"@spree/dashboard-core": "^1.0.0-beta.4",
|
|
21
|
+
"@spree/dashboard-ui": "^1.0.0-beta.4",
|
|
22
|
+
"@spree/seller-sdk": "^1.0.0-beta.3",
|
|
18
23
|
"@tanstack/react-query": "^5.100.9",
|
|
19
24
|
"@tanstack/react-router": "^1.169.2",
|
|
20
25
|
"react": "^19.2.6",
|
|
@@ -22,11 +27,13 @@
|
|
|
22
27
|
},
|
|
23
28
|
"devDependencies": {
|
|
24
29
|
"@biomejs/biome": "^2.4.14",
|
|
30
|
+
"@playwright/test": "^1.59.1",
|
|
25
31
|
"@types/react": "^19.2.14",
|
|
26
32
|
"@types/react-dom": "^19.2.3",
|
|
27
33
|
"@vitejs/plugin-react": "^6.0.1",
|
|
28
34
|
"typescript": "~6.0.3",
|
|
29
|
-
"vite": "^8.0.16"
|
|
35
|
+
"vite": "^8.0.16",
|
|
36
|
+
"vitest": "^4.1.5"
|
|
30
37
|
},
|
|
31
38
|
"packageManager": "pnpm@11.13.1"
|
|
32
39
|
}
|