@spree/dashboard-core 0.13.0 → 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.
Files changed (126) hide show
  1. package/README.md +4 -0
  2. package/dist/vite/index.js +17 -3
  3. package/package.json +7 -7
  4. package/src/api-client.ts +643 -0
  5. package/src/components/address-form-dialog.test.ts +25 -0
  6. package/src/components/address-form-dialog.tsx +203 -79
  7. package/src/components/app-shell.tsx +171 -0
  8. package/src/components/app-sidebar.tsx +184 -27
  9. package/src/components/bulk-action-bar.tsx +89 -157
  10. package/src/components/country-combobox.tsx +69 -34
  11. package/src/components/country-state-fields.tsx +43 -11
  12. package/src/components/currency-select.tsx +81 -66
  13. package/src/components/export-button.tsx +62 -5
  14. package/src/components/file-upload-field.tsx +32 -10
  15. package/src/components/import-button.tsx +97 -36
  16. package/src/components/import-wizard-dialog.tsx +710 -0
  17. package/src/components/locale-select.tsx +70 -57
  18. package/src/components/media-picker-sheet.tsx +307 -0
  19. package/src/components/mobile-top-bar.tsx +75 -0
  20. package/src/components/nav-main.tsx +197 -27
  21. package/src/components/page-header.tsx +203 -61
  22. package/src/components/page-tabs.tsx +4 -1
  23. package/src/components/preferences-form.tsx +98 -4
  24. package/src/components/resource-combobox.tsx +52 -4
  25. package/src/components/resource-picker-sheet.tsx +209 -73
  26. package/src/components/resource-table.tsx +425 -131
  27. package/src/components/route-announcer.tsx +64 -0
  28. package/src/components/section-heading.tsx +95 -0
  29. package/src/components/settings-sidebar.tsx +290 -60
  30. package/src/components/sheet-top-bar.tsx +44 -0
  31. package/src/components/sidebar-search.tsx +83 -0
  32. package/src/components/sidebar-user.tsx +213 -0
  33. package/src/components/skip-link.tsx +36 -0
  34. package/src/components/store-date-picker.tsx +6 -2
  35. package/src/components/store-switcher.tsx +63 -22
  36. package/src/components/table-toolbar.tsx +1373 -442
  37. package/src/components/tag-combobox.tsx +12 -4
  38. package/src/components/top-bar-breadcrumbs.tsx +235 -0
  39. package/src/components/wizard-close-guard.test.ts +54 -0
  40. package/src/hooks/use-auto-collapse-sidebar.ts +57 -0
  41. package/src/hooks/use-command-palette.tsx +13 -0
  42. package/src/hooks/use-countries.test.ts +56 -0
  43. package/src/hooks/use-countries.ts +51 -5
  44. package/src/hooks/use-direct-upload.ts +16 -3
  45. package/src/hooks/use-export.ts +51 -16
  46. package/src/hooks/use-import.ts +261 -14
  47. package/src/hooks/use-package-types.ts +132 -0
  48. package/src/hooks/use-resource-mutation.ts +74 -13
  49. package/src/hooks/use-stock-locations.ts +131 -0
  50. package/src/hooks/use-switch-admin-locale.ts +2 -2
  51. package/src/hooks/use-visual-viewport-offset.ts +61 -0
  52. package/src/index.ts +45 -1
  53. package/src/lib/cancel-order-form.ts +27 -0
  54. package/src/lib/create-action-registry.ts +278 -0
  55. package/src/lib/date-presets.test.ts +86 -0
  56. package/src/lib/date-presets.ts +98 -0
  57. package/src/lib/delivery-form.ts +15 -0
  58. package/src/lib/docs.ts +32 -0
  59. package/src/lib/download.ts +13 -1
  60. package/src/lib/filters-to-ransack.test.ts +113 -0
  61. package/src/lib/filters-to-ransack.ts +22 -1
  62. package/src/lib/form-errors.ts +122 -10
  63. package/src/lib/form-mappers.ts +76 -0
  64. package/src/lib/fulfillment-items.test.ts +213 -0
  65. package/src/lib/fulfillment-items.ts +171 -0
  66. package/src/lib/i18n.ts +19 -11
  67. package/src/lib/import-types.ts +25 -0
  68. package/src/lib/label-upload-form.ts +22 -0
  69. package/src/lib/nav-registry.ts +71 -3
  70. package/src/lib/permissions.ts +46 -1
  71. package/src/lib/progress.ts +18 -0
  72. package/src/lib/query-keys.ts +13 -13
  73. package/src/lib/sample-csv.ts +21 -0
  74. package/src/lib/search-registry.ts +6 -2
  75. package/src/lib/settings-nav-registry.ts +104 -11
  76. package/src/lib/storefront.ts +19 -0
  77. package/src/lib/table-registry.ts +56 -0
  78. package/src/lib/type-labels.test.ts +68 -0
  79. package/src/lib/type-labels.ts +78 -0
  80. package/src/locales/ar.json +1889 -516
  81. package/src/locales/de.json +1883 -515
  82. package/src/locales/en.json +1886 -516
  83. package/src/locales/fr.json +1884 -516
  84. package/src/locales/pl.json +1913 -516
  85. package/src/locales/zh-CN.json +1883 -516
  86. package/src/pages/package-types.tsx +484 -0
  87. package/src/pages/settings-index.tsx +112 -0
  88. package/src/pages/stock-locations.tsx +674 -0
  89. package/src/plugin.ts +29 -0
  90. package/src/products/add-video-dialog.tsx +100 -0
  91. package/src/products/bulk-variants-dialog.tsx +301 -0
  92. package/src/products/carton-size.test.ts +51 -0
  93. package/src/products/carton-size.ts +20 -0
  94. package/src/products/currency-parts.ts +21 -0
  95. package/src/products/inventory-section.tsx +259 -0
  96. package/src/products/media-edit-sheet.tsx +336 -0
  97. package/src/products/normalize-customs.test.ts +39 -0
  98. package/src/products/normalize-customs.ts +27 -0
  99. package/src/products/normalize-money.test.ts +58 -0
  100. package/src/products/normalize-money.ts +73 -0
  101. package/src/products/normalize-quantity.test.ts +32 -0
  102. package/src/products/normalize-quantity.ts +31 -0
  103. package/src/products/product-bulk-price-editor.tsx +165 -0
  104. package/src/products/product-form-cards.tsx +1167 -0
  105. package/src/products/product-form-mapping.ts +212 -0
  106. package/src/products/product-schema.ts +318 -0
  107. package/src/products/product-types.ts +130 -0
  108. package/src/products/use-product-form-data.ts +273 -0
  109. package/src/products/variant-edit-sheet.tsx +725 -0
  110. package/src/products/variants-matrix.test.ts +154 -0
  111. package/src/products/variants-matrix.ts +244 -0
  112. package/src/products/variants-options-builder.tsx +571 -0
  113. package/src/products/variants-section.tsx +535 -0
  114. package/src/products/video-url.test.ts +71 -0
  115. package/src/products/video-url.ts +83 -0
  116. package/src/providers/auth-provider.tsx +70 -12
  117. package/src/providers/permission-provider.tsx +68 -28
  118. package/src/providers/sticky-header-provider.tsx +134 -0
  119. package/src/providers/store-provider.tsx +36 -3
  120. package/src/providers/tenant-provider.tsx +33 -0
  121. package/src/schemas/package-type.ts +95 -0
  122. package/src/schemas/stock-location.ts +126 -0
  123. package/src/tables/package-types.tsx +74 -0
  124. package/src/tables/stock-locations.tsx +124 -0
  125. package/src/vite/index.ts +40 -7
  126. package/src/components/top-bar.tsx +0 -210
@@ -0,0 +1,58 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { normalizeMoneyInput } from './normalize-money'
3
+
4
+ describe('normalizeMoneyInput', () => {
5
+ it('passes through canonical en input', () => {
6
+ expect(normalizeMoneyInput('19.99', 'en')).toBe('19.99')
7
+ expect(normalizeMoneyInput('1,234.56', 'en')).toBe('1234.56')
8
+ expect(normalizeMoneyInput('1234.56', 'en')).toBe('1234.56')
9
+ })
10
+
11
+ it('converts de comma-decimal + dot grouping to canonical', () => {
12
+ expect(normalizeMoneyInput('19,99', 'de')).toBe('19.99')
13
+ expect(normalizeMoneyInput('1.234,56', 'de')).toBe('1234.56')
14
+ expect(normalizeMoneyInput('1.000.000,00', 'de')).toBe('1000000.00')
15
+ })
16
+
17
+ it('handles fr narrow-space grouping', () => {
18
+ // fr groups with U+202F (narrow no-break space) and uses a comma decimal.
19
+ expect(normalizeMoneyInput('1 234,56', 'fr')).toBe('1234.56')
20
+ expect(normalizeMoneyInput('73,45', 'fr')).toBe('73.45')
21
+ })
22
+
23
+ it('returns empty string for blank input', () => {
24
+ expect(normalizeMoneyInput('', 'de')).toBe('')
25
+ expect(normalizeMoneyInput(' ', 'de')).toBe('')
26
+ expect(normalizeMoneyInput(null, 'de')).toBe('')
27
+ expect(normalizeMoneyInput(undefined, 'de')).toBe('')
28
+ })
29
+
30
+ it('preserves a leading minus', () => {
31
+ expect(normalizeMoneyInput('-5,00', 'de')).toBe('-5.00')
32
+ expect(normalizeMoneyInput('-5.00', 'en')).toBe('-5.00')
33
+ })
34
+
35
+ it('drops stray currency symbols and whitespace', () => {
36
+ expect(normalizeMoneyInput(' € 1.234,56 ', 'de')).toBe('1234.56')
37
+ expect(normalizeMoneyInput('$19.99', 'en')).toBe('19.99')
38
+ })
39
+
40
+ it('keeps no-decimal values intact', () => {
41
+ expect(normalizeMoneyInput('50', 'de')).toBe('50')
42
+ expect(normalizeMoneyInput('1.000', 'de')).toBe('1000') // de: dot is grouping
43
+ expect(normalizeMoneyInput('1,000', 'en')).toBe('1000') // en: comma is grouping
44
+ })
45
+
46
+ it('collapses stray separators to a single decimal point', () => {
47
+ // Malformed multi-dot/interior-minus input must canonicalize, not pass through.
48
+ expect(normalizeMoneyInput('1.2.3', 'en')).toBe('1.23')
49
+ expect(normalizeMoneyInput('12-3.4', 'en')).toBe('123.4')
50
+ expect(normalizeMoneyInput('-12-3.4', 'en')).toBe('-123.4')
51
+ })
52
+
53
+ it('returns empty for inputs with no digits', () => {
54
+ expect(normalizeMoneyInput('.', 'en')).toBe('')
55
+ expect(normalizeMoneyInput('-', 'en')).toBe('')
56
+ expect(normalizeMoneyInput('abc', 'en')).toBe('')
57
+ })
58
+ })
@@ -0,0 +1,73 @@
1
+ // Normalize a merchant's locale-formatted money input into the canonical
2
+ // decimal string the Admin API expects (`"1234.56"` — period decimal, no
3
+ // grouping). Industry-standard: localization is a presentation concern, so the
4
+ // dashboard converts here, in the browser, and never relies on the server to
5
+ // parse comma-vs-period. See docs/plans/5.5-client-side-money-normalization.md.
6
+ //
7
+ // The locale is the currency's display locale (its market locale, e.g. EUR →
8
+ // `de`), so the same locale drives both display formatting and this parse.
9
+
10
+ /**
11
+ * Derives a locale's decimal and group separators via `Intl`. `fr` groups with
12
+ * a narrow no-break space (U+202F), so we read the real characters rather than
13
+ * assuming `,`/`.`/`" "`.
14
+ */
15
+ function separatorsFor(locale: string): { decimal: string; group: string } {
16
+ try {
17
+ const parts = new Intl.NumberFormat(locale, { useGrouping: true }).formatToParts(11111.1)
18
+ return {
19
+ decimal: parts.find((p) => p.type === 'decimal')?.value ?? '.',
20
+ group: parts.find((p) => p.type === 'group')?.value ?? '',
21
+ }
22
+ } catch {
23
+ return { decimal: '.', group: '' }
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Converts a locale-formatted amount string to a canonical decimal string.
29
+ *
30
+ * - `"1.234,56"` under `de` → `"1234.56"`
31
+ * - `"1 234,56"` under `fr` → `"1234.56"` (narrow-space grouping)
32
+ * - `"19.99"` under `en` → `"19.99"`
33
+ * - `""` / whitespace → `""`
34
+ *
35
+ * Strips the locale's group separator and any whitespace, standardizes the
36
+ * decimal separator to `.`, and drops stray characters. Keeps the value as a
37
+ * string throughout — never `Number()`-coerces — to preserve precision.
38
+ *
39
+ * @param raw the merchant's typed value
40
+ * @param locale the display locale the value was entered in (e.g. `de`)
41
+ * @returns canonical `"1234.56"`-form string, or `''` for blank input
42
+ */
43
+ export function normalizeMoneyInput(raw: string | null | undefined, locale: string): string {
44
+ if (raw == null) return ''
45
+ const trimmed = raw.trim()
46
+ if (trimmed === '') return ''
47
+
48
+ const { decimal, group } = separatorsFor(locale)
49
+
50
+ let out = trimmed
51
+ // Remove the locale's grouping separator (explicit char) plus any whitespace
52
+ // (covers narrow/no-break spaces used by some locales as the group char).
53
+ if (group) out = out.split(group).join('')
54
+ out = out.replace(/\s/g, '')
55
+ // Standardize the decimal separator to `.`.
56
+ if (decimal !== '.') out = out.split(decimal).join('.')
57
+ // Drop anything that isn't a digit, dot, or minus.
58
+ out = out.replace(/[^0-9.-]/g, '')
59
+
60
+ // Enforce the canonical shape: a single optional leading minus, then digits
61
+ // with at most one decimal point. Stray separators (`1.2.3`) or interior
62
+ // minus signs (`12-3.4`) would otherwise reach the API as malformed values.
63
+ const negative = out.startsWith('-')
64
+ const digitsAndDots = out.replace(/-/g, '')
65
+ const firstDot = digitsAndDots.indexOf('.')
66
+ const canonical =
67
+ firstDot === -1
68
+ ? digitsAndDots
69
+ : `${digitsAndDots.slice(0, firstDot)}.${digitsAndDots.slice(firstDot + 1).replace(/\./g, '')}`
70
+
71
+ if (canonical === '' || canonical === '.') return ''
72
+ return negative ? `-${canonical}` : canonical
73
+ }
@@ -0,0 +1,32 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { normalizeQuantityRule } from './normalize-quantity'
3
+
4
+ describe('normalizeQuantityRule', () => {
5
+ it('reads a whole number', () => {
6
+ expect(normalizeQuantityRule('48')).toBe(48)
7
+ expect(normalizeQuantityRule(24)).toBe(24)
8
+ })
9
+
10
+ // Blank means the rule is unset, which is not zero: an empty minimum lets
11
+ // a buyer take one.
12
+ it('treats a blank field as unset', () => {
13
+ expect(normalizeQuantityRule('')).toBeNull()
14
+ expect(normalizeQuantityRule(' ')).toBeNull()
15
+ expect(normalizeQuantityRule(null)).toBeNull()
16
+ expect(normalizeQuantityRule(undefined)).toBeNull()
17
+ })
18
+
19
+ it('rejects values a rule cannot mean', () => {
20
+ expect(normalizeQuantityRule('0')).toBeNull()
21
+ expect(normalizeQuantityRule('-5')).toBeNull()
22
+ expect(normalizeQuantityRule('abc')).toBeNull()
23
+ })
24
+
25
+ // Truncating would be the same silent adjustment the whole feature exists
26
+ // to prevent, one level up.
27
+ it('refuses a fraction rather than truncating it', () => {
28
+ expect(normalizeQuantityRule('48.9')).toBeNull()
29
+ expect(normalizeQuantityRule('2.5')).toBeNull()
30
+ expect(normalizeQuantityRule(1.5)).toBeNull()
31
+ })
32
+ })
@@ -0,0 +1,31 @@
1
+ // Purchasing-rule normalization for the variant editors, mirroring
2
+ // `Spree::Variant`'s own validations so a merchant hears about a bad value
3
+ // while typing rather than on save.
4
+
5
+ /** What a buyer is quoted in. Stored quantities stay units at every level. */
6
+ export const PURCHASE_UNITS = ['unit', 'carton'] as const
7
+
8
+ export type PurchaseUnit = (typeof PURCHASE_UNITS)[number]
9
+
10
+ /**
11
+ * Reads a quantity rule from a number input. Blank means the rule is unset,
12
+ * which is not the same as zero: an empty minimum lets a buyer take one, a
13
+ * minimum of zero is meaningless and the server refuses it.
14
+ *
15
+ * A fraction is refused rather than truncated. These rules are whole units,
16
+ * and this feature exists because silently adjusting a wholesale quantity is
17
+ * a five-figure surprise — a rule of 2.5 saved as 2 is the same mistake one
18
+ * level up.
19
+ *
20
+ * @returns a positive whole number, or null when the field says nothing or
21
+ * cannot be one.
22
+ */
23
+ export function normalizeQuantityRule(value: unknown): number | null {
24
+ const trimmed = String(value ?? '').trim()
25
+ if (trimmed === '') return null
26
+
27
+ const parsed = Number(trimmed)
28
+ if (!Number.isInteger(parsed) || parsed < 1) return null
29
+
30
+ return parsed
31
+ }
@@ -0,0 +1,165 @@
1
+ import type { ProductFormValues, VariantPriceFormValues } from '@spree/dashboard-core'
2
+ import { type BulkPriceRow, BulkPriceTable } from '@spree/dashboard-ui'
3
+ import { useCallback, useMemo } from 'react'
4
+ import { type UseFormReturn, useWatch } from 'react-hook-form'
5
+ import { useTranslation } from 'react-i18next'
6
+ import { composeOptionsText } from '../products/variants-matrix'
7
+ import { currencyParts } from './currency-parts'
8
+ import { normalizeMoneyInput } from './normalize-money'
9
+ import {
10
+ useFormCurrencyLocale as useCurrencyLocale,
11
+ useFormOptionTypes as useOptionTypes,
12
+ } from './use-product-form-data'
13
+
14
+ interface Props {
15
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
16
+ form: UseFormReturn<ProductFormValues, any, any>
17
+ /** ISO currency code (e.g. "USD", "EUR"). */
18
+ currency: string
19
+ /** Display label for the product, used as the table's section header. */
20
+ productName: string
21
+ }
22
+
23
+ /**
24
+ * Form-backed prices spreadsheet for ONE product's variants in ONE currency.
25
+ * Reads from and writes to the parent product form's `variants[].prices`
26
+ * array — works equally for persisted and unsaved variants, since both live
27
+ * in form state the same way. No server fetch, no edit-tracking map: the
28
+ * parent form's `isDirty` is the single source of truth for save gating.
29
+ *
30
+ * Amounts are STRINGS, not numbers. The merchant's raw input flows straight
31
+ * through to the API; the backend's `Spree::LocalizedNumber.parse` handles
32
+ * locale-aware parsing (comma decimals, grouped digits, etc.) so the
33
+ * frontend doesn't have to reimplement it. Frontend coercion via `Number()`
34
+ * would silently mangle inputs like `"1.234,56"` into `NaN` and drop the
35
+ * price entirely.
36
+ */
37
+ export function ProductBulkPriceEditor({ form, currency, productName }: Props) {
38
+ const { t } = useTranslation()
39
+ const variants = useWatch({ control: form.control, name: 'variants' }) ?? []
40
+ const { data: optionTypesData } = useOptionTypes()
41
+ const optionTypes = useMemo(() => optionTypesData?.data ?? [], [optionTypesData])
42
+ const localeForCurrency = useCurrencyLocale()
43
+
44
+ // Format the grid in the currency's market locale (e.g. EUR → `de`, comma
45
+ // decimal). The same locale normalizes the merchant's input back to canonical
46
+ // form on commit (see `handleChange`), so form state — like the API value it
47
+ // hydrates from — is ALWAYS canonical `"1234.56"`. Untouched prices therefore
48
+ // never get re-normalized on save.
49
+ //
50
+ // Fall back to `en` (canonical period-decimal), NOT the UI language: money
51
+ // formatting/parsing must never depend on the dashboard's language, or a USD
52
+ // value under a German UI would be parsed as `40.00` → 4000.
53
+ const marketLocale = localeForCurrency(currency) || 'en'
54
+ const { symbol, decimal } = useMemo(
55
+ () => currencyParts(currency, marketLocale),
56
+ [currency, marketLocale],
57
+ )
58
+
59
+ // Project the form's variants into BulkPriceRow[] for the picked currency.
60
+ // One header row per product (always this product), then one row per variant.
61
+ // Row id is the variant's array index so onChange can write back unambiguously,
62
+ // even for variants without a persisted id.
63
+ const rows = useMemo<BulkPriceRow[]>(() => {
64
+ if (variants.length === 0) return []
65
+ const out: BulkPriceRow[] = [{ id: `header:product`, kind: 'header', groupLabel: productName }]
66
+ variants.forEach((v, idx) => {
67
+ const price = (v.prices ?? []).find((p) => p.currency === currency)
68
+ const label = v.options.length > 0 ? composeOptionsText(v.options, optionTypes) : null
69
+ out.push({
70
+ id: `variant:${idx}`,
71
+ kind: 'item',
72
+ variantLabel: label,
73
+ sku: v.sku ?? null,
74
+ // Display the canonical-decimal string swapped into the user's
75
+ // locale separator. The merchant's raw input is stored as-is in
76
+ // form state — this swap is presentation-only.
77
+ amount: price?.amount ? price.amount.replace('.', decimal) : null,
78
+ compareAt: price?.compare_at_amount ? price.compare_at_amount.replace('.', decimal) : null,
79
+ })
80
+ })
81
+ return out
82
+ }, [variants, currency, productName, decimal, optionTypes])
83
+
84
+ const handleChange = useCallback(
85
+ (rowId: string, field: 'amount' | 'compareAt', next: string | null) => {
86
+ if (!rowId.startsWith('variant:')) return
87
+ const idx = Number.parseInt(rowId.slice('variant:'.length), 10)
88
+ if (Number.isNaN(idx)) return
89
+
90
+ const current = form.getValues(`variants.${idx}.prices`) ?? []
91
+ const existingIdx = current.findIndex((p) => p.currency === currency)
92
+ // Normalize the merchant's localized input to canonical `"1234.56"` here,
93
+ // on commit — so form state is always canonical (matching the API values
94
+ // it hydrates from) and the save path never re-normalizes. Empty,
95
+ // whitespace, OR malformed input (normalizes to `''`) all mean "no value"
96
+ // — never persist `''` as a real amount.
97
+ const trimmed = next == null ? '' : next.trim()
98
+ const normalized = trimmed === '' ? '' : normalizeMoneyInput(trimmed, marketLocale)
99
+ const raw = normalized === '' ? null : normalized
100
+
101
+ const nextPrices: VariantPriceFormValues[] = [...current]
102
+
103
+ if (existingIdx === -1) {
104
+ // No price entry in this currency yet. Only create one when the user
105
+ // sets the AMOUNT — a compare-at-only edit without a base amount is
106
+ // meaningless (and would otherwise persist a real $0 price). Defer
107
+ // compare-at until amount exists.
108
+ if (raw == null) return
109
+ if (field !== 'amount') return
110
+ nextPrices.push({
111
+ currency,
112
+ amount: raw,
113
+ compare_at_amount: null,
114
+ })
115
+ } else {
116
+ const existing = nextPrices[existingIdx]
117
+ if (field === 'amount') {
118
+ // Clearing the amount removes the price entry for this currency.
119
+ // Coercing to "0" would persist a real $0 price and never allow
120
+ // the merchant to drop a currency from a variant.
121
+ if (raw == null) {
122
+ nextPrices.splice(existingIdx, 1)
123
+ } else {
124
+ nextPrices[existingIdx] = { ...existing, amount: raw }
125
+ }
126
+ } else {
127
+ nextPrices[existingIdx] = { ...existing, compare_at_amount: raw }
128
+ }
129
+ }
130
+
131
+ form.setValue(`variants.${idx}.prices`, nextPrices, {
132
+ shouldDirty: true,
133
+ })
134
+ },
135
+ [form, currency, marketLocale],
136
+ )
137
+
138
+ return (
139
+ <BulkPriceTable
140
+ rows={rows}
141
+ symbol={symbol}
142
+ decimal={decimal}
143
+ onChange={handleChange}
144
+ labels={{
145
+ variant: t('admin.pages.products.price_lists.edit_prices.columns.variant'),
146
+ sku: t('admin.pages.products.price_lists.edit_prices.columns.sku'),
147
+ price: t('admin.pages.products.price_lists.edit_prices.columns.price'),
148
+ compareAt: t('admin.pages.products.price_lists.edit_prices.columns.compare_at_price'),
149
+ variantDefault: t('admin.pages.products.price_lists.edit_prices.variant_default'),
150
+ loading: t('admin.common.loading'),
151
+ pageOf: t('admin.common.page_of', { page: '{page}', total: '{total}' }),
152
+ prev: t('admin.common.prev'),
153
+ next: t('admin.common.next'),
154
+ emptyMessage: t('admin.pages.products.edit.bulk_prices.no_variants'),
155
+ gridAriaLabel: t('admin.pages.products.price_lists.edit_prices.grid_aria'),
156
+ priceAriaTemplate: t('admin.pages.products.price_lists.edit_prices.price_aria', {
157
+ label: '{label}',
158
+ }),
159
+ compareAtAriaTemplate: t('admin.pages.products.price_lists.edit_prices.compare_at_aria', {
160
+ label: '{label}',
161
+ }),
162
+ }}
163
+ />
164
+ )
165
+ }