@spree/dashboard-core 6.0.1 → 6.0.3
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/package.json +13 -13
- package/src/components/product-stock-summary.tsx +58 -0
- package/src/components/table-toolbar.tsx +11 -14
- package/src/hooks/use-global-search.ts +1 -1
- package/src/index.ts +1 -0
- package/src/lib/search-registry.ts +1 -1
- package/src/locales/ar.json +8 -0
- package/src/locales/de.json +8 -0
- package/src/locales/en.json +8 -0
- package/src/locales/es.json +8 -0
- package/src/locales/fr.json +8 -0
- package/src/locales/ja.json +8 -0
- package/src/locales/pl.json +10 -0
- package/src/locales/vi.json +2130 -0
- package/src/locales/zh-CN.json +8 -0
- package/src/products/product-form-cards.tsx +3 -3
- package/src/products/product-form-mapping.test.ts +42 -1
- package/src/products/product-form-mapping.ts +19 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spree/dashboard-core",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "6.0.
|
|
4
|
+
"version": "6.0.3",
|
|
5
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.",
|
|
6
6
|
"author": "Vendo Connect Inc.",
|
|
7
7
|
"homepage": "https://spreecommerce.org",
|
|
@@ -49,8 +49,8 @@
|
|
|
49
49
|
"LICENSE"
|
|
50
50
|
],
|
|
51
51
|
"peerDependencies": {
|
|
52
|
-
"@spree/admin-sdk": ">=6.0.
|
|
53
|
-
"@spree/dashboard-ui": ">=6.0.
|
|
52
|
+
"@spree/admin-sdk": ">=6.0.3 <7.0.0-0",
|
|
53
|
+
"@spree/dashboard-ui": ">=6.0.3 <7.0.0-0",
|
|
54
54
|
"@tanstack/react-query": "^5.100.9",
|
|
55
55
|
"@tanstack/react-router": "^1.169.2",
|
|
56
56
|
"i18next": "^26.2.0",
|
|
@@ -60,17 +60,17 @@
|
|
|
60
60
|
"react-i18next": "^17.0.8"
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"@dnd-kit/core": "
|
|
64
|
-
"@dnd-kit/sortable": "
|
|
65
|
-
"@dnd-kit/utilities": "
|
|
63
|
+
"@dnd-kit/core": "6.3.1",
|
|
64
|
+
"@dnd-kit/sortable": "10.0.0",
|
|
65
|
+
"@dnd-kit/utilities": "3.2.2",
|
|
66
66
|
"@hookform/resolvers": "^6.0.0",
|
|
67
67
|
"@tailwindcss/vite": "^4.2.4",
|
|
68
|
-
"@tanstack/react-hotkeys": "
|
|
69
|
-
"date-fns": "
|
|
70
|
-
"date-fns-tz": "
|
|
71
|
-
"lucide-react": "
|
|
68
|
+
"@tanstack/react-hotkeys": "0.16.0",
|
|
69
|
+
"date-fns": "4.4.0",
|
|
70
|
+
"date-fns-tz": "3.2.0",
|
|
71
|
+
"lucide-react": "1.60.0",
|
|
72
72
|
"zod": "^4.6.7",
|
|
73
|
-
"@spree/core": "^6.0.
|
|
73
|
+
"@spree/core": "^6.0.3"
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
76
|
"@tanstack/react-query": "^5.104.5",
|
|
@@ -86,8 +86,8 @@
|
|
|
86
86
|
"typescript": "~7.0.2",
|
|
87
87
|
"vite": "^8.3.4",
|
|
88
88
|
"vitest": "^5.0.5",
|
|
89
|
-
"@spree/dashboard-ui": "^6.0.
|
|
90
|
-
"@spree/admin-sdk": "^6.0.
|
|
89
|
+
"@spree/dashboard-ui": "^6.0.3",
|
|
90
|
+
"@spree/admin-sdk": "^6.0.3"
|
|
91
91
|
},
|
|
92
92
|
"scripts": {
|
|
93
93
|
"lint": "biome check src",
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { useTranslation } from 'react-i18next'
|
|
2
|
+
|
|
3
|
+
/** The fields both panels' product payloads carry for a stock summary. */
|
|
4
|
+
export interface ProductStockFields {
|
|
5
|
+
in_stock: boolean
|
|
6
|
+
backorderable: boolean
|
|
7
|
+
total_on_hand?: number | null
|
|
8
|
+
variant_count: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Whether a product has stock, by its total rather than `in_stock`. Null is
|
|
13
|
+
* untracked stock, which is unlimited; undefined is a server too old to send
|
|
14
|
+
* a total, which leaves only the sellable flag to go by. Shared so every place
|
|
15
|
+
* that labels a product's stock agrees.
|
|
16
|
+
*/
|
|
17
|
+
export function productInStock(product: Pick<ProductStockFields, 'in_stock' | 'total_on_hand'>) {
|
|
18
|
+
const total = product.total_on_hand
|
|
19
|
+
return total === undefined ? product.in_stock : total === null || total > 0
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A product's stock in a products list: how many units are on hand, on
|
|
24
|
+
* backorder, or out of stock, followed by the variant count when there are
|
|
25
|
+
* several. Shared by the admin and seller product lists.
|
|
26
|
+
*
|
|
27
|
+
* It reads the stock total rather than `in_stock`, which leaves out variants
|
|
28
|
+
* whose seller cannot sell yet: a pending seller's units on the shelf are in
|
|
29
|
+
* stock as far as inventory goes.
|
|
30
|
+
*/
|
|
31
|
+
export function ProductStockSummary({ product }: { product: ProductStockFields }) {
|
|
32
|
+
const { t } = useTranslation()
|
|
33
|
+
const total = product.total_on_hand
|
|
34
|
+
|
|
35
|
+
const status = productInStock(product) ? (
|
|
36
|
+
typeof total === 'number' ? (
|
|
37
|
+
t('admin.components.product_stock.in_stock', { count: total })
|
|
38
|
+
) : (
|
|
39
|
+
t('admin.components.product_stock.in_stock_short')
|
|
40
|
+
)
|
|
41
|
+
) : product.backorderable ? (
|
|
42
|
+
t('admin.components.product_stock.on_backorder')
|
|
43
|
+
) : (
|
|
44
|
+
<span className="text-destructive">{t('admin.components.product_stock.out_of_stock')}</span>
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<span className="text-sm text-muted-foreground">
|
|
49
|
+
{status}
|
|
50
|
+
{product.variant_count > 1 && (
|
|
51
|
+
<>
|
|
52
|
+
{' · '}
|
|
53
|
+
{t('admin.components.product_stock.variants', { count: product.variant_count })}
|
|
54
|
+
</>
|
|
55
|
+
)}
|
|
56
|
+
</span>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type FilterPredicate, randomUUID } from '@spree/core'
|
|
2
2
|
import {
|
|
3
3
|
Button,
|
|
4
4
|
Checkbox,
|
|
@@ -865,7 +865,7 @@ function QuickEnumFilter({
|
|
|
865
865
|
onFiltersChange([
|
|
866
866
|
...others,
|
|
867
867
|
{
|
|
868
|
-
id: existing?.id ??
|
|
868
|
+
id: existing?.id ?? randomUUID(),
|
|
869
869
|
field: column.key,
|
|
870
870
|
operator: 'in',
|
|
871
871
|
value: values.join(','),
|
|
@@ -956,7 +956,7 @@ function QuickResourceFilter({
|
|
|
956
956
|
onFiltersChange([
|
|
957
957
|
...others,
|
|
958
958
|
{
|
|
959
|
-
id: existing?.id ??
|
|
959
|
+
id: existing?.id ?? randomUUID(),
|
|
960
960
|
field: column.key,
|
|
961
961
|
operator: 'in',
|
|
962
962
|
value: values.join(','),
|
|
@@ -1039,14 +1039,14 @@ function QuickDateFilter({
|
|
|
1039
1039
|
const next: FilterRule[] = [...others]
|
|
1040
1040
|
if (rangeFrom) {
|
|
1041
1041
|
next.push({
|
|
1042
|
-
id:
|
|
1042
|
+
id: randomUUID(),
|
|
1043
1043
|
field: column.key,
|
|
1044
1044
|
operator: 'gteq',
|
|
1045
1045
|
value: rangeFrom,
|
|
1046
1046
|
})
|
|
1047
1047
|
}
|
|
1048
1048
|
if (rangeTo) {
|
|
1049
|
-
next.push({ id:
|
|
1049
|
+
next.push({ id: randomUUID(), field: column.key, operator: 'lteq', value: rangeTo })
|
|
1050
1050
|
}
|
|
1051
1051
|
onFiltersChange(next)
|
|
1052
1052
|
},
|
|
@@ -1626,10 +1626,7 @@ function FilterPanel({
|
|
|
1626
1626
|
const others = filters.filter(
|
|
1627
1627
|
(f) => !(f.field === field && f.operator === nextOperator) && f.id !== editing,
|
|
1628
1628
|
)
|
|
1629
|
-
onApply([
|
|
1630
|
-
...others,
|
|
1631
|
-
{ id: crypto.randomUUID(), field, operator: nextOperator, value: nextValue },
|
|
1632
|
-
])
|
|
1629
|
+
onApply([...others, { id: randomUUID(), field, operator: nextOperator, value: nextValue }])
|
|
1633
1630
|
}
|
|
1634
1631
|
|
|
1635
1632
|
/** Replace this field's bounds with a preset's, and close. */
|
|
@@ -1641,10 +1638,10 @@ function FilterPanel({
|
|
|
1641
1638
|
)
|
|
1642
1639
|
const next = [...others]
|
|
1643
1640
|
if (range.from) {
|
|
1644
|
-
next.push({ id:
|
|
1641
|
+
next.push({ id: randomUUID(), field, operator: 'gteq', value: range.from })
|
|
1645
1642
|
}
|
|
1646
1643
|
if (range.to) {
|
|
1647
|
-
next.push({ id:
|
|
1644
|
+
next.push({ id: randomUUID(), field, operator: 'lteq', value: range.to })
|
|
1648
1645
|
}
|
|
1649
1646
|
onApply(next)
|
|
1650
1647
|
}
|
|
@@ -1672,7 +1669,7 @@ function FilterPanel({
|
|
|
1672
1669
|
onChange(
|
|
1673
1670
|
next.length === 0
|
|
1674
1671
|
? others
|
|
1675
|
-
: [...others, { id:
|
|
1672
|
+
: [...others, { id: randomUUID(), field, operator: 'in', value: next.join(',') }],
|
|
1676
1673
|
)
|
|
1677
1674
|
}
|
|
1678
1675
|
|
|
@@ -1911,7 +1908,7 @@ function FilterPanel({
|
|
|
1911
1908
|
const next = [...others]
|
|
1912
1909
|
if (customRange.from) {
|
|
1913
1910
|
next.push({
|
|
1914
|
-
id:
|
|
1911
|
+
id: randomUUID(),
|
|
1915
1912
|
field,
|
|
1916
1913
|
operator: 'gteq',
|
|
1917
1914
|
value: customRange.from,
|
|
@@ -1919,7 +1916,7 @@ function FilterPanel({
|
|
|
1919
1916
|
}
|
|
1920
1917
|
if (customRange.to) {
|
|
1921
1918
|
next.push({
|
|
1922
|
-
id:
|
|
1919
|
+
id: randomUUID(),
|
|
1923
1920
|
field,
|
|
1924
1921
|
operator: 'lteq',
|
|
1925
1922
|
value: customRange.to,
|
|
@@ -44,7 +44,7 @@ export function useGlobalSearch(rawQuery: string) {
|
|
|
44
44
|
queries: permitted.map((entry) => ({
|
|
45
45
|
// Scope by user so a logout→login on the same store can't serve the
|
|
46
46
|
// previous user's cached results from React Query.
|
|
47
|
-
queryKey: ['
|
|
47
|
+
queryKey: ['global-search', user?.id, storeId, entry.key, query],
|
|
48
48
|
queryFn: () => entry.fetch(query, RESULT_LIMIT),
|
|
49
49
|
enabled,
|
|
50
50
|
gcTime: GC_TIME_MS,
|
package/src/index.ts
CHANGED
|
@@ -51,6 +51,7 @@ export * from './components/nav-main'
|
|
|
51
51
|
export * from './components/page-header'
|
|
52
52
|
export * from './components/page-tabs'
|
|
53
53
|
export * from './components/preferences-form'
|
|
54
|
+
export * from './components/product-stock-summary'
|
|
54
55
|
export * from './components/record-metadata'
|
|
55
56
|
export * from './components/resource-combobox'
|
|
56
57
|
export * from './components/resource-multi-autocomplete'
|
|
@@ -21,7 +21,7 @@ import type { SubjectName } from './permissions'
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
export interface SearchResultRow {
|
|
24
|
-
/**
|
|
24
|
+
/** Item value — must be unique across the whole list; prefix with the entry key. */
|
|
25
25
|
value: string
|
|
26
26
|
/** Row contents rendered inside the `CommandItem`. */
|
|
27
27
|
content: ReactNode
|
package/src/locales/ar.json
CHANGED
|
@@ -493,6 +493,14 @@
|
|
|
493
493
|
"discard_confirm": "تجاهل",
|
|
494
494
|
"discard_message": "سيُفقد كل ما أدخلته حتى الآن، ولا يمكن التراجع عن ذلك.",
|
|
495
495
|
"discard_title": "تجاهل هذه المسودة؟"
|
|
496
|
+
},
|
|
497
|
+
"product_stock": {
|
|
498
|
+
"out_of_stock": "إنتهى من المخزن",
|
|
499
|
+
"on_backorder": "على المؤجل",
|
|
500
|
+
"in_stock": "{{count}} في المخزون",
|
|
501
|
+
"in_stock_short": "في المخزون",
|
|
502
|
+
"variants_one": "{{count}} البديل",
|
|
503
|
+
"variants_other": "{{count}} متغيرات"
|
|
496
504
|
}
|
|
497
505
|
},
|
|
498
506
|
"currency_amounts": {
|
package/src/locales/de.json
CHANGED
|
@@ -493,6 +493,14 @@
|
|
|
493
493
|
"discard_confirm": "Verwerfen",
|
|
494
494
|
"discard_message": "Alles bisher Eingegebene geht verloren. Das lässt sich nicht rückgängig machen.",
|
|
495
495
|
"discard_title": "Entwurf verwerfen?"
|
|
496
|
+
},
|
|
497
|
+
"product_stock": {
|
|
498
|
+
"out_of_stock": "Nicht auf Lager",
|
|
499
|
+
"on_backorder": "Im Rückstand",
|
|
500
|
+
"in_stock": "{{count}} auf Lager",
|
|
501
|
+
"in_stock_short": "Auf Lager",
|
|
502
|
+
"variants_one": "{{count}} Variante",
|
|
503
|
+
"variants_other": "{{count}} Varianten"
|
|
496
504
|
}
|
|
497
505
|
},
|
|
498
506
|
"currency_amounts": {
|
package/src/locales/en.json
CHANGED
|
@@ -495,6 +495,14 @@
|
|
|
495
495
|
"discard_confirm": "Discard",
|
|
496
496
|
"discard_message": "Everything you have filled in so far will be lost. This cannot be undone.",
|
|
497
497
|
"discard_title": "Discard this draft?"
|
|
498
|
+
},
|
|
499
|
+
"product_stock": {
|
|
500
|
+
"out_of_stock": "Out of stock",
|
|
501
|
+
"on_backorder": "On backorder",
|
|
502
|
+
"in_stock": "{{count}} in stock",
|
|
503
|
+
"in_stock_short": "In stock",
|
|
504
|
+
"variants_one": "{{count}} variant",
|
|
505
|
+
"variants_other": "{{count}} variants"
|
|
498
506
|
}
|
|
499
507
|
},
|
|
500
508
|
"currency_amounts": {
|
package/src/locales/es.json
CHANGED
|
@@ -306,6 +306,14 @@
|
|
|
306
306
|
"discard_confirm": "Descartar",
|
|
307
307
|
"discard_message": "Todo lo que completaste hasta ahora se perderá. Esta acción no se puede deshacer.",
|
|
308
308
|
"discard_title": "¿Descartar este borrador?"
|
|
309
|
+
},
|
|
310
|
+
"product_stock": {
|
|
311
|
+
"out_of_stock": "Sin stock",
|
|
312
|
+
"on_backorder": "En contrarreserva",
|
|
313
|
+
"in_stock": "{{count}} en stock",
|
|
314
|
+
"in_stock_short": "Con stock",
|
|
315
|
+
"variants_one": "{{count}} variante",
|
|
316
|
+
"variants_other": "{{count}} variantes"
|
|
309
317
|
}
|
|
310
318
|
},
|
|
311
319
|
"currency_amounts": {
|
package/src/locales/fr.json
CHANGED
|
@@ -493,6 +493,14 @@
|
|
|
493
493
|
"discard_confirm": "Abandonner",
|
|
494
494
|
"discard_message": "Tout ce que vous avez saisi sera perdu. Cette action est irréversible.",
|
|
495
495
|
"discard_title": "Abandonner ce brouillon ?"
|
|
496
|
+
},
|
|
497
|
+
"product_stock": {
|
|
498
|
+
"out_of_stock": "En rupture de stock",
|
|
499
|
+
"on_backorder": "En réapprovisionnement",
|
|
500
|
+
"in_stock": "{{count}} en stock",
|
|
501
|
+
"in_stock_short": "En stock",
|
|
502
|
+
"variants_one": "{{count}} variante",
|
|
503
|
+
"variants_other": "{{count}} variantes"
|
|
496
504
|
}
|
|
497
505
|
},
|
|
498
506
|
"currency_amounts": {
|
package/src/locales/ja.json
CHANGED
|
@@ -495,6 +495,14 @@
|
|
|
495
495
|
"discard_confirm": "破棄",
|
|
496
496
|
"discard_message": "ここまで入力した内容はすべて失われます。元に戻すことはできません。",
|
|
497
497
|
"discard_title": "この下書きを破棄しますか?"
|
|
498
|
+
},
|
|
499
|
+
"product_stock": {
|
|
500
|
+
"out_of_stock": "在庫切れ",
|
|
501
|
+
"on_backorder": "入荷待ち",
|
|
502
|
+
"in_stock": "在庫 {{count}}",
|
|
503
|
+
"in_stock_short": "在庫あり",
|
|
504
|
+
"variants_one": "バリアント {{count}} 件",
|
|
505
|
+
"variants_other": "バリアント {{count}} 件"
|
|
498
506
|
}
|
|
499
507
|
},
|
|
500
508
|
"currency_amounts": {
|
package/src/locales/pl.json
CHANGED
|
@@ -494,6 +494,16 @@
|
|
|
494
494
|
"discard_confirm": "Odrzuć",
|
|
495
495
|
"discard_message": "Wszystko, co zostało wprowadzone, zostanie utracone. Nie można tego cofnąć.",
|
|
496
496
|
"discard_title": "Odrzucić szkic?"
|
|
497
|
+
},
|
|
498
|
+
"product_stock": {
|
|
499
|
+
"out_of_stock": "Brak w magazynie",
|
|
500
|
+
"on_backorder": "Na zamówienie",
|
|
501
|
+
"in_stock": "{{count}} w magazynie",
|
|
502
|
+
"in_stock_short": "W magazynie",
|
|
503
|
+
"variants_few": "{{count}} warianty",
|
|
504
|
+
"variants_many": "{{count}} wariantów",
|
|
505
|
+
"variants_one": "{{count}} wariant",
|
|
506
|
+
"variants_other": "{{count}} wariantów"
|
|
497
507
|
}
|
|
498
508
|
},
|
|
499
509
|
"currency_amounts": {
|