@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.
@@ -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": {
@@ -13,7 +13,7 @@ import {
13
13
  useSortable,
14
14
  } from '@dnd-kit/sortable'
15
15
  import { CSS } from '@dnd-kit/utilities'
16
- import type { CurrencyCode } from '@spree/core'
16
+ import { type CurrencyCode, randomUUID } from '@spree/core'
17
17
  import type { MediaType, ProductFormValues } from '@spree/dashboard-core'
18
18
  import {
19
19
  getApiClient,
@@ -368,7 +368,7 @@ export function MediaCard({
368
368
  async (files: FileList | File[]) => {
369
369
  const fileArray = Array.from(files)
370
370
  for (const file of fileArray) {
371
- const uploadId = crypto.randomUUID()
371
+ const uploadId = randomUUID()
372
372
  const preview = URL.createObjectURL(file)
373
373
  setPending((prev) => [...prev, { id: uploadId, file, preview, progress: 'uploading' }])
374
374
  try {
@@ -445,7 +445,7 @@ export function MediaCard({
445
445
  ...entries.map((entry, index) => ({
446
446
  ...entry,
447
447
  position: current.length + index + 1,
448
- uploadId: crypto.randomUUID(),
448
+ uploadId: randomUUID(),
449
449
  })),
450
450
  ],
451
451
  { shouldDirty: true },
@@ -1,11 +1,12 @@
1
1
  import { describe, expect, it } from 'vitest'
2
2
  import {
3
+ omitUnchangedListFields,
3
4
  prepareVariantsTaxForProductSave,
4
5
  variantHasTaxCategoryOverride,
5
6
  variantToFormValues,
6
7
  variantToWirePayload,
7
8
  } from './product-form-mapping'
8
- import type { VariantFormValues } from './product-schema'
9
+ import type { ProductFormValues, VariantFormValues } from './product-schema'
9
10
  import type { PanelVariant } from './product-types'
10
11
 
11
12
  // variantToFormValues reads a handful of fields; cast a minimal fixture rather
@@ -129,3 +130,43 @@ describe('variant tax category overrides', () => {
129
130
  expect(variantToWirePayload(prepared[0], 0).tax_category_id).toBeNull()
130
131
  })
131
132
  })
133
+
134
+ describe('omitUnchangedListFields', () => {
135
+ const payload = {
136
+ name: 'Linen shirt',
137
+ category_ids: ['ctg_1'],
138
+ collection_ids: ['coll_1'],
139
+ tags: ['summer'],
140
+ }
141
+
142
+ it('sends a list when its field changed', () => {
143
+ expect(omitUnchangedListFields(payload, { category_ids: true })).toEqual({
144
+ name: 'Linen shirt',
145
+ category_ids: ['ctg_1'],
146
+ })
147
+ expect(omitUnchangedListFields(payload, { tags: true, collection_ids: true })).toEqual({
148
+ name: 'Linen shirt',
149
+ collection_ids: ['coll_1'],
150
+ tags: ['summer'],
151
+ })
152
+ })
153
+
154
+ // React Hook Form marks an array field dirty per item when it compares item by item.
155
+ it('reads a per-item dirty flag', () => {
156
+ expect(omitUnchangedListFields(payload, { category_ids: [false, true] })).toEqual({
157
+ name: 'Linen shirt',
158
+ category_ids: ['ctg_1'],
159
+ })
160
+ expect(omitUnchangedListFields(payload, { category_ids: [false] })).toEqual({
161
+ name: 'Linen shirt',
162
+ })
163
+ })
164
+
165
+ // Another tab, a bulk action or an import may have added to a list since the
166
+ // page loaded; sending the loaded list back would remove those additions.
167
+ it('leaves the lists out when their fields did not change', () => {
168
+ const dirtyFields: Partial<Record<keyof ProductFormValues, boolean>> = { name: true }
169
+
170
+ expect(omitUnchangedListFields(payload, dirtyFields)).toEqual({ name: 'Linen shirt' })
171
+ })
172
+ })
@@ -195,6 +195,25 @@ export function withExtensionRowValues(
195
195
  }
196
196
  }
197
197
 
198
+ // The API replaces each of these lists with what a save sends.
199
+ const LIST_FIELDS = ['category_ids', 'collection_ids', 'tags'] as const
200
+
201
+ /**
202
+ * The save payload without the category, collection and tag lists the merchant
203
+ * left unchanged, so a save keeps what something else added since the page loaded.
204
+ */
205
+ export function omitUnchangedListFields(
206
+ payload: Record<string, unknown>,
207
+ dirtyFields: Partial<Record<(typeof LIST_FIELDS)[number], unknown>>,
208
+ ): Record<string, unknown> {
209
+ const trimmed = { ...payload }
210
+ for (const field of LIST_FIELDS) {
211
+ const dirty = dirtyFields[field]
212
+ if (!(Array.isArray(dirty) ? dirty.some(Boolean) : dirty)) delete trimmed[field]
213
+ }
214
+ return trimmed
215
+ }
216
+
198
217
  // Strip UI-only fields (stock_location_name) and undefined entries so the
199
218
  // PATCH body matches the Admin API VariantUpdateParams shape exactly. The
200
219
  // Spree::Product#variants= setter reconciles by id, creates new entries,