@designcrowd/library.brand-page 6.0.6-undo-8 → 6.0.6-undo-10

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 (29) hide show
  1. package/lib/src/{CreatedOnBrandCrowd-de-de-DdT7VIEh.js → CreatedOnBrandCrowd-de-de-C2L1EZgT.js} +1 -1
  2. package/lib/src/{CreatedOnBrandCrowd-en-us-YSkiGVKr.js → CreatedOnBrandCrowd-en-us-DHyH1IiM.js} +1 -1
  3. package/lib/src/{CreatedOnBrandCrowd-es-es-1EPcCaOy.js → CreatedOnBrandCrowd-es-es-A1e80jTb.js} +1 -1
  4. package/lib/src/{CreatedOnBrandCrowd-fr-fr-C8fnEHlw.js → CreatedOnBrandCrowd-fr-ca-DHVMHkEQ.js} +1 -1
  5. package/lib/src/{CreatedOnBrandCrowd-fr-ca-C8fnEHlw.js → CreatedOnBrandCrowd-fr-fr-DHVMHkEQ.js} +1 -1
  6. package/lib/src/{CreatedOnBrandCrowd-pt-pt-Dl3Se_ur.js → CreatedOnBrandCrowd-pt-br-Bte4ECWP.js} +1 -1
  7. package/lib/src/{CreatedOnBrandCrowd-pt-br-Dl3Se_ur.js → CreatedOnBrandCrowd-pt-pt-Bte4ECWP.js} +1 -1
  8. package/lib/src/{CreatedOnDcom-de-de-rXbL8YwY.js → CreatedOnDcom-de-de-5c5j6YI-.js} +1 -1
  9. package/lib/src/{CreatedOnDcom-en-us-Bm9CAhKw.js → CreatedOnDcom-en-us-CXjiYOAe.js} +1 -1
  10. package/lib/src/{CreatedOnDcom-es-es-B8diEgO8.js → CreatedOnDcom-es-es-Dd1HoC__.js} +1 -1
  11. package/lib/src/{CreatedOnDcom-fr-ca-b6jqk1vU.js → CreatedOnDcom-fr-ca-8l8Jgm76.js} +1 -1
  12. package/lib/src/{CreatedOnDcom-fr-fr-ECBv8SyF.js → CreatedOnDcom-fr-fr-DOvBm9IA.js} +1 -1
  13. package/lib/src/{CreatedOnDcom-pt-br-C7THecsn.js → CreatedOnDcom-pt-br-C67z2_nX.js} +1 -1
  14. package/lib/src/{CreatedOnDcom-pt-pt-C7f06hrr.js → CreatedOnDcom-pt-pt-CrIk4yit.js} +1 -1
  15. package/lib/src/css/library.brand-page-brandCrowd.min.css +8 -2
  16. package/lib/src/css/library.brand-page-brandPage.min.css +8 -2
  17. package/lib/src/css/library.brand-page-designCom.min.css +8 -2
  18. package/lib/src/css/library.brand-page-preview.min.css +8 -2
  19. package/lib/src/index.mjs +1 -1
  20. package/lib/src/{lib-DQvE17ni.js → lib-E0I6wyrj.js} +1164 -1150
  21. package/lib/src/shared/content-settings/components/form-fields-field/FormFieldsField.mixin.js +5 -4
  22. package/lib/src/shared/content-settings/store/content-settings-store.js +8 -11
  23. package/package.json +1 -1
  24. package/src/brand-page-maker/components/quick-link-bar/RedoButton.vue +15 -1
  25. package/src/brand-page-maker/components/quick-link-bar/UndoButton.vue +15 -1
  26. package/src/brand-page-maker/components/quick-link-bar/undo-redo-buttons.test.ts +7 -0
  27. package/src/shared/content-settings/components/form-fields-field/FormFieldsField.mixin.ts +9 -6
  28. package/src/shared/content-settings/store/content-settings-store.test.ts +63 -25
  29. package/src/shared/content-settings/store/content-settings-store.ts +8 -12
@@ -69,8 +69,10 @@ export default defineComponent({
69
69
  },
70
70
  methods: {
71
71
  toggleEnabled(index, value) {
72
- const result = [...this.value];
73
- result[index].enabled = value;
72
+ // Clone the row — `this.value[index]` is the store's reactive
73
+ // proxy and mutating it in place would leak the change to the
74
+ // store before the emit reaches updateItemValue.
75
+ const result = this.value.map((field, i) => i === index ? { ...field, enabled: value } : field);
74
76
  this.$emit('update:modelValue', {
75
77
  id: this.id,
76
78
  name: this.name,
@@ -78,8 +80,7 @@ export default defineComponent({
78
80
  });
79
81
  },
80
82
  toggleRequired(index, value) {
81
- const result = [...this.value];
82
- result[index].required = value;
83
+ const result = this.value.map((field, i) => i === index ? { ...field, required: value } : field);
83
84
  this.$emit('update:modelValue', {
84
85
  id: this.id,
85
86
  name: this.name,
@@ -14,17 +14,14 @@ 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
+ // Identity-only, not deep-equal. Drops the two BP-5772 phantoms
18
+ // (LogoSelectionField Upload re-click passes props.content.logoImage —
19
+ // the same proxy that's in the store; display-style re-click emits the
20
+ // stored primitive) and never false-positives on a caller that mutates
21
+ // the store in place via a shared reference (e.g. `[...this.value];
22
+ // result[i].x = v`), which would leave the payload byte-identical to
23
+ // the store while state actually changed.
24
+ const isSameValue = (a, b) => a === b;
28
25
  export const CONTENT_SETTINGS_MODES = {
29
26
  edit: 'edit',
30
27
  add: 'add',
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-8",
4
+ "version": "6.0.6-undo-10",
5
5
  "author": "Design.com Eng",
6
6
  "license": "UNLICENSED",
7
7
  "type": "module",
@@ -3,7 +3,10 @@
3
3
  direction="top"
4
4
  alignment="center"
5
5
  :should-show-icon="false"
6
- class="c-redo-button-tooltip"
6
+ :class="[
7
+ 'c-redo-button-tooltip',
8
+ { 'c-tooltip-hidden': isMobileScreenSize },
9
+ ]"
7
10
  >
8
11
  <template #open>
9
12
  <Icon
@@ -39,13 +42,20 @@ import { Icon, Tooltip } from '@designcrowd/fe-shared-lib';
39
42
  import { useHistoryStore } from '@brand-page-maker/stores/history/history-store';
40
43
  import { useAiWebsiteBuilderThreadStore } from '@brand-page-maker/stores/ai-thread/ai-website-builder-thread-store';
41
44
  import { useBrandPageContainerStore } from '@brand-page-maker/stores/brandpage-container';
45
+ import { useUiStore } from '@brand-page-maker/stores/ui/ui-store';
42
46
  import { useUiTemplateSettingsStore } from '@shared/template-settings/store';
47
+ import { UI_SCREENSIZE_MODES } from '@shared/models';
43
48
  import { quickLinkBarTr } from '@src/composables/useTranslate';
44
49
 
45
50
  const historyStore = useHistoryStore();
46
51
  const aiThreadStore = useAiWebsiteBuilderThreadStore();
47
52
  const containerStore = useBrandPageContainerStore();
48
53
  const templateSettingsStore = useUiTemplateSettingsStore();
54
+ const uiStore = useUiStore();
55
+
56
+ const isMobileScreenSize = computed(
57
+ () => uiStore.screenSizeMode === UI_SCREENSIZE_MODES.mobile,
58
+ );
49
59
 
50
60
  const isDisabled = computed(
51
61
  () =>
@@ -71,4 +81,8 @@ const onClick = () => {
71
81
  width: auto;
72
82
  padding: 4px 8px;
73
83
  }
84
+
85
+ .c-tooltip-hidden :deep(.shared-tooltip__content) {
86
+ display: none !important;
87
+ }
74
88
  </style>
@@ -3,7 +3,10 @@
3
3
  direction="top"
4
4
  alignment="center"
5
5
  :should-show-icon="false"
6
- class="c-undo-button-tooltip"
6
+ :class="[
7
+ 'c-undo-button-tooltip',
8
+ { 'c-tooltip-hidden': isMobileScreenSize },
9
+ ]"
7
10
  >
8
11
  <template #open>
9
12
  <Icon
@@ -39,13 +42,20 @@ import { Icon, Tooltip } from '@designcrowd/fe-shared-lib';
39
42
  import { useHistoryStore } from '@brand-page-maker/stores/history/history-store';
40
43
  import { useAiWebsiteBuilderThreadStore } from '@brand-page-maker/stores/ai-thread/ai-website-builder-thread-store';
41
44
  import { useBrandPageContainerStore } from '@brand-page-maker/stores/brandpage-container';
45
+ import { useUiStore } from '@brand-page-maker/stores/ui/ui-store';
42
46
  import { useUiTemplateSettingsStore } from '@shared/template-settings/store';
47
+ import { UI_SCREENSIZE_MODES } from '@shared/models';
43
48
  import { quickLinkBarTr } from '@src/composables/useTranslate';
44
49
 
45
50
  const historyStore = useHistoryStore();
46
51
  const aiThreadStore = useAiWebsiteBuilderThreadStore();
47
52
  const containerStore = useBrandPageContainerStore();
48
53
  const templateSettingsStore = useUiTemplateSettingsStore();
54
+ const uiStore = useUiStore();
55
+
56
+ const isMobileScreenSize = computed(
57
+ () => uiStore.screenSizeMode === UI_SCREENSIZE_MODES.mobile,
58
+ );
49
59
 
50
60
  const isDisabled = computed(
51
61
  () =>
@@ -71,4 +81,8 @@ const onClick = () => {
71
81
  width: auto;
72
82
  padding: 4px 8px;
73
83
  }
84
+
85
+ .c-tooltip-hidden :deep(.shared-tooltip__content) {
86
+ display: none !important;
87
+ }
74
88
  </style>
@@ -66,6 +66,12 @@ vi.mock('@shared/template-settings/store', () => ({
66
66
  useUiTemplateSettingsStore: vi.fn(() => templateSettingsStoreMock),
67
67
  }));
68
68
 
69
+ const uiStoreMock = { screenSizeMode: 'desktop' as 'desktop' | 'mobile' };
70
+
71
+ vi.mock('@brand-page-maker/stores/ui/ui-store', () => ({
72
+ useUiStore: vi.fn(() => uiStoreMock),
73
+ }));
74
+
69
75
  beforeEach(() => {
70
76
  setActivePinia(createPinia());
71
77
  historyStoreMock.canUndo = true;
@@ -76,6 +82,7 @@ beforeEach(() => {
76
82
  aiStoreMock.isProcessing = false;
77
83
  containerStoreMock.isSaving = false;
78
84
  templateSettingsStoreMock.isBusy = false;
85
+ uiStoreMock.screenSizeMode = 'desktop';
79
86
  });
80
87
 
81
88
  describe('UndoButton', () => {
@@ -81,9 +81,12 @@ export default defineComponent({
81
81
  },
82
82
  methods: {
83
83
  toggleEnabled(index, value) {
84
- const result = [...this.value];
85
-
86
- result[index].enabled = value;
84
+ // Clone the row — `this.value[index]` is the store's reactive
85
+ // proxy and mutating it in place would leak the change to the
86
+ // store before the emit reaches updateItemValue.
87
+ const result = this.value.map((field, i) =>
88
+ i === index ? { ...field, enabled: value } : field,
89
+ );
87
90
 
88
91
  this.$emit('update:modelValue', {
89
92
  id: this.id,
@@ -92,9 +95,9 @@ export default defineComponent({
92
95
  });
93
96
  },
94
97
  toggleRequired(index, value) {
95
- const result = [...this.value];
96
-
97
- result[index].required = value;
98
+ const result = this.value.map((field, i) =>
99
+ i === index ? { ...field, required: value } : field,
100
+ );
98
101
 
99
102
  this.$emit('update:modelValue', {
100
103
  id: this.id,
@@ -207,30 +207,26 @@ describe('content settings store — hydration writes arm the history merge', ()
207
207
  });
208
208
  });
209
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.
210
+ // BP-5772: a same-reference re-write must be dropped — the assignment
211
+ // is a reactive no-op but onContentChanged()'s flip would land a phantom
212
+ // history entry. Guard is identity-only; a deep-equal guard would also
213
+ // drop writes where the caller mutated the store in place before emit
214
+ // (FormFieldsField pre-fix) and break the canvas re-render.
219
215
  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
216
  const makeItem = () => ({
227
217
  id: 'item-1',
228
218
  type: 'logo',
229
219
  settings: {
230
220
  content: {
231
221
  title: '<p>old</p>',
222
+ displayStyle: 'preview',
232
223
  isLogoVisible: true,
233
- logoImage: logoImage(),
224
+ logoImage: {
225
+ logoDraftToken: 'dae5cda0-602d-4c69-a26e-000000000001',
226
+ logoVariation: 0,
227
+ logoLastUpdate: '638000000000000000',
228
+ },
229
+ logoTemplateImage: { url: 'https://example.test/tpl.png' },
234
230
  },
235
231
  },
236
232
  });
@@ -254,26 +250,32 @@ describe('content settings store — value-identical writes are dropped', () =>
254
250
 
255
251
  test('updateMultipleItemValues drops the logo-panel Upload re-click payload', () => {
256
252
  const store = setupStore();
253
+ const content = store.items[0].settings.content as Record<string, unknown>;
257
254
 
258
- // Deep-equal to the model but every reference is fresh — exactly what
259
- // the Upload option click emits when visibility didn't change.
255
+ // Exactly what LogoSelectionField.onLogoVisibleChanged emits when
256
+ // visibility didn't change: the same reactive-proxy refs it read from
257
+ // the store, plus the current primitive visibility value.
260
258
  store.updateMultipleItemValues({
261
259
  id: 'item-1',
262
260
  name: 'logoImage',
263
- value: { logoImage: logoImage(), isLogoVisible: true },
261
+ value: {
262
+ logoImage: content.logoImage,
263
+ logoTemplateImage: content.logoTemplateImage,
264
+ isLogoVisible: content.isLogoVisible,
265
+ },
264
266
  });
265
267
 
266
268
  expect(store.onContentChanged).not.toHaveBeenCalled();
267
269
  expect(store.validateItem).not.toHaveBeenCalled();
268
270
  });
269
271
 
270
- test('updateItemValue drops an identical primitive', () => {
272
+ test('updateItemValue drops an identical primitive (display-style re-click)', () => {
271
273
  const store = setupStore();
272
274
 
273
275
  store.updateItemValue({
274
276
  id: 'item-1',
275
- name: 'title',
276
- value: '<p>old</p>',
277
+ name: 'displayStyle',
278
+ value: 'preview',
277
279
  });
278
280
 
279
281
  expect(store.onContentChanged).not.toHaveBeenCalled();
@@ -282,11 +284,12 @@ describe('content settings store — value-identical writes are dropped', () =>
282
284
 
283
285
  test('updateMultipleItemValues writes when any one field differs', () => {
284
286
  const store = setupStore();
287
+ const content = store.items[0].settings.content as Record<string, unknown>;
285
288
 
286
289
  store.updateMultipleItemValues({
287
290
  id: 'item-1',
288
291
  name: 'logoImage',
289
- value: { logoImage: logoImage(), isLogoVisible: false },
292
+ value: { logoImage: content.logoImage, isLogoVisible: false },
290
293
  });
291
294
 
292
295
  expect(store.onContentChanged).toHaveBeenCalledTimes(1);
@@ -301,11 +304,46 @@ describe('content settings store — value-identical writes are dropped', () =>
301
304
 
302
305
  store.updateItemValue({
303
306
  id: 'item-1',
304
- name: 'title',
305
- value: '<p>old</p>',
307
+ name: 'displayStyle',
308
+ value: 'preview',
306
309
  isHydrationWrite: true,
307
310
  });
308
311
 
309
312
  expect(mergeNextMutationMock).not.toHaveBeenCalled();
310
313
  });
314
+
315
+ // Contact-form toggle repro (bp-5772 UAT env): pre-fix FormFieldsField
316
+ // shallow-copied `[...this.value]` and mutated `result[i].enabled` —
317
+ // hitting the store's inner obj before emit. Deep-equal guard would
318
+ // drop this as a no-op and the canvas would stay stale.
319
+ test('updateItemValue writes when the outer container is a fresh reference, even if a caller mutated the shared leaf in place', () => {
320
+ const store = setupStore();
321
+ const content = store.items[0].settings.content as Record<string, unknown>;
322
+
323
+ content.fields = [
324
+ { id: 'firstName', enabled: true, required: true, type: 'text' },
325
+ { id: 'message', enabled: true, required: true, type: 'text-area' },
326
+ ];
327
+
328
+ const payloadValue = [...(content.fields as Record<string, unknown>[])];
329
+
330
+ payloadValue[0].enabled = false;
331
+
332
+ store.updateItemValue({
333
+ id: 'item-1',
334
+ name: 'fields',
335
+ value: payloadValue,
336
+ });
337
+
338
+ expect(store.onContentChanged).toHaveBeenCalledTimes(1);
339
+ expect(store.validateItem).toHaveBeenCalledTimes(1);
340
+ // Check the effect rather than identity — Pinia wraps in a proxy.
341
+ expect(
342
+ (
343
+ (store.items[0].settings.content as Record<string, unknown>).fields as {
344
+ enabled: boolean;
345
+ }[]
346
+ )[0].enabled,
347
+ ).toBe(false);
348
+ });
311
349
  });
@@ -60,18 +60,14 @@ 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);
63
+ // Identity-only, not deep-equal. Drops the two BP-5772 phantoms
64
+ // (LogoSelectionField Upload re-click passes props.content.logoImage —
65
+ // the same proxy that's in the store; display-style re-click emits the
66
+ // stored primitive) and never false-positives on a caller that mutates
67
+ // the store in place via a shared reference (e.g. `[...this.value];
68
+ // result[i].x = v`), which would leave the payload byte-identical to
69
+ // the store while state actually changed.
70
+ const isSameValue = (a: unknown, b: unknown): boolean => a === b;
75
71
 
76
72
  export const CONTENT_SETTINGS_MODES = {
77
73
  edit: 'edit',