@designcrowd/library.brand-page 6.0.6-undo-5 → 6.0.6-undo-6

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 (20) hide show
  1. package/lib/src/{CreatedOnBrandCrowd-de-de-BRaWY6nF.js → CreatedOnBrandCrowd-de-de-BfSQG4or.js} +1 -1
  2. package/lib/src/{CreatedOnBrandCrowd-en-us-D0DuxW2v.js → CreatedOnBrandCrowd-en-us-CO07UsMZ.js} +1 -1
  3. package/lib/src/{CreatedOnBrandCrowd-es-es-Du0Wjx0E.js → CreatedOnBrandCrowd-es-es-BElFyN72.js} +1 -1
  4. package/lib/src/{CreatedOnBrandCrowd-fr-fr-Cz1OGFZT.js → CreatedOnBrandCrowd-fr-ca-DnTxsYIb.js} +1 -1
  5. package/lib/src/{CreatedOnBrandCrowd-fr-ca-Cz1OGFZT.js → CreatedOnBrandCrowd-fr-fr-DnTxsYIb.js} +1 -1
  6. package/lib/src/{CreatedOnBrandCrowd-pt-pt-TfMgQ231.js → CreatedOnBrandCrowd-pt-br-DSMhWz5Z.js} +1 -1
  7. package/lib/src/{CreatedOnBrandCrowd-pt-br-TfMgQ231.js → CreatedOnBrandCrowd-pt-pt-DSMhWz5Z.js} +1 -1
  8. package/lib/src/{CreatedOnDcom-de-de-CIxWfXsX.js → CreatedOnDcom-de-de-C2469ycC.js} +1 -1
  9. package/lib/src/{CreatedOnDcom-en-us-BQeFEYIF.js → CreatedOnDcom-en-us-Cvz7kq6J.js} +1 -1
  10. package/lib/src/{CreatedOnDcom-es-es-Bu9Nr8t-.js → CreatedOnDcom-es-es-BvDTs1Y_.js} +1 -1
  11. package/lib/src/{CreatedOnDcom-fr-ca-Dpsu-wxQ.js → CreatedOnDcom-fr-ca-vJ9zwwPI.js} +1 -1
  12. package/lib/src/{CreatedOnDcom-fr-fr-CWGZAKa0.js → CreatedOnDcom-fr-fr-D90gm3ay.js} +1 -1
  13. package/lib/src/{CreatedOnDcom-pt-br-Ds8H3Kiq.js → CreatedOnDcom-pt-br-C1VGGfTF.js} +1 -1
  14. package/lib/src/{CreatedOnDcom-pt-pt-kDx8VWIo.js → CreatedOnDcom-pt-pt-DGL1py4g.js} +1 -1
  15. package/lib/src/index.mjs +1 -1
  16. package/lib/src/{lib-BJdIOaxe.js → lib-nGjOXase.js} +10145 -10140
  17. package/lib/src/shared/content-settings/store/content-settings-store.js +17 -0
  18. package/package.json +1 -1
  19. package/src/shared/content-settings/store/content-settings-store.test.ts +103 -0
  20. package/src/shared/content-settings/store/content-settings-store.ts +28 -0
@@ -14,6 +14,17 @@ const DEFAULT_TEXT_COLOR_DEPENDENT_TYPES = [
14
14
  'profile',
15
15
  'text',
16
16
  ];
17
+ /**
18
+ * True when the incoming value is byte-identical (as JSON, matching how
19
+ * history snapshots the model) to what the model already holds. Such
20
+ * re-writes must be dropped whole: the assignment would be a reactive
21
+ * no-op, but onContentChanged() still flips isModifiedAfterTemplateApplied
22
+ * — and when that flag was false, its flip is itself a model mutation, so
23
+ * undo history commits an entry in which nothing user-visible changed
24
+ * (phantom undo state, BP-5772: re-clicking the logo panel's Upload/Logo
25
+ * option, re-clicking an already-active display style).
26
+ */
27
+ const isSameValue = (a, b) => a === b || JSON.stringify(a) === JSON.stringify(b);
17
28
  export const CONTENT_SETTINGS_MODES = {
18
29
  edit: 'edit',
19
30
  add: 'add',
@@ -211,6 +222,9 @@ export const useUiContentSettingsStore = defineStore('uiContentSettings', {
211
222
  if (!item) {
212
223
  return;
213
224
  }
225
+ if (isSameValue(item.settings.content[payload.name], payload.value)) {
226
+ return; // no-op write: don't validate, don't flip the modified flag
227
+ }
214
228
  // Hydration write-backs (RTE canonical HTML on mount, lazy config
215
229
  // hydration, stale-id clears) fold into the current undo head instead
216
230
  // of pushing an entry. Arming here — same tick as the write — is what
@@ -232,6 +246,9 @@ export const useUiContentSettingsStore = defineStore('uiContentSettings', {
232
246
  if (!item) {
233
247
  return;
234
248
  }
249
+ if (Object.keys(value ?? {}).every((k) => isSameValue(item.settings.content[k], value[k]))) {
250
+ return; // every field a no-op — see isSameValue
251
+ }
235
252
  // Same hydration-write contract as updateItemValue.
236
253
  if (isHydrationWrite) {
237
254
  useHistoryStore().mergeNextMutation();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@designcrowd/library.brand-page",
3
3
  "description": "A collection of Maker/Designer applications",
4
- "version": "6.0.6-undo-5",
4
+ "version": "6.0.6-undo-6",
5
5
  "author": "Design.com Eng",
6
6
  "license": "UNLICENSED",
7
7
  "type": "module",
@@ -206,3 +206,106 @@ describe('content settings store — hydration writes arm the history merge', ()
206
206
  expect(mergeNextMutationMock).not.toHaveBeenCalled();
207
207
  });
208
208
  });
209
+
210
+ // BP-5772: a value-identical re-write must be dropped whole. The assignment
211
+ // would be a reactive no-op, but onContentChanged() still flips
212
+ // isModifiedAfterTemplateApplied — and when that flag was false, its flip is
213
+ // itself a model mutation, so history commits an entry in which nothing
214
+ // user-visible changed. Live repro: with the logo visible, clicking the logo
215
+ // panel's "Upload" option re-emits { logoImage, logoTemplateImage,
216
+ // logoImageBase64Url, isLogoVisible } deep-equal to the model under fresh
217
+ // references (LogoSelectionField.onLogoVisibleChanged). Same class:
218
+ // re-clicking the already-active display style.
219
+ describe('content settings store — value-identical writes are dropped', () => {
220
+ const logoImage = () => ({
221
+ logoDraftToken: 'dae5cda0-602d-4c69-a26e-000000000001',
222
+ logoVariation: 0,
223
+ logoLastUpdate: '638000000000000000',
224
+ });
225
+
226
+ const makeItem = () => ({
227
+ id: 'item-1',
228
+ type: 'logo',
229
+ settings: {
230
+ content: {
231
+ title: '<p>old</p>',
232
+ isLogoVisible: true,
233
+ logoImage: logoImage(),
234
+ },
235
+ },
236
+ });
237
+
238
+ const setupStore = () => {
239
+ setActivePinia(createPinia());
240
+
241
+ const store = useUiContentSettingsStore();
242
+
243
+ store.items.push(makeItem() as never);
244
+
245
+ store.validateItem = vi.fn();
246
+ store.onContentChanged = vi.fn();
247
+
248
+ return store;
249
+ };
250
+
251
+ beforeEach(() => {
252
+ mergeNextMutationMock.mockClear();
253
+ });
254
+
255
+ test('updateMultipleItemValues drops the logo-panel Upload re-click payload', () => {
256
+ const store = setupStore();
257
+
258
+ // Deep-equal to the model but every reference is fresh — exactly what
259
+ // the Upload option click emits when visibility didn't change.
260
+ store.updateMultipleItemValues({
261
+ id: 'item-1',
262
+ name: 'logoImage',
263
+ value: { logoImage: logoImage(), isLogoVisible: true },
264
+ });
265
+
266
+ expect(store.onContentChanged).not.toHaveBeenCalled();
267
+ expect(store.validateItem).not.toHaveBeenCalled();
268
+ });
269
+
270
+ test('updateItemValue drops an identical primitive', () => {
271
+ const store = setupStore();
272
+
273
+ store.updateItemValue({
274
+ id: 'item-1',
275
+ name: 'title',
276
+ value: '<p>old</p>',
277
+ });
278
+
279
+ expect(store.onContentChanged).not.toHaveBeenCalled();
280
+ expect(store.validateItem).not.toHaveBeenCalled();
281
+ });
282
+
283
+ test('updateMultipleItemValues writes when any one field differs', () => {
284
+ const store = setupStore();
285
+
286
+ store.updateMultipleItemValues({
287
+ id: 'item-1',
288
+ name: 'logoImage',
289
+ value: { logoImage: logoImage(), isLogoVisible: false },
290
+ });
291
+
292
+ expect(store.onContentChanged).toHaveBeenCalledTimes(1);
293
+ expect(
294
+ (store.items[0].settings.content as { isLogoVisible?: boolean })
295
+ .isLogoVisible,
296
+ ).toBe(false);
297
+ });
298
+
299
+ test('a dropped write never arms the history merge, even when hydration-flagged', () => {
300
+ const store = setupStore();
301
+
302
+ store.updateItemValue({
303
+ id: 'item-1',
304
+ name: 'title',
305
+ value: '<p>old</p>',
306
+ isHydrationWrite: true,
307
+ });
308
+
309
+ expect(mergeNextMutationMock).not.toHaveBeenCalled();
310
+ });
311
+ });
@@ -60,6 +60,19 @@ const DEFAULT_TEXT_COLOR_DEPENDENT_TYPES: readonly ContentType[] = [
60
60
  'text',
61
61
  ] as const;
62
62
 
63
+ /**
64
+ * True when the incoming value is byte-identical (as JSON, matching how
65
+ * history snapshots the model) to what the model already holds. Such
66
+ * re-writes must be dropped whole: the assignment would be a reactive
67
+ * no-op, but onContentChanged() still flips isModifiedAfterTemplateApplied
68
+ * — and when that flag was false, its flip is itself a model mutation, so
69
+ * undo history commits an entry in which nothing user-visible changed
70
+ * (phantom undo state, BP-5772: re-clicking the logo panel's Upload/Logo
71
+ * option, re-clicking an already-active display style).
72
+ */
73
+ const isSameValue = (a: unknown, b: unknown): boolean =>
74
+ a === b || JSON.stringify(a) === JSON.stringify(b);
75
+
63
76
  export const CONTENT_SETTINGS_MODES = {
64
77
  edit: 'edit',
65
78
  add: 'add',
@@ -390,6 +403,10 @@ export const useUiContentSettingsStore = defineStore('uiContentSettings', {
390
403
  return;
391
404
  }
392
405
 
406
+ if (isSameValue(item.settings.content[payload.name], payload.value)) {
407
+ return; // no-op write: don't validate, don't flip the modified flag
408
+ }
409
+
393
410
  // Hydration write-backs (RTE canonical HTML on mount, lazy config
394
411
  // hydration, stale-id clears) fold into the current undo head instead
395
412
  // of pushing an entry. Arming here — same tick as the write — is what
@@ -421,6 +438,17 @@ export const useUiContentSettingsStore = defineStore('uiContentSettings', {
421
438
  return;
422
439
  }
423
440
 
441
+ if (
442
+ Object.keys(value ?? {}).every((k) =>
443
+ isSameValue(
444
+ item.settings.content[k],
445
+ (value as Record<string, unknown>)[k],
446
+ ),
447
+ )
448
+ ) {
449
+ return; // every field a no-op — see isSameValue
450
+ }
451
+
424
452
  // Same hydration-write contract as updateItemValue.
425
453
  if (isHydrationWrite) {
426
454
  useHistoryStore().mergeNextMutation();