@spree/dashboard-core 1.0.0-rc.4 → 1.0.0-rc.5
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 +5 -4
- package/src/api-client.ts +12 -7
- package/src/components/address-form-dialog.tsx +2 -2
- package/src/components/country-combobox.tsx +1 -2
- package/src/components/currency-select.tsx +6 -7
- package/src/components/file-upload-field.tsx +1 -0
- package/src/components/locale-select.tsx +22 -23
- package/src/components/nav-main.tsx +7 -2
- package/src/components/preferences-form.tsx +2 -8
- package/src/components/settings-sidebar.tsx +8 -1
- package/src/components/slot.tsx +21 -1
- package/src/components/table-toolbar.tsx +102 -39
- package/src/hooks/use-display-name.ts +1 -18
- package/src/hooks/use-file-upload.ts +2 -1
- package/src/hooks/use-import.ts +3 -2
- package/src/index.ts +13 -3
- package/src/lib/currencies.ts +2 -34
- package/src/lib/currency-parts.ts +2 -21
- package/src/lib/filters-to-ransack.ts +4 -1
- package/src/lib/form-fields-registry.ts +23 -1
- package/src/lib/form-mappers.ts +1 -1
- package/src/lib/formatters.ts +6 -10
- package/src/lib/fulfillment-items.ts +11 -0
- package/src/lib/i18n.ts +1 -12
- package/src/lib/iso-codes.ts +9 -26
- package/src/lib/money.ts +9 -99
- package/src/lib/normalize-money.ts +2 -85
- package/src/lib/return-refunds.ts +1 -2
- package/src/lib/route-registry.ts +29 -0
- package/src/lib/settings-nav-registry.ts +6 -1
- package/src/lib/store-config.test.ts +93 -0
- package/src/lib/store-config.ts +76 -0
- package/src/lib/table-registry.ts +16 -5
- package/src/locales/ar.json +4 -0
- package/src/locales/de.json +4 -0
- package/src/locales/en.json +4 -0
- package/src/locales/es.json +4 -0
- package/src/locales/fr.json +4 -0
- package/src/locales/pl.json +4 -0
- package/src/locales/zh-CN.json +4 -0
- package/src/pages/package-types.tsx +50 -11
- package/src/pages/settings-index.tsx +4 -1
- package/src/pages/stock-locations.tsx +31 -6
- package/src/products/bulk-variants-dialog.tsx +27 -28
- package/src/products/media-edit-sheet.tsx +67 -8
- package/src/products/normalize-quantity.ts +2 -3
- package/src/products/product-bulk-price-editor.tsx +1 -3
- package/src/products/product-form-cards.tsx +67 -37
- package/src/products/product-form-mapping.ts +44 -5
- package/src/products/product-schema.test.ts +8 -0
- package/src/products/product-schema.ts +14 -7
- package/src/products/product-types.ts +1 -1
- package/src/products/sheet-save-handlers.ts +37 -0
- package/src/products/variant-edit-sheet.tsx +21 -11
- package/src/products/variants-section.tsx +4 -1
- package/src/providers/store-provider.tsx +31 -7
- package/src/schemas/package-type.ts +14 -12
- package/src/schemas/stock-location.ts +2 -3
- package/src/tables/stock-locations.tsx +5 -8
- package/src/lib/money.test.ts +0 -63
- package/src/lib/normalize-money.test.ts +0 -67
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { Market } from '@spree/admin-sdk'
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import { adminClient } from '../client'
|
|
4
|
+
import { listAllMarkets, marketLocalization } from './store-config'
|
|
5
|
+
|
|
6
|
+
vi.mock('../client', () => ({ adminClient: { markets: { list: vi.fn() } } }))
|
|
7
|
+
|
|
8
|
+
function market(attributes: Partial<Market>): Market {
|
|
9
|
+
return {
|
|
10
|
+
id: 'mkt_1',
|
|
11
|
+
name: 'Market',
|
|
12
|
+
currency: 'USD',
|
|
13
|
+
default_locale: 'en',
|
|
14
|
+
tax_inclusive: false,
|
|
15
|
+
default: false,
|
|
16
|
+
country_codes: [],
|
|
17
|
+
supported_locales: [],
|
|
18
|
+
created_at: '',
|
|
19
|
+
updated_at: '',
|
|
20
|
+
tax_provider: null,
|
|
21
|
+
...attributes,
|
|
22
|
+
} as Market
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('marketLocalization', () => {
|
|
26
|
+
const markets = [
|
|
27
|
+
market({
|
|
28
|
+
id: 'mkt_eu',
|
|
29
|
+
currency: 'EUR',
|
|
30
|
+
default_locale: 'de',
|
|
31
|
+
supported_locales: ['de', 'fr'],
|
|
32
|
+
}),
|
|
33
|
+
market({ id: 'mkt_us', currency: 'USD', default_locale: 'en', default: true }),
|
|
34
|
+
market({ id: 'mkt_ie', currency: 'EUR', default_locale: 'en' }),
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
it('takes the defaults from the default market', () => {
|
|
38
|
+
const { defaultCurrency, defaultLocale } = marketLocalization(markets)
|
|
39
|
+
|
|
40
|
+
expect(defaultCurrency).toBe('USD')
|
|
41
|
+
expect(defaultLocale).toBe('en')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('lists each market currency once, the default market first', () => {
|
|
45
|
+
expect(marketLocalization(markets).currencies).toEqual(['USD', 'EUR'])
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('lists every language any market offers', () => {
|
|
49
|
+
expect(marketLocalization(markets).locales).toEqual(['de', 'en', 'fr'])
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('falls back to the first market when none is flagged default', () => {
|
|
53
|
+
const { defaultCurrency } = marketLocalization([market({ currency: 'GBP' })])
|
|
54
|
+
|
|
55
|
+
expect(defaultCurrency).toBe('GBP')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('returns empty lists before markets load', () => {
|
|
59
|
+
expect(marketLocalization([])).toEqual({
|
|
60
|
+
currencies: [],
|
|
61
|
+
locales: [],
|
|
62
|
+
defaultCurrency: undefined,
|
|
63
|
+
defaultLocale: undefined,
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
describe('listAllMarkets', () => {
|
|
69
|
+
it('joins every page so markets past the first page are not lost', async () => {
|
|
70
|
+
const page = (number: number, pages: number) => ({
|
|
71
|
+
data: [market({ id: `mkt_${number}` })],
|
|
72
|
+
meta: {
|
|
73
|
+
page: number,
|
|
74
|
+
pages,
|
|
75
|
+
limit: 100,
|
|
76
|
+
count: pages,
|
|
77
|
+
from: 1,
|
|
78
|
+
to: 1,
|
|
79
|
+
in: 1,
|
|
80
|
+
previous: null,
|
|
81
|
+
next: null,
|
|
82
|
+
},
|
|
83
|
+
})
|
|
84
|
+
vi.mocked(adminClient.markets.list).mockImplementation(async (params) =>
|
|
85
|
+
page(params?.page ?? 1, 3),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
const result = await listAllMarkets()
|
|
89
|
+
|
|
90
|
+
expect(result.data.map((record) => record.id)).toEqual(['mkt_1', 'mkt_2', 'mkt_3'])
|
|
91
|
+
expect(adminClient.markets.list).toHaveBeenCalledTimes(3)
|
|
92
|
+
})
|
|
93
|
+
})
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { Market, PaginatedResponse } from '@spree/admin-sdk'
|
|
2
|
+
import type { CurrencyCode, LocaleCode } from '@spree/core'
|
|
3
|
+
import { adminClient } from '../client'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Markets and channels are store configuration that rarely changes, so the
|
|
7
|
+
* dashboard fetches each list once and keeps it for an hour. Their create,
|
|
8
|
+
* update and delete mutations invalidate `[resource, storeId]`, which refreshes
|
|
9
|
+
* the cached list immediately.
|
|
10
|
+
*/
|
|
11
|
+
export const STORE_CONFIG_STALE_TIME = 60 * 60_000
|
|
12
|
+
|
|
13
|
+
const PAGE_SIZE = 100
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Fetches every page of a list and joins them, so a store with more records
|
|
17
|
+
* than one page holds still gets all of them.
|
|
18
|
+
*/
|
|
19
|
+
async function listEveryPage<T>(
|
|
20
|
+
fetchPage: (page: number) => Promise<PaginatedResponse<T>>,
|
|
21
|
+
): Promise<PaginatedResponse<T>> {
|
|
22
|
+
const first = await fetchPage(1)
|
|
23
|
+
if (first.meta.pages <= 1) return first
|
|
24
|
+
|
|
25
|
+
const rest = await Promise.all(
|
|
26
|
+
Array.from({ length: first.meta.pages - 1 }, (_, index) => fetchPage(index + 2)),
|
|
27
|
+
)
|
|
28
|
+
return { ...first, data: [first, ...rest].flatMap((response) => response.data) }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Every market of the current store, under `['markets', storeId, 'all']`. */
|
|
32
|
+
export function listAllMarkets() {
|
|
33
|
+
return listEveryPage((page) =>
|
|
34
|
+
adminClient.markets.list({ page, limit: PAGE_SIZE, sort: 'position' }),
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Every channel of the current store, under `['channels', storeId, 'all']`. */
|
|
39
|
+
export function listAllChannels() {
|
|
40
|
+
return listEveryPage((page) => adminClient.channels.list({ page, limit: PAGE_SIZE }))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface MarketLocalization {
|
|
44
|
+
/** The default market's currency first, then every other market's. */
|
|
45
|
+
currencies: CurrencyCode[]
|
|
46
|
+
/** Every language any market offers, sorted. */
|
|
47
|
+
locales: LocaleCode[]
|
|
48
|
+
defaultCurrency: CurrencyCode | undefined
|
|
49
|
+
defaultLocale: LocaleCode | undefined
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The currencies and languages a store sells in, read from its markets. The
|
|
54
|
+
* default market's currency and language are the store's defaults.
|
|
55
|
+
*/
|
|
56
|
+
export function marketLocalization(markets: readonly Market[]): MarketLocalization {
|
|
57
|
+
const defaultMarket = markets.find((market) => market.default) ?? markets[0]
|
|
58
|
+
const currencies = [
|
|
59
|
+
...new Set([
|
|
60
|
+
...(defaultMarket ? [defaultMarket.currency] : []),
|
|
61
|
+
...markets.map((market) => market.currency),
|
|
62
|
+
]),
|
|
63
|
+
]
|
|
64
|
+
const locales = [
|
|
65
|
+
...new Set(
|
|
66
|
+
markets.flatMap((market) => [market.default_locale, ...(market.supported_locales ?? [])]),
|
|
67
|
+
),
|
|
68
|
+
].sort()
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
currencies,
|
|
72
|
+
locales,
|
|
73
|
+
defaultCurrency: defaultMarket?.currency,
|
|
74
|
+
defaultLocale: defaultMarket?.default_locale,
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -67,6 +67,11 @@ interface ColumnDefBase<T = any> {
|
|
|
67
67
|
* can show human-readable names instead of raw prefixed IDs.
|
|
68
68
|
*/
|
|
69
69
|
export interface ResourceFilterConfig<R extends { id: string } = { id: string }> {
|
|
70
|
+
/**
|
|
71
|
+
* Leads every cache entry the filter creates, ahead of the tenant. Naming
|
|
72
|
+
* it after the resource (`'markets'`) lets that resource's writes, which
|
|
73
|
+
* invalidate `[resource, tenant]`, refresh the filter's lists too.
|
|
74
|
+
*/
|
|
70
75
|
queryKey: string
|
|
71
76
|
search: (query: string) => Promise<{ data: R[] }>
|
|
72
77
|
hydrate: (ids: string[]) => Promise<{ data: R[] }>
|
|
@@ -75,12 +80,18 @@ export interface ResourceFilterConfig<R extends { id: string } = { id: string }>
|
|
|
75
80
|
emptyText?: string
|
|
76
81
|
/**
|
|
77
82
|
* Fetches the whole list, for a resource small enough to show in full —
|
|
78
|
-
* a store's warehouses, its channels.
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
83
|
+
* a store's warehouses, its channels. It is fetched once and cached under
|
|
84
|
+
* `[queryKey, tenant, 'all']`; the filter narrows it as the operator types
|
|
85
|
+
* and labels its chips from it, without further requests. Use the same
|
|
86
|
+
* request as the resource's own full-list hook, which shares that key.
|
|
87
|
+
* Leave it off for anything that could run to hundreds of rows.
|
|
82
88
|
*/
|
|
83
89
|
listAll?: () => Promise<{ data: R[] }>
|
|
90
|
+
/**
|
|
91
|
+
* How long fetched options stay fresh, in milliseconds. Raise it for
|
|
92
|
+
* rarely-changing resources whose writes invalidate `queryKey`.
|
|
93
|
+
*/
|
|
94
|
+
staleTime?: number
|
|
84
95
|
}
|
|
85
96
|
|
|
86
97
|
/**
|
|
@@ -100,7 +111,7 @@ export type TaggableType = 'product' | 'customer' | 'order' | (string & {})
|
|
|
100
111
|
*/
|
|
101
112
|
export type ColumnDef<T = any> =
|
|
102
113
|
| (ColumnDefBase<T> & {
|
|
103
|
-
filterType?: 'string' | 'boolean' | 'number' | 'date' | 'currency'
|
|
114
|
+
filterType?: 'string' | 'boolean' | 'number' | 'date' | 'currency' | 'locale'
|
|
104
115
|
filterOptions?: never
|
|
105
116
|
filterResource?: never
|
|
106
117
|
taggableType?: never
|
package/src/locales/ar.json
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"admin": {
|
|
3
|
+
"address_select": {
|
|
4
|
+
"none": "لم يتم اختيار عنوان"
|
|
5
|
+
},
|
|
3
6
|
"app_name": "Spree Admin",
|
|
4
7
|
"a11y": {
|
|
5
8
|
"breadcrumb": "مسار التنقل",
|
|
@@ -97,6 +100,7 @@
|
|
|
97
100
|
"address_book": {
|
|
98
101
|
"default_billing": "الفوترة الافتراضية",
|
|
99
102
|
"default_shipping": "الشحن الافتراضي",
|
|
103
|
+
"inherited_from": "موروث من {{name}}",
|
|
100
104
|
"set_default_billing": "تعيين كعنوان فوترة افتراضي",
|
|
101
105
|
"set_default_shipping": "تعيين كعنوان شحن افتراضي"
|
|
102
106
|
},
|
package/src/locales/de.json
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"admin": {
|
|
3
|
+
"address_select": {
|
|
4
|
+
"none": "Keine Adresse ausgewählt"
|
|
5
|
+
},
|
|
3
6
|
"app_name": "Spree Admin",
|
|
4
7
|
"a11y": {
|
|
5
8
|
"breadcrumb": "Breadcrumb",
|
|
@@ -97,6 +100,7 @@
|
|
|
97
100
|
"address_book": {
|
|
98
101
|
"default_billing": "Standard-Rechnung",
|
|
99
102
|
"default_shipping": "Standard-Versand",
|
|
103
|
+
"inherited_from": "Geerbt von {{name}}",
|
|
100
104
|
"set_default_billing": "Als Standard-Rechnungsadresse festlegen",
|
|
101
105
|
"set_default_shipping": "Als Standard-Versandadresse festlegen"
|
|
102
106
|
},
|
package/src/locales/en.json
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"admin": {
|
|
3
|
+
"address_select": {
|
|
4
|
+
"none": "No address selected"
|
|
5
|
+
},
|
|
3
6
|
"app_name": "Spree Admin",
|
|
4
7
|
"a11y": {
|
|
5
8
|
"breadcrumb": "Breadcrumb",
|
|
@@ -97,6 +100,7 @@
|
|
|
97
100
|
"address_book": {
|
|
98
101
|
"default_billing": "Default billing",
|
|
99
102
|
"default_shipping": "Default shipping",
|
|
103
|
+
"inherited_from": "Inherited from {{name}}",
|
|
100
104
|
"set_default_billing": "Set as default billing",
|
|
101
105
|
"set_default_shipping": "Set as default shipping"
|
|
102
106
|
},
|
package/src/locales/es.json
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"admin": {
|
|
3
|
+
"address_select": {
|
|
4
|
+
"none": "Ninguna dirección seleccionada"
|
|
5
|
+
},
|
|
3
6
|
"components": {
|
|
4
7
|
"address_block": {
|
|
5
8
|
"not_provided": "No proporcionado"
|
|
@@ -2003,6 +2006,7 @@
|
|
|
2003
2006
|
"address_book": {
|
|
2004
2007
|
"default_billing": "Facturación predeterminada",
|
|
2005
2008
|
"default_shipping": "Envío predeterminado",
|
|
2009
|
+
"inherited_from": "Heredada de {{name}}",
|
|
2006
2010
|
"set_default_billing": "Establecer como facturación predeterminada",
|
|
2007
2011
|
"set_default_shipping": "Establecer como envío predeterminado"
|
|
2008
2012
|
},
|
package/src/locales/fr.json
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"admin": {
|
|
3
|
+
"address_select": {
|
|
4
|
+
"none": "Aucune adresse sélectionnée"
|
|
5
|
+
},
|
|
3
6
|
"app_name": "Spree Admin",
|
|
4
7
|
"a11y": {
|
|
5
8
|
"breadcrumb": "Fil d'Ariane",
|
|
@@ -97,6 +100,7 @@
|
|
|
97
100
|
"address_book": {
|
|
98
101
|
"default_billing": "Facturation par défaut",
|
|
99
102
|
"default_shipping": "Livraison par défaut",
|
|
103
|
+
"inherited_from": "Héritée de {{name}}",
|
|
100
104
|
"set_default_billing": "Définir comme adresse de facturation par défaut",
|
|
101
105
|
"set_default_shipping": "Définir comme adresse de livraison par défaut"
|
|
102
106
|
},
|
package/src/locales/pl.json
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"admin": {
|
|
3
|
+
"address_select": {
|
|
4
|
+
"none": "Nie wybrano adresu"
|
|
5
|
+
},
|
|
3
6
|
"app_name": "Spree Admin",
|
|
4
7
|
"a11y": {
|
|
5
8
|
"breadcrumb": "Ścieżka nawigacji",
|
|
@@ -97,6 +100,7 @@
|
|
|
97
100
|
"address_book": {
|
|
98
101
|
"default_billing": "Domyślny rozliczeniowy",
|
|
99
102
|
"default_shipping": "Domyślny wysyłkowy",
|
|
103
|
+
"inherited_from": "Dziedziczony z: {{name}}",
|
|
100
104
|
"set_default_billing": "Ustaw jako domyślny adres rozliczeniowy",
|
|
101
105
|
"set_default_shipping": "Ustaw jako domyślny adres wysyłkowy"
|
|
102
106
|
},
|
package/src/locales/zh-CN.json
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"admin": {
|
|
3
|
+
"address_select": {
|
|
4
|
+
"none": "未选择地址"
|
|
5
|
+
},
|
|
3
6
|
"app_name": "Spree Admin",
|
|
4
7
|
"a11y": {
|
|
5
8
|
"breadcrumb": "面包屑导航",
|
|
@@ -97,6 +100,7 @@
|
|
|
97
100
|
"address_book": {
|
|
98
101
|
"default_billing": "默认账单",
|
|
99
102
|
"default_shipping": "默认收货",
|
|
103
|
+
"inherited_from": "继承自 {{name}}",
|
|
100
104
|
"set_default_billing": "设为默认账单地址",
|
|
101
105
|
"set_default_shipping": "设为默认收货地址"
|
|
102
106
|
},
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { zodResolver } from '@hookform/resolvers/zod'
|
|
2
|
+
import { DimensionUnit, WeightUnit } from '@spree/core'
|
|
2
3
|
import {
|
|
3
4
|
Button,
|
|
4
5
|
Field,
|
|
@@ -32,6 +33,7 @@ import { z } from 'zod/v4'
|
|
|
32
33
|
import type { PanelPackageType } from '../api-client'
|
|
33
34
|
import { Can } from '../components/can'
|
|
34
35
|
import { ResourceTable, resourceSearchSchema } from '../components/resource-table'
|
|
36
|
+
import { HostFormSlot } from '../components/slot'
|
|
35
37
|
import {
|
|
36
38
|
canDeletePackageTypes,
|
|
37
39
|
canWritePackageTypes,
|
|
@@ -42,13 +44,12 @@ import {
|
|
|
42
44
|
useUpdatePackageType,
|
|
43
45
|
} from '../hooks/use-package-types'
|
|
44
46
|
import { mapSpreeErrorsToForm } from '../lib/form-errors'
|
|
47
|
+
import { extensionFormValues, extensionSubmitValues } from '../lib/form-fields-registry'
|
|
45
48
|
import { Subject } from '../lib/permissions'
|
|
46
49
|
import { usePermissions } from '../providers/permission-provider'
|
|
47
50
|
import {
|
|
48
|
-
PACKAGE_DIMENSION_UNITS,
|
|
49
51
|
PACKAGE_TYPE_DEFAULTS,
|
|
50
52
|
PACKAGE_TYPE_KINDS,
|
|
51
|
-
PACKAGE_WEIGHT_UNITS,
|
|
52
53
|
type PackageTypeFormValues,
|
|
53
54
|
packageTypeFormSchema,
|
|
54
55
|
packageTypeFormValuesToParams,
|
|
@@ -89,7 +90,14 @@ export type PackageTypesSearch = z.infer<typeof packageTypesSearchSchema>
|
|
|
89
90
|
*/
|
|
90
91
|
function noop() {}
|
|
91
92
|
|
|
92
|
-
export function PackageTypesPage({
|
|
93
|
+
export function PackageTypesPage({
|
|
94
|
+
search,
|
|
95
|
+
formKey = 'package_type',
|
|
96
|
+
}: {
|
|
97
|
+
search: PackageTypesSearch
|
|
98
|
+
/** Names the sheets' slot and extension fields — `seller.package_type` in the seller panel. */
|
|
99
|
+
formKey?: string
|
|
100
|
+
}) {
|
|
93
101
|
const { t } = useTranslation()
|
|
94
102
|
const navigate = useNavigate()
|
|
95
103
|
const confirm = useConfirm()
|
|
@@ -194,16 +202,27 @@ export function PackageTypesPage({ search }: { search: PackageTypesSearch }) {
|
|
|
194
202
|
}
|
|
195
203
|
/>
|
|
196
204
|
|
|
197
|
-
{isCreating &&
|
|
198
|
-
|
|
205
|
+
{isCreating && (
|
|
206
|
+
<CreatePackageTypeSheet formKey={formKey} open onOpenChange={(o) => !o && closeSheet()} />
|
|
207
|
+
)}
|
|
208
|
+
{editId && (
|
|
209
|
+
<EditPackageTypeSheet
|
|
210
|
+
id={editId}
|
|
211
|
+
formKey={formKey}
|
|
212
|
+
open
|
|
213
|
+
onOpenChange={(o) => !o && closeSheet()}
|
|
214
|
+
/>
|
|
215
|
+
)}
|
|
199
216
|
</>
|
|
200
217
|
)
|
|
201
218
|
}
|
|
202
219
|
|
|
203
220
|
function CreatePackageTypeSheet({
|
|
221
|
+
formKey,
|
|
204
222
|
open,
|
|
205
223
|
onOpenChange,
|
|
206
224
|
}: {
|
|
225
|
+
formKey: string
|
|
207
226
|
open: boolean
|
|
208
227
|
onOpenChange: (open: boolean) => void
|
|
209
228
|
}) {
|
|
@@ -211,12 +230,15 @@ function CreatePackageTypeSheet({
|
|
|
211
230
|
const createMutation = useCreatePackageType()
|
|
212
231
|
const form = useForm<PackageTypeFormValues>({
|
|
213
232
|
resolver: zodResolver(packageTypeFormSchema),
|
|
214
|
-
defaultValues: PACKAGE_TYPE_DEFAULTS,
|
|
233
|
+
defaultValues: { ...PACKAGE_TYPE_DEFAULTS, ...extensionFormValues(formKey, null) },
|
|
215
234
|
})
|
|
216
235
|
|
|
217
236
|
async function onSubmit(values: PackageTypeFormValues) {
|
|
218
237
|
try {
|
|
219
|
-
await createMutation.mutateAsync(
|
|
238
|
+
await createMutation.mutateAsync({
|
|
239
|
+
...packageTypeFormValuesToParams(values),
|
|
240
|
+
...extensionSubmitValues(formKey, form),
|
|
241
|
+
})
|
|
220
242
|
form.reset(PACKAGE_TYPE_DEFAULTS)
|
|
221
243
|
onOpenChange(false)
|
|
222
244
|
} catch (err) {
|
|
@@ -240,6 +262,11 @@ function CreatePackageTypeSheet({
|
|
|
240
262
|
<form onSubmit={form.handleSubmit(onSubmit)} className="flex min-h-0 flex-1 flex-col">
|
|
241
263
|
<div className="flex flex-1 flex-col gap-4 overflow-y-auto p-4">
|
|
242
264
|
<PackageTypeFormFields form={form} />
|
|
265
|
+
<HostFormSlot
|
|
266
|
+
form={form}
|
|
267
|
+
name={`${formKey}.form_fields`}
|
|
268
|
+
context={{ packageType: undefined, mode: 'new' }}
|
|
269
|
+
/>
|
|
243
270
|
</div>
|
|
244
271
|
<SheetFooter>
|
|
245
272
|
<Button type="submit" disabled={createMutation.isPending}>
|
|
@@ -254,10 +281,12 @@ function CreatePackageTypeSheet({
|
|
|
254
281
|
|
|
255
282
|
function EditPackageTypeSheet({
|
|
256
283
|
id,
|
|
284
|
+
formKey,
|
|
257
285
|
open,
|
|
258
286
|
onOpenChange,
|
|
259
287
|
}: {
|
|
260
288
|
id: string
|
|
289
|
+
formKey: string
|
|
261
290
|
open: boolean
|
|
262
291
|
onOpenChange: (open: boolean) => void
|
|
263
292
|
}) {
|
|
@@ -271,12 +300,17 @@ function EditPackageTypeSheet({
|
|
|
271
300
|
const form = useForm<PackageTypeFormValues>({
|
|
272
301
|
resolver: zodResolver(packageTypeFormSchema),
|
|
273
302
|
defaultValues: PACKAGE_TYPE_DEFAULTS,
|
|
274
|
-
values: packageType
|
|
303
|
+
values: packageType
|
|
304
|
+
? { ...packageTypeToFormValues(packageType), ...extensionFormValues(formKey, packageType) }
|
|
305
|
+
: undefined,
|
|
275
306
|
})
|
|
276
307
|
|
|
277
308
|
async function onSubmit(values: PackageTypeFormValues) {
|
|
278
309
|
try {
|
|
279
|
-
await updateMutation.mutateAsync(
|
|
310
|
+
await updateMutation.mutateAsync({
|
|
311
|
+
...packageTypeFormValuesToParams(values),
|
|
312
|
+
...extensionSubmitValues(formKey, form),
|
|
313
|
+
})
|
|
280
314
|
onOpenChange(false)
|
|
281
315
|
} catch (err) {
|
|
282
316
|
if (!mapSpreeErrorsToForm(err, form.setError)) throw err
|
|
@@ -299,6 +333,11 @@ function EditPackageTypeSheet({
|
|
|
299
333
|
<form onSubmit={form.handleSubmit(onSubmit)} className="flex min-h-0 flex-1 flex-col">
|
|
300
334
|
<div className="flex flex-1 flex-col gap-4 overflow-y-auto p-4">
|
|
301
335
|
<PackageTypeFormFields form={form} />
|
|
336
|
+
<HostFormSlot
|
|
337
|
+
form={form}
|
|
338
|
+
name={`${formKey}.form_fields`}
|
|
339
|
+
context={{ packageType, mode: 'edit' }}
|
|
340
|
+
/>
|
|
302
341
|
</div>
|
|
303
342
|
<SheetFooter>
|
|
304
343
|
<Button type="submit" disabled={updateMutation.isPending}>
|
|
@@ -376,7 +415,7 @@ function PackageTypeFormFields({ form }: { form: UseFormReturn<PackageTypeFormVa
|
|
|
376
415
|
<UnitField
|
|
377
416
|
form={form}
|
|
378
417
|
name="dimensions_unit"
|
|
379
|
-
units={
|
|
418
|
+
units={Object.values(DimensionUnit)}
|
|
380
419
|
label={t('admin.fields.package_type.dimensions_unit.label')}
|
|
381
420
|
/>
|
|
382
421
|
</div>
|
|
@@ -416,7 +455,7 @@ function PackageTypeFormFields({ form }: { form: UseFormReturn<PackageTypeFormVa
|
|
|
416
455
|
<UnitField
|
|
417
456
|
form={form}
|
|
418
457
|
name="weight_unit"
|
|
419
|
-
units={
|
|
458
|
+
units={Object.values(WeightUnit)}
|
|
420
459
|
label={t('admin.fields.package_type.weight_unit.label')}
|
|
421
460
|
/>
|
|
422
461
|
</div>
|
|
@@ -103,7 +103,10 @@ function SettingsCard({ entry, tenantId }: { entry: SettingsNavEntry; tenantId:
|
|
|
103
103
|
>
|
|
104
104
|
<Icon className="mt-0.5 size-6 shrink-0 text-muted-foreground opacity-80 hover:opacity-100" />
|
|
105
105
|
<span className="flex min-w-0 flex-col gap-1">
|
|
106
|
-
<span className="text-sm font-medium leading-none">
|
|
106
|
+
<span className="flex items-center gap-1.5 text-sm font-medium leading-none">
|
|
107
|
+
{label}
|
|
108
|
+
{entry.badge && <entry.badge />}
|
|
109
|
+
</span>
|
|
107
110
|
{description && <span className="text-sm text-muted-foreground">{description}</span>}
|
|
108
111
|
</span>
|
|
109
112
|
</Link>
|
|
@@ -34,6 +34,7 @@ import { Can } from '../components/can'
|
|
|
34
34
|
import { CountryCombobox } from '../components/country-combobox'
|
|
35
35
|
import { StateCombobox, useCountryStates } from '../components/country-state-fields'
|
|
36
36
|
import { ResourceTable, resourceSearchSchema } from '../components/resource-table'
|
|
37
|
+
import { HostFormSlot } from '../components/slot'
|
|
37
38
|
import {
|
|
38
39
|
canDeleteStockLocations,
|
|
39
40
|
listStockLocations,
|
|
@@ -43,6 +44,7 @@ import {
|
|
|
43
44
|
useUpdateStockLocation,
|
|
44
45
|
} from '../hooks/use-stock-locations'
|
|
45
46
|
import { mapSpreeErrorsToForm } from '../lib/form-errors'
|
|
47
|
+
import { extensionFormValues, extensionSubmitValues } from '../lib/form-fields-registry'
|
|
46
48
|
import { Subject } from '../lib/permissions'
|
|
47
49
|
import { usePermissions } from '../providers/permission-provider'
|
|
48
50
|
import {
|
|
@@ -90,8 +92,11 @@ export function StockLocationsPage({
|
|
|
90
92
|
search,
|
|
91
93
|
stockLevelsPanel: StockLevelsPanel,
|
|
92
94
|
activityPanel: ActivityPanel,
|
|
95
|
+
formKey = 'stock_location',
|
|
93
96
|
}: {
|
|
94
97
|
search: StockLocationsSearch
|
|
98
|
+
/** Names the sheets' slot and extension fields — `seller.stock_location` in the seller panel. */
|
|
99
|
+
formKey?: string
|
|
95
100
|
/** Rendered inside the edit sheet, below the form. Optional — see above. */
|
|
96
101
|
stockLevelsPanel?: ComponentType<{ stockLocationId: string }>
|
|
97
102
|
/** What changed in this warehouse, for reconciliation. Optional. */
|
|
@@ -170,10 +175,13 @@ export function StockLocationsPage({
|
|
|
170
175
|
}
|
|
171
176
|
/>
|
|
172
177
|
|
|
173
|
-
{isCreating &&
|
|
178
|
+
{isCreating && (
|
|
179
|
+
<CreateStockLocationSheet formKey={formKey} open onOpenChange={(o) => !o && closeSheet()} />
|
|
180
|
+
)}
|
|
174
181
|
{editId && (
|
|
175
182
|
<EditStockLocationSheet
|
|
176
183
|
id={editId}
|
|
184
|
+
formKey={formKey}
|
|
177
185
|
open
|
|
178
186
|
onOpenChange={(o) => !o && closeSheet()}
|
|
179
187
|
stockLevelsPanel={StockLevelsPanel}
|
|
@@ -189,9 +197,11 @@ export function StockLocationsPage({
|
|
|
189
197
|
// ============================================================================
|
|
190
198
|
|
|
191
199
|
function CreateStockLocationSheet({
|
|
200
|
+
formKey,
|
|
192
201
|
open,
|
|
193
202
|
onOpenChange,
|
|
194
203
|
}: {
|
|
204
|
+
formKey: string
|
|
195
205
|
open: boolean
|
|
196
206
|
onOpenChange: (open: boolean) => void
|
|
197
207
|
}) {
|
|
@@ -201,13 +211,13 @@ function CreateStockLocationSheet({
|
|
|
201
211
|
const form = useForm<StockLocationFormValues>({
|
|
202
212
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
203
213
|
resolver: zodResolver(stockLocationFormSchema) as any,
|
|
204
|
-
defaultValues: STOCK_LOCATION_DEFAULTS,
|
|
214
|
+
defaultValues: { ...STOCK_LOCATION_DEFAULTS, ...extensionFormValues(formKey, null) },
|
|
205
215
|
})
|
|
206
216
|
|
|
207
217
|
async function onSubmit(values: StockLocationFormValues) {
|
|
208
218
|
try {
|
|
209
219
|
const params = formValuesToParams(values)
|
|
210
|
-
await createMutation.mutateAsync(params)
|
|
220
|
+
await createMutation.mutateAsync({ ...params, ...extensionSubmitValues(formKey, form) })
|
|
211
221
|
form.reset(STOCK_LOCATION_DEFAULTS)
|
|
212
222
|
onOpenChange(false)
|
|
213
223
|
} catch (err) {
|
|
@@ -231,6 +241,11 @@ function CreateStockLocationSheet({
|
|
|
231
241
|
<form onSubmit={form.handleSubmit(onSubmit)} className="flex min-h-0 flex-1 flex-col">
|
|
232
242
|
<div className="flex flex-1 flex-col gap-4 overflow-y-auto p-4">
|
|
233
243
|
<StockLocationFormFields form={form} />
|
|
244
|
+
<HostFormSlot
|
|
245
|
+
form={form}
|
|
246
|
+
name={`${formKey}.form_fields`}
|
|
247
|
+
context={{ stockLocation: undefined, mode: 'new' }}
|
|
248
|
+
/>
|
|
234
249
|
</div>
|
|
235
250
|
<SheetFooter>
|
|
236
251
|
<Button
|
|
@@ -259,12 +274,14 @@ function CreateStockLocationSheet({
|
|
|
259
274
|
|
|
260
275
|
function EditStockLocationSheet({
|
|
261
276
|
id,
|
|
277
|
+
formKey,
|
|
262
278
|
open,
|
|
263
279
|
onOpenChange,
|
|
264
280
|
stockLevelsPanel: StockLevelsPanel,
|
|
265
281
|
activityPanel: ActivityPanel,
|
|
266
282
|
}: {
|
|
267
283
|
id: string
|
|
284
|
+
formKey: string
|
|
268
285
|
open: boolean
|
|
269
286
|
onOpenChange: (open: boolean) => void
|
|
270
287
|
stockLevelsPanel?: ComponentType<{ stockLocationId: string }>
|
|
@@ -284,14 +301,17 @@ function EditStockLocationSheet({
|
|
|
284
301
|
// with whatever the server last persisted (including external edits).
|
|
285
302
|
useEffect(() => {
|
|
286
303
|
if (stockLocation) {
|
|
287
|
-
form.reset(
|
|
304
|
+
form.reset({
|
|
305
|
+
...stockLocationToFormValues(stockLocation),
|
|
306
|
+
...extensionFormValues(formKey, stockLocation),
|
|
307
|
+
})
|
|
288
308
|
}
|
|
289
|
-
}, [stockLocation, form])
|
|
309
|
+
}, [stockLocation, form, formKey])
|
|
290
310
|
|
|
291
311
|
async function onSubmit(values: StockLocationFormValues) {
|
|
292
312
|
try {
|
|
293
313
|
const params = formValuesToParams(values)
|
|
294
|
-
await updateMutation.mutateAsync(params)
|
|
314
|
+
await updateMutation.mutateAsync({ ...params, ...extensionSubmitValues(formKey, form) })
|
|
295
315
|
form.reset(values)
|
|
296
316
|
onOpenChange(false)
|
|
297
317
|
} catch (err) {
|
|
@@ -314,6 +334,11 @@ function EditStockLocationSheet({
|
|
|
314
334
|
<form onSubmit={form.handleSubmit(onSubmit)} className="flex min-h-0 flex-1 flex-col">
|
|
315
335
|
<div className="flex flex-1 flex-col gap-4 overflow-y-auto p-4">
|
|
316
336
|
<StockLocationFormFields form={form} />
|
|
337
|
+
<HostFormSlot
|
|
338
|
+
form={form}
|
|
339
|
+
name={`${formKey}.form_fields`}
|
|
340
|
+
context={{ stockLocation, mode: 'edit' }}
|
|
341
|
+
/>
|
|
317
342
|
{StockLevelsPanel && <StockLevelsPanel stockLocationId={id} />}
|
|
318
343
|
{ActivityPanel && <ActivityPanel stockLocationId={id} />}
|
|
319
344
|
</div>
|