@spree/dashboard-core 0.13.1 → 1.0.0-beta.1
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 +4 -0
- package/dist/vite/index.js +17 -3
- package/package.json +7 -7
- package/src/api-client.ts +643 -0
- package/src/components/address-form-dialog.test.ts +25 -0
- package/src/components/address-form-dialog.tsx +203 -79
- package/src/components/app-shell.tsx +171 -0
- package/src/components/app-sidebar.tsx +184 -27
- package/src/components/bulk-action-bar.tsx +89 -157
- package/src/components/country-combobox.tsx +69 -34
- package/src/components/country-state-fields.tsx +43 -11
- package/src/components/currency-select.tsx +81 -66
- package/src/components/export-button.tsx +62 -5
- package/src/components/file-upload-field.tsx +32 -10
- package/src/components/import-button.tsx +52 -27
- package/src/components/import-wizard-dialog.tsx +710 -0
- package/src/components/locale-select.tsx +70 -57
- package/src/components/media-picker-sheet.tsx +307 -0
- package/src/components/mobile-top-bar.tsx +75 -0
- package/src/components/nav-main.tsx +197 -27
- package/src/components/page-header.tsx +203 -61
- package/src/components/page-tabs.tsx +4 -1
- package/src/components/preferences-form.tsx +98 -4
- package/src/components/resource-combobox.tsx +52 -4
- package/src/components/resource-picker-sheet.tsx +209 -73
- package/src/components/resource-table.tsx +383 -127
- package/src/components/route-announcer.tsx +64 -0
- package/src/components/section-heading.tsx +95 -0
- package/src/components/settings-sidebar.tsx +290 -60
- package/src/components/sheet-top-bar.tsx +44 -0
- package/src/components/sidebar-search.tsx +83 -0
- package/src/components/sidebar-user.tsx +213 -0
- package/src/components/skip-link.tsx +36 -0
- package/src/components/store-date-picker.tsx +6 -2
- package/src/components/store-switcher.tsx +19 -14
- package/src/components/table-toolbar.tsx +1357 -440
- package/src/components/tag-combobox.tsx +12 -4
- package/src/components/top-bar-breadcrumbs.tsx +235 -0
- package/src/components/wizard-close-guard.test.ts +54 -0
- package/src/hooks/use-auto-collapse-sidebar.ts +57 -0
- package/src/hooks/use-command-palette.tsx +13 -0
- package/src/hooks/use-countries.test.ts +56 -0
- package/src/hooks/use-countries.ts +51 -5
- package/src/hooks/use-direct-upload.ts +16 -3
- package/src/hooks/use-export.ts +51 -16
- package/src/hooks/use-import.ts +241 -16
- package/src/hooks/use-package-types.ts +132 -0
- package/src/hooks/use-resource-mutation.ts +74 -13
- package/src/hooks/use-stock-locations.ts +131 -0
- package/src/hooks/use-switch-admin-locale.ts +2 -2
- package/src/hooks/use-visual-viewport-offset.ts +61 -0
- package/src/index.ts +45 -1
- package/src/lib/cancel-order-form.ts +27 -0
- package/src/lib/create-action-registry.ts +278 -0
- package/src/lib/date-presets.test.ts +86 -0
- package/src/lib/date-presets.ts +98 -0
- package/src/lib/delivery-form.ts +15 -0
- package/src/lib/docs.ts +32 -0
- package/src/lib/download.ts +13 -1
- package/src/lib/filters-to-ransack.test.ts +113 -0
- package/src/lib/filters-to-ransack.ts +22 -1
- package/src/lib/form-errors.ts +122 -10
- package/src/lib/form-mappers.ts +76 -0
- package/src/lib/fulfillment-items.test.ts +213 -0
- package/src/lib/fulfillment-items.ts +171 -0
- package/src/lib/i18n.ts +19 -11
- package/src/lib/import-types.ts +25 -0
- package/src/lib/label-upload-form.ts +22 -0
- package/src/lib/nav-registry.ts +71 -3
- package/src/lib/permissions.ts +46 -1
- package/src/lib/progress.ts +18 -0
- package/src/lib/query-keys.ts +13 -13
- package/src/lib/sample-csv.ts +9 -6
- package/src/lib/search-registry.ts +6 -2
- package/src/lib/settings-nav-registry.ts +104 -11
- package/src/lib/table-registry.ts +50 -0
- package/src/lib/type-labels.test.ts +68 -0
- package/src/lib/type-labels.ts +78 -0
- package/src/locales/ar.json +1889 -517
- package/src/locales/de.json +1884 -517
- package/src/locales/en.json +1886 -519
- package/src/locales/fr.json +1884 -517
- package/src/locales/pl.json +1913 -517
- package/src/locales/zh-CN.json +1883 -517
- package/src/pages/package-types.tsx +484 -0
- package/src/pages/settings-index.tsx +112 -0
- package/src/pages/stock-locations.tsx +674 -0
- package/src/plugin.ts +29 -0
- package/src/products/add-video-dialog.tsx +100 -0
- package/src/products/bulk-variants-dialog.tsx +301 -0
- package/src/products/carton-size.test.ts +51 -0
- package/src/products/carton-size.ts +20 -0
- package/src/products/currency-parts.ts +21 -0
- package/src/products/inventory-section.tsx +259 -0
- package/src/products/media-edit-sheet.tsx +336 -0
- package/src/products/normalize-customs.test.ts +39 -0
- package/src/products/normalize-customs.ts +27 -0
- package/src/products/normalize-money.test.ts +58 -0
- package/src/products/normalize-money.ts +73 -0
- package/src/products/normalize-quantity.test.ts +32 -0
- package/src/products/normalize-quantity.ts +31 -0
- package/src/products/product-bulk-price-editor.tsx +165 -0
- package/src/products/product-form-cards.tsx +1167 -0
- package/src/products/product-form-mapping.ts +212 -0
- package/src/products/product-schema.ts +318 -0
- package/src/products/product-types.ts +130 -0
- package/src/products/use-product-form-data.ts +273 -0
- package/src/products/variant-edit-sheet.tsx +725 -0
- package/src/products/variants-matrix.test.ts +154 -0
- package/src/products/variants-matrix.ts +244 -0
- package/src/products/variants-options-builder.tsx +571 -0
- package/src/products/variants-section.tsx +535 -0
- package/src/products/video-url.test.ts +71 -0
- package/src/products/video-url.ts +83 -0
- package/src/providers/auth-provider.tsx +70 -12
- package/src/providers/permission-provider.tsx +68 -28
- package/src/providers/sticky-header-provider.tsx +134 -0
- package/src/providers/store-provider.tsx +36 -3
- package/src/providers/tenant-provider.tsx +33 -0
- package/src/schemas/package-type.ts +95 -0
- package/src/schemas/stock-location.ts +126 -0
- package/src/tables/package-types.tsx +74 -0
- package/src/tables/stock-locations.tsx +124 -0
- package/src/vite/index.ts +40 -7
- package/src/components/top-bar.tsx +0 -209
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { requiredMessage } from '@spree/dashboard-ui'
|
|
2
|
+
import { z } from 'zod/v4'
|
|
3
|
+
import type { PanelStockLocation, PanelStockLocationCreateParams } from '../api-client'
|
|
4
|
+
import { blankToUndefined, emptyToUndefined } from '../lib/form-mappers'
|
|
5
|
+
|
|
6
|
+
// Labels live in `en.json` under `admin.stock_locations.kinds.*` and
|
|
7
|
+
// `admin.stock_locations.pickup_stock_policies.*`. Consumers map these
|
|
8
|
+
// values to translated labels at render time.
|
|
9
|
+
export const STOCK_LOCATION_KINDS = ['warehouse', 'store', 'fulfillment_center'] as const
|
|
10
|
+
export const PICKUP_STOCK_POLICIES = ['local', 'any'] as const
|
|
11
|
+
export type PickupStockPolicy = (typeof PICKUP_STOCK_POLICIES)[number]
|
|
12
|
+
|
|
13
|
+
export const stockLocationFormSchema = z.object({
|
|
14
|
+
name: z
|
|
15
|
+
.string()
|
|
16
|
+
.trim()
|
|
17
|
+
.min(1, { error: requiredMessage('name') }),
|
|
18
|
+
admin_name: z.string().optional(),
|
|
19
|
+
kind: z.enum(STOCK_LOCATION_KINDS),
|
|
20
|
+
active: z.boolean(),
|
|
21
|
+
default: z.boolean(),
|
|
22
|
+
propagate_all_variants: z.boolean(),
|
|
23
|
+
backorderable_default: z.boolean(),
|
|
24
|
+
address1: z.string().optional(),
|
|
25
|
+
address2: z.string().optional(),
|
|
26
|
+
city: z.string().optional(),
|
|
27
|
+
zipcode: z.string().optional(),
|
|
28
|
+
phone: z.string().optional(),
|
|
29
|
+
company: z.string().optional(),
|
|
30
|
+
country_code: z.string().optional(),
|
|
31
|
+
state_code: z.string().optional(),
|
|
32
|
+
state_name: z.string().optional(),
|
|
33
|
+
pickup_enabled: z.boolean(),
|
|
34
|
+
returns_enabled: z.boolean(),
|
|
35
|
+
pickup_stock_policy: z.enum(PICKUP_STOCK_POLICIES),
|
|
36
|
+
pickup_ready_in_minutes: z.preprocess(
|
|
37
|
+
emptyToUndefined,
|
|
38
|
+
z.coerce.number().int().min(0).optional().nullable(),
|
|
39
|
+
),
|
|
40
|
+
pickup_instructions: z.string().optional(),
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
export type StockLocationFormValues = z.infer<typeof stockLocationFormSchema>
|
|
44
|
+
|
|
45
|
+
export const STOCK_LOCATION_DEFAULTS: StockLocationFormValues = {
|
|
46
|
+
name: '',
|
|
47
|
+
admin_name: '',
|
|
48
|
+
kind: 'warehouse',
|
|
49
|
+
active: true,
|
|
50
|
+
default: false,
|
|
51
|
+
propagate_all_variants: false,
|
|
52
|
+
backorderable_default: false,
|
|
53
|
+
address1: '',
|
|
54
|
+
address2: '',
|
|
55
|
+
city: '',
|
|
56
|
+
zipcode: '',
|
|
57
|
+
phone: '',
|
|
58
|
+
company: '',
|
|
59
|
+
country_code: '',
|
|
60
|
+
state_code: '',
|
|
61
|
+
state_name: '',
|
|
62
|
+
pickup_enabled: false,
|
|
63
|
+
// Locations accept returns unless a merchant narrows it, which is how
|
|
64
|
+
// goods behaved before the flag existed.
|
|
65
|
+
returns_enabled: true,
|
|
66
|
+
pickup_stock_policy: 'local',
|
|
67
|
+
pickup_ready_in_minutes: null,
|
|
68
|
+
pickup_instructions: '',
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function stockLocationToFormValues(sl: PanelStockLocation): StockLocationFormValues {
|
|
72
|
+
return {
|
|
73
|
+
name: sl.name,
|
|
74
|
+
admin_name: sl.admin_name ?? '',
|
|
75
|
+
kind: STOCK_LOCATION_KINDS.includes(sl.kind as (typeof STOCK_LOCATION_KINDS)[number])
|
|
76
|
+
? (sl.kind as (typeof STOCK_LOCATION_KINDS)[number])
|
|
77
|
+
: 'warehouse',
|
|
78
|
+
active: sl.active,
|
|
79
|
+
default: sl.default,
|
|
80
|
+
propagate_all_variants: sl.propagate_all_variants ?? false,
|
|
81
|
+
backorderable_default: sl.backorderable_default ?? false,
|
|
82
|
+
address1: sl.address1 ?? '',
|
|
83
|
+
address2: sl.address2 ?? '',
|
|
84
|
+
city: sl.city ?? '',
|
|
85
|
+
zipcode: sl.zipcode ?? '',
|
|
86
|
+
phone: sl.phone ?? '',
|
|
87
|
+
company: sl.company ?? '',
|
|
88
|
+
country_code: sl.country_code ?? '',
|
|
89
|
+
state_code: sl.state_code ?? '',
|
|
90
|
+
state_name: sl.state_name ?? '',
|
|
91
|
+
pickup_enabled: sl.pickup_enabled ?? false,
|
|
92
|
+
returns_enabled: sl.returns_enabled ?? true,
|
|
93
|
+
pickup_stock_policy: PICKUP_STOCK_POLICIES.includes(sl.pickup_stock_policy as PickupStockPolicy)
|
|
94
|
+
? (sl.pickup_stock_policy as PickupStockPolicy)
|
|
95
|
+
: 'local',
|
|
96
|
+
pickup_ready_in_minutes: sl.pickup_ready_in_minutes ?? null,
|
|
97
|
+
pickup_instructions: sl.pickup_instructions ?? '',
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Drops blank strings → undefined so we don't overwrite null fields with "".
|
|
102
|
+
export function formValuesToParams(v: StockLocationFormValues): PanelStockLocationCreateParams {
|
|
103
|
+
return {
|
|
104
|
+
name: v.name,
|
|
105
|
+
admin_name: blankToUndefined(v.admin_name),
|
|
106
|
+
kind: v.kind,
|
|
107
|
+
active: v.active,
|
|
108
|
+
default: v.default,
|
|
109
|
+
propagate_all_variants: v.propagate_all_variants,
|
|
110
|
+
backorderable_default: v.backorderable_default,
|
|
111
|
+
address1: blankToUndefined(v.address1),
|
|
112
|
+
address2: blankToUndefined(v.address2),
|
|
113
|
+
city: blankToUndefined(v.city),
|
|
114
|
+
zipcode: blankToUndefined(v.zipcode),
|
|
115
|
+
phone: blankToUndefined(v.phone),
|
|
116
|
+
company: blankToUndefined(v.company),
|
|
117
|
+
country_code: blankToUndefined(v.country_code),
|
|
118
|
+
state_code: blankToUndefined(v.state_code),
|
|
119
|
+
state_name: blankToUndefined(v.state_name),
|
|
120
|
+
pickup_enabled: v.pickup_enabled,
|
|
121
|
+
returns_enabled: v.returns_enabled,
|
|
122
|
+
pickup_stock_policy: v.pickup_stock_policy,
|
|
123
|
+
pickup_ready_in_minutes: v.pickup_ready_in_minutes ?? null,
|
|
124
|
+
pickup_instructions: blankToUndefined(v.pickup_instructions),
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { RelativeTime, ResourceNameCell } from '@spree/dashboard-ui'
|
|
2
|
+
import { PackageIcon } from '@spree/dashboard-ui/icons'
|
|
3
|
+
import i18n from 'i18next'
|
|
4
|
+
import type { PanelPackageType } from '../api-client'
|
|
5
|
+
import { defineTable } from '../lib/table-registry'
|
|
6
|
+
|
|
7
|
+
defineTable<PanelPackageType>('package-types', {
|
|
8
|
+
docsPath: 'settings/package-types',
|
|
9
|
+
description: i18n.t('admin.package_types.description'),
|
|
10
|
+
title: i18n.t('admin.package_types.table_title'),
|
|
11
|
+
searchParam: 'name_cont',
|
|
12
|
+
searchPlaceholder: i18n.t('admin.package_types.search_placeholder'),
|
|
13
|
+
defaultSort: { field: 'name', direction: 'asc' },
|
|
14
|
+
emptyIcon: <PackageIcon className="size-8 text-muted-foreground" />,
|
|
15
|
+
emptyMessage: i18n.t('admin.package_types.empty'),
|
|
16
|
+
columns: [
|
|
17
|
+
{
|
|
18
|
+
key: 'name',
|
|
19
|
+
label: i18n.t('admin.fields.package_type.name.label'),
|
|
20
|
+
sortable: true,
|
|
21
|
+
default: true,
|
|
22
|
+
render: (packageType) => (
|
|
23
|
+
<ResourceNameCell
|
|
24
|
+
id={packageType.id}
|
|
25
|
+
// Omitted on a row this panel may read but not write, so the row
|
|
26
|
+
// click bridge never opens an edit sheet whose save could only
|
|
27
|
+
// 404. A seller sees the marketplace's packaging to know what they
|
|
28
|
+
// may pack into; it is not theirs to change.
|
|
29
|
+
dataAttr={packageType.editable === false ? undefined : 'data-package-type-id'}
|
|
30
|
+
name={packageType.name}
|
|
31
|
+
// This owner's own box, called out because every parcel quote from
|
|
32
|
+
// their warehouse is built on it.
|
|
33
|
+
secondary={packageType.default ? i18n.t('admin.package_types.default_badge') : undefined}
|
|
34
|
+
/>
|
|
35
|
+
),
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
key: 'kind',
|
|
39
|
+
label: i18n.t('admin.fields.package_type.kind.label'),
|
|
40
|
+
sortable: true,
|
|
41
|
+
default: true,
|
|
42
|
+
render: (packageType) => i18n.t(`admin.package_types.kinds.${packageType.kind}`),
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
key: 'dimensions',
|
|
46
|
+
label: i18n.t('admin.package_types.dimensions_column'),
|
|
47
|
+
default: true,
|
|
48
|
+
className: 'text-sm text-muted-foreground whitespace-nowrap',
|
|
49
|
+
// Three numbers mean nothing without the unit they were measured in —
|
|
50
|
+
// the same figures read as inches or centimetres differ sixteenfold by
|
|
51
|
+
// volume, which is what the freight tiers compare.
|
|
52
|
+
render: (packageType) =>
|
|
53
|
+
packageType.length && packageType.width && packageType.height
|
|
54
|
+
? `${packageType.length} × ${packageType.width} × ${packageType.height} ${packageType.dimensions_unit}`
|
|
55
|
+
: '—',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
key: 'volume',
|
|
59
|
+
label: i18n.t('admin.package_types.volume_column'),
|
|
60
|
+
className: 'text-sm text-muted-foreground whitespace-nowrap',
|
|
61
|
+
render: (packageType) =>
|
|
62
|
+
packageType.volume ? `${Number(packageType.volume).toFixed(3)} m³` : '—',
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
key: 'created_at',
|
|
66
|
+
label: i18n.t('admin.fields.created_at.label'),
|
|
67
|
+
sortable: true,
|
|
68
|
+
filterable: true,
|
|
69
|
+
filterType: 'date',
|
|
70
|
+
className: 'text-sm text-muted-foreground whitespace-nowrap',
|
|
71
|
+
render: (packageType) => <RelativeTime iso={packageType.created_at} />,
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
})
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { ActiveBadge, ResourceNameCell } from '@spree/dashboard-ui'
|
|
2
|
+
import { WarehouseIcon } from '@spree/dashboard-ui/icons'
|
|
3
|
+
import i18n from 'i18next'
|
|
4
|
+
import type { PanelStockLocation } from '../api-client'
|
|
5
|
+
import { defineTable } from '../lib/table-registry'
|
|
6
|
+
|
|
7
|
+
defineTable<PanelStockLocation>('stock-locations', {
|
|
8
|
+
docsPath: 'settings/locations',
|
|
9
|
+
description: i18n.t('admin.stock_locations.description'),
|
|
10
|
+
title: i18n.t('admin.settings_nav.items.stock_locations'),
|
|
11
|
+
searchParam: 'name_cont',
|
|
12
|
+
searchPlaceholder: i18n.t('admin.stock_locations.search_placeholder'),
|
|
13
|
+
defaultSort: { field: 'name', direction: 'asc' },
|
|
14
|
+
emptyIcon: <WarehouseIcon className="size-8 text-muted-foreground" />,
|
|
15
|
+
emptyMessage: i18n.t('admin.stock_locations.empty'),
|
|
16
|
+
columns: [
|
|
17
|
+
{
|
|
18
|
+
key: 'name',
|
|
19
|
+
label: i18n.t('admin.fields.name.label'),
|
|
20
|
+
sortable: true,
|
|
21
|
+
default: true,
|
|
22
|
+
render: (sl) => (
|
|
23
|
+
<ResourceNameCell
|
|
24
|
+
id={sl.id}
|
|
25
|
+
dataAttr="data-stock-location-id"
|
|
26
|
+
name={sl.name}
|
|
27
|
+
secondary={sl.admin_name}
|
|
28
|
+
/>
|
|
29
|
+
),
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
key: 'kind',
|
|
33
|
+
label: i18n.t('admin.stock_locations.columns.type'),
|
|
34
|
+
sortable: true,
|
|
35
|
+
filterable: true,
|
|
36
|
+
default: true,
|
|
37
|
+
filterType: 'enum',
|
|
38
|
+
filterOptions: [
|
|
39
|
+
{ value: 'warehouse', label: i18n.t('admin.stock_locations.kinds.warehouse') },
|
|
40
|
+
{ value: 'store', label: i18n.t('admin.stock_locations.kinds.store') },
|
|
41
|
+
{
|
|
42
|
+
value: 'fulfillment_center',
|
|
43
|
+
label: i18n.t('admin.stock_locations.kinds.fulfillment_center'),
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
quickFilter: true,
|
|
47
|
+
render: (sl) => {
|
|
48
|
+
const key = `admin.stock_locations.kinds.${sl.kind}`
|
|
49
|
+
const label = i18n.exists(key) ? i18n.t(key) : sl.kind.replace('_', ' ')
|
|
50
|
+
return <span className="capitalize">{label}</span>
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
key: 'city',
|
|
55
|
+
label: i18n.t('admin.stock_locations.columns.location'),
|
|
56
|
+
default: true,
|
|
57
|
+
render: (sl) => {
|
|
58
|
+
const parts = [sl.city, sl.state_text || sl.state_code, sl.country_code]
|
|
59
|
+
.filter(Boolean)
|
|
60
|
+
.join(', ')
|
|
61
|
+
return parts || '—'
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
key: 'pickup_enabled',
|
|
66
|
+
label: i18n.t('admin.stock_locations.columns.pickup'),
|
|
67
|
+
sortable: true,
|
|
68
|
+
filterable: true,
|
|
69
|
+
default: true,
|
|
70
|
+
filterType: 'boolean',
|
|
71
|
+
render: (sl) => (
|
|
72
|
+
<ActiveBadge
|
|
73
|
+
active={sl.pickup_enabled}
|
|
74
|
+
activeLabel={i18n.t('admin.stock_locations.badges.enabled')}
|
|
75
|
+
dashWhenInactive
|
|
76
|
+
/>
|
|
77
|
+
),
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
key: 'returns_enabled',
|
|
81
|
+
label: i18n.t('admin.stock_locations.columns.returns'),
|
|
82
|
+
sortable: true,
|
|
83
|
+
filterable: true,
|
|
84
|
+
default: true,
|
|
85
|
+
filterType: 'boolean',
|
|
86
|
+
render: (sl) => (
|
|
87
|
+
<ActiveBadge
|
|
88
|
+
active={sl.returns_enabled ?? true}
|
|
89
|
+
activeLabel={i18n.t('admin.stock_locations.badges.enabled')}
|
|
90
|
+
dashWhenInactive
|
|
91
|
+
/>
|
|
92
|
+
),
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
key: 'active',
|
|
96
|
+
label: i18n.t('admin.fields.active.label'),
|
|
97
|
+
sortable: true,
|
|
98
|
+
filterable: true,
|
|
99
|
+
default: true,
|
|
100
|
+
filterType: 'boolean',
|
|
101
|
+
render: (sl) => (
|
|
102
|
+
<ActiveBadge
|
|
103
|
+
active={sl.active}
|
|
104
|
+
activeLabel={i18n.t('admin.fields.active.label')}
|
|
105
|
+
inactiveLabel={i18n.t('admin.stock_locations.badges.inactive')}
|
|
106
|
+
/>
|
|
107
|
+
),
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
key: 'default',
|
|
111
|
+
label: i18n.t('admin.stock_locations.columns.default'),
|
|
112
|
+
default: true,
|
|
113
|
+
filterable: true,
|
|
114
|
+
filterType: 'boolean',
|
|
115
|
+
render: (sl) => (
|
|
116
|
+
<ActiveBadge
|
|
117
|
+
active={sl.default}
|
|
118
|
+
activeLabel={i18n.t('admin.stock_locations.badges.default')}
|
|
119
|
+
dashWhenInactive
|
|
120
|
+
/>
|
|
121
|
+
),
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
})
|
package/src/vite/index.ts
CHANGED
|
@@ -21,6 +21,17 @@ export interface SpreeDashboardPluginOptions {
|
|
|
21
21
|
*/
|
|
22
22
|
cssEntry?: string
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Shell packages whose source Tailwind should scan, on top of
|
|
26
|
+
* `@spree/dashboard-core` and `@spree/dashboard-ui`. Defaults to
|
|
27
|
+
* `['@spree/dashboard']`, the operator's dashboard. A different panel — a
|
|
28
|
+
* seller's, or a host's own shell — passes its own package name here so its
|
|
29
|
+
* components' classes survive the build. Unresolvable names are skipped
|
|
30
|
+
* rather than erroring: a custom dashboard composed straight from core + ui
|
|
31
|
+
* has no shell at all.
|
|
32
|
+
*/
|
|
33
|
+
shellPackages?: string[]
|
|
34
|
+
|
|
24
35
|
/**
|
|
25
36
|
* Names of dashboard plugin packages installed in the host app. Each name
|
|
26
37
|
* must be a resolvable npm package specifier — the same string you'd pass
|
|
@@ -83,11 +94,29 @@ export interface SpreeDashboardPluginOptions {
|
|
|
83
94
|
*
|
|
84
95
|
* spreeDashboardPlugin({
|
|
85
96
|
* cssEntry: './src/admin.css',
|
|
86
|
-
* plugins: ['@my-store/
|
|
97
|
+
* plugins: ['@my-store/seller-portal-plugin'], // explicit whitelist opt-out
|
|
87
98
|
* })
|
|
88
99
|
*/
|
|
89
100
|
export function spreeDashboardPlugin(options: SpreeDashboardPluginOptions = {}): PluginOption[] {
|
|
90
|
-
return [dashboardTailwindSourcePlugin(options), tailwindcss()]
|
|
101
|
+
return [dashboardProseMirrorDedupePlugin(), dashboardTailwindSourcePlugin(options), tailwindcss()]
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* TipTap 3.31's `@tiptap/pm` asks for `prosemirror-model` ^1.25.11, but
|
|
106
|
+
* `prosemirror-schema-list` still resolves 1.25.4. Two copies in the
|
|
107
|
+
* bundle make `wrapInList` throw. Force Vite onto one module.
|
|
108
|
+
*/
|
|
109
|
+
function dashboardProseMirrorDedupePlugin(): Plugin {
|
|
110
|
+
return {
|
|
111
|
+
name: 'spree:dashboard-prosemirror-dedupe',
|
|
112
|
+
config() {
|
|
113
|
+
return {
|
|
114
|
+
resolve: {
|
|
115
|
+
dedupe: ['prosemirror-model', 'prosemirror-view', '@tiptap/pm', '@tiptap/core'],
|
|
116
|
+
},
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
}
|
|
91
120
|
}
|
|
92
121
|
|
|
93
122
|
/** Module specifier hosts import to activate discovered dashboard plugins. */
|
|
@@ -155,11 +184,15 @@ function dashboardTailwindSourcePlugin(options: SpreeDashboardPluginOptions): Pl
|
|
|
155
184
|
pluginErrors.push({ pkg, kind: 'unresolved' })
|
|
156
185
|
}
|
|
157
186
|
}
|
|
158
|
-
// The app shell is optional: present when the host builds on
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
|
|
162
|
-
|
|
187
|
+
// The app shell is optional: present when the host builds on a full
|
|
188
|
+
// shell package, absent for custom dashboards composed directly from
|
|
189
|
+
// core + ui — hence no error when unresolved. Which shell varies by
|
|
190
|
+
// panel: `@spree/dashboard` for the operator's, `@spree/seller-dashboard`
|
|
191
|
+
// for a seller's, or a host's own.
|
|
192
|
+
for (const shell of options.shellPackages ?? ['@spree/dashboard']) {
|
|
193
|
+
const shellDir = resolvePackageSourceDir(shell, fromHost)
|
|
194
|
+
if (shellDir) resolvedSources.push(shellDir)
|
|
195
|
+
}
|
|
163
196
|
for (const pkg of pluginPackages) {
|
|
164
197
|
const dir = resolvePackageSourceDir(pkg, fromHost)
|
|
165
198
|
if (dir) {
|
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Avatar,
|
|
3
|
-
AvatarFallback,
|
|
4
|
-
AvatarImage,
|
|
5
|
-
Button,
|
|
6
|
-
DropdownMenu,
|
|
7
|
-
DropdownMenuContent,
|
|
8
|
-
DropdownMenuItem,
|
|
9
|
-
DropdownMenuSeparator,
|
|
10
|
-
DropdownMenuTrigger,
|
|
11
|
-
LanguageMenuItems,
|
|
12
|
-
SidebarTrigger,
|
|
13
|
-
ThemeMenuItems,
|
|
14
|
-
} from '@spree/dashboard-ui'
|
|
15
|
-
import { Link } from '@tanstack/react-router'
|
|
16
|
-
import {
|
|
17
|
-
BookOpenIcon,
|
|
18
|
-
ExternalLinkIcon,
|
|
19
|
-
LogOutIcon,
|
|
20
|
-
MailIcon,
|
|
21
|
-
MessageCircleIcon,
|
|
22
|
-
SearchIcon,
|
|
23
|
-
UserIcon,
|
|
24
|
-
} from 'lucide-react'
|
|
25
|
-
import { useTranslation } from 'react-i18next'
|
|
26
|
-
import { useAuth } from '../hooks/use-auth'
|
|
27
|
-
import { useCommandPalette } from '../hooks/use-command-palette'
|
|
28
|
-
import { useSwitchAdminLocale } from '../hooks/use-switch-admin-locale'
|
|
29
|
-
import { getInitials } from '../lib/formatters'
|
|
30
|
-
import { i18n } from '../lib/i18n'
|
|
31
|
-
import { storefrontHref } from '../lib/storefront'
|
|
32
|
-
import { useStore } from '../providers/store-provider'
|
|
33
|
-
|
|
34
|
-
const IS_MAC = typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.platform ?? '')
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Top-bar shell for the admin SPA. Three-slot layout:
|
|
38
|
-
* [☰ sidebar toggle] [global search] [view store · user menu]
|
|
39
|
-
*
|
|
40
|
-
* The search trigger opens the global ⌘K command palette (provider mounted at
|
|
41
|
-
* the layout root). The keyboard shortcut is registered in the provider, not
|
|
42
|
-
* here, so it works regardless of focus location.
|
|
43
|
-
*/
|
|
44
|
-
/**
|
|
45
|
-
* @param uiLocales Admin UI languages for the in-menu switcher. The app owns
|
|
46
|
-
* which locale bundles ship (see the dashboard's `getAvailableUiLocales`) and
|
|
47
|
-
* injects them here, so this core component stays free of bundle knowledge.
|
|
48
|
-
*/
|
|
49
|
-
export function TopBar({
|
|
50
|
-
uiLocales = [],
|
|
51
|
-
}: {
|
|
52
|
-
uiLocales?: ReadonlyArray<{ code: string; name: string }>
|
|
53
|
-
}) {
|
|
54
|
-
return (
|
|
55
|
-
<header className="sticky top-0 z-40 flex h-header-height shrink-0 items-center gap-3 bg-background/90 px-4 border-b border-border/50 backdrop-blur supports-[backdrop-filter]:bg-background/75">
|
|
56
|
-
<SidebarTrigger className="-ml-1 h-8 w-8" />
|
|
57
|
-
|
|
58
|
-
<div className="flex flex-1 justify-center">
|
|
59
|
-
<SearchTrigger />
|
|
60
|
-
</div>
|
|
61
|
-
|
|
62
|
-
<div className="flex items-center gap-2">
|
|
63
|
-
<ViewStoreLink />
|
|
64
|
-
<TopBarUser uiLocales={uiLocales} />
|
|
65
|
-
</div>
|
|
66
|
-
</header>
|
|
67
|
-
)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// ---------------------------------------------------------------------------
|
|
71
|
-
// Search trigger — opens the global command palette
|
|
72
|
-
// ---------------------------------------------------------------------------
|
|
73
|
-
|
|
74
|
-
function SearchTrigger() {
|
|
75
|
-
const { t } = useTranslation()
|
|
76
|
-
const { setOpen } = useCommandPalette()
|
|
77
|
-
|
|
78
|
-
return (
|
|
79
|
-
<button
|
|
80
|
-
type="button"
|
|
81
|
-
onClick={() => setOpen(true)}
|
|
82
|
-
className="flex w-full max-w-md items-center gap-2 rounded-lg border border-border bg-muted/40 px-2 py-1.5 text-sm text-muted-foreground transition-colors hover:bg-muted"
|
|
83
|
-
>
|
|
84
|
-
<SearchIcon className="size-4" />
|
|
85
|
-
<span className="flex-1 text-left">{t('admin.components.command_palette.placeholder')}</span>
|
|
86
|
-
<kbd className="hidden rounded border bg-background px-1.5 py-0.5 font-mono text-xs sm:inline-flex">
|
|
87
|
-
{IS_MAC ? '⌘K' : 'Ctrl+K'}
|
|
88
|
-
</kbd>
|
|
89
|
-
</button>
|
|
90
|
-
)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// ---------------------------------------------------------------------------
|
|
94
|
-
// View Store link
|
|
95
|
-
// ---------------------------------------------------------------------------
|
|
96
|
-
|
|
97
|
-
function ViewStoreLink() {
|
|
98
|
-
const { t } = useTranslation()
|
|
99
|
-
const { store } = useStore()
|
|
100
|
-
const href = storefrontHref(store)
|
|
101
|
-
if (!href) return null
|
|
102
|
-
|
|
103
|
-
return (
|
|
104
|
-
<Button asChild variant="ghost" size="sm">
|
|
105
|
-
<a href={href} target="_blank" rel="noreferrer">
|
|
106
|
-
<ExternalLinkIcon className="size-4" />
|
|
107
|
-
<span className="hidden sm:inline">{t('admin.account.view_store')}</span>
|
|
108
|
-
</a>
|
|
109
|
-
</Button>
|
|
110
|
-
)
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// ---------------------------------------------------------------------------
|
|
114
|
-
// User menu
|
|
115
|
-
// ---------------------------------------------------------------------------
|
|
116
|
-
|
|
117
|
-
function TopBarUser({ uiLocales }: { uiLocales: ReadonlyArray<{ code: string; name: string }> }) {
|
|
118
|
-
const { t } = useTranslation()
|
|
119
|
-
const { user, logout } = useAuth()
|
|
120
|
-
const { store } = useStore()
|
|
121
|
-
const switchAdminLocale = useSwitchAdminLocale()
|
|
122
|
-
if (!user) return null
|
|
123
|
-
|
|
124
|
-
const initials = getInitials(user.full_name, user.email)
|
|
125
|
-
|
|
126
|
-
// Switching the admin UI language persists to the account, mirrors it into
|
|
127
|
-
// the auth context, and reloads — see useSwitchAdminLocale for why all three
|
|
128
|
-
// steps are required.
|
|
129
|
-
const handleSelectLocale = (code: string) => {
|
|
130
|
-
void switchAdminLocale(code)
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return (
|
|
134
|
-
<DropdownMenu>
|
|
135
|
-
<DropdownMenuTrigger asChild>
|
|
136
|
-
<button
|
|
137
|
-
type="button"
|
|
138
|
-
aria-label={t('admin.a11y.user_menu')}
|
|
139
|
-
className="flex items-center gap-2 rounded-lg p-1 transition-colors hover:bg-accent"
|
|
140
|
-
>
|
|
141
|
-
<Avatar className="size-7">
|
|
142
|
-
{user.avatar_url && <AvatarImage src={user.avatar_url} alt="" />}
|
|
143
|
-
<AvatarFallback className="bg-primary text-xs text-primary-foreground dark:bg-accent dark:text-foreground">
|
|
144
|
-
{initials}
|
|
145
|
-
</AvatarFallback>
|
|
146
|
-
</Avatar>
|
|
147
|
-
</button>
|
|
148
|
-
</DropdownMenuTrigger>
|
|
149
|
-
<DropdownMenuContent align="end" className="w-56" sideOffset={8}>
|
|
150
|
-
<div className="flex items-center gap-2 p-1.5">
|
|
151
|
-
<Avatar className="size-8">
|
|
152
|
-
{user.avatar_url && <AvatarImage src={user.avatar_url} alt="" />}
|
|
153
|
-
<AvatarFallback className="bg-primary text-xs text-primary-foreground dark:bg-accent dark:text-foreground">
|
|
154
|
-
{initials}
|
|
155
|
-
</AvatarFallback>
|
|
156
|
-
</Avatar>
|
|
157
|
-
<div className="grid min-w-0 flex-1 text-sm leading-tight">
|
|
158
|
-
<span className="truncate font-medium text-foreground">
|
|
159
|
-
{user.full_name || user.email}
|
|
160
|
-
</span>
|
|
161
|
-
{user.full_name && (
|
|
162
|
-
<span className="truncate text-xs text-muted-foreground">{user.email}</span>
|
|
163
|
-
)}
|
|
164
|
-
</div>
|
|
165
|
-
</div>
|
|
166
|
-
<DropdownMenuSeparator />
|
|
167
|
-
<ThemeMenuItems />
|
|
168
|
-
{/* Compact nested submenu; self-hides when < 2 languages are installed. */}
|
|
169
|
-
<LanguageMenuItems
|
|
170
|
-
label={t('admin.account.language.label')}
|
|
171
|
-
locales={uiLocales}
|
|
172
|
-
value={i18n.language}
|
|
173
|
-
onSelect={handleSelectLocale}
|
|
174
|
-
/>
|
|
175
|
-
{uiLocales.length >= 2 && <DropdownMenuSeparator />}
|
|
176
|
-
{store && (
|
|
177
|
-
<DropdownMenuItem asChild>
|
|
178
|
-
<Link
|
|
179
|
-
to="/$storeId/settings/profile"
|
|
180
|
-
params={{ storeId: store.id }}
|
|
181
|
-
className="no-underline"
|
|
182
|
-
>
|
|
183
|
-
<UserIcon className="size-4" />
|
|
184
|
-
{t('admin.account.edit_profile')}
|
|
185
|
-
</Link>
|
|
186
|
-
</DropdownMenuItem>
|
|
187
|
-
)}
|
|
188
|
-
<DropdownMenuSeparator />
|
|
189
|
-
<DropdownMenuItem>
|
|
190
|
-
<BookOpenIcon className="size-4" />
|
|
191
|
-
{t('admin.account.documentation')}
|
|
192
|
-
</DropdownMenuItem>
|
|
193
|
-
<DropdownMenuItem>
|
|
194
|
-
<MessageCircleIcon className="size-4" />
|
|
195
|
-
{t('admin.account.community')}
|
|
196
|
-
</DropdownMenuItem>
|
|
197
|
-
<DropdownMenuItem>
|
|
198
|
-
<MailIcon className="size-4" />
|
|
199
|
-
{t('admin.account.contact_support')}
|
|
200
|
-
</DropdownMenuItem>
|
|
201
|
-
<DropdownMenuSeparator />
|
|
202
|
-
<DropdownMenuItem onClick={logout} className="text-destructive focus:text-destructive">
|
|
203
|
-
<LogOutIcon className="size-4" />
|
|
204
|
-
{t('admin.account.log_out')}
|
|
205
|
-
</DropdownMenuItem>
|
|
206
|
-
</DropdownMenuContent>
|
|
207
|
-
</DropdownMenu>
|
|
208
|
-
)
|
|
209
|
-
}
|