@designcrowd/library.brand-page 6.0.6-undo-2 → 6.0.6-undo-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.
- package/lib/src/{CreatedOnBrandCrowd-de-de-BSFTp6yl.js → CreatedOnBrandCrowd-de-de-x7BcD7Sj.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-en-us-rPQrOY1L.js → CreatedOnBrandCrowd-en-us-CG-ThD_q.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-es-es-awd8-QFa.js → CreatedOnBrandCrowd-es-es-aab4_Re2.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-fr-CwJ4J7Ow.js → CreatedOnBrandCrowd-fr-ca-Drl8X5uh.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-ca-CwJ4J7Ow.js → CreatedOnBrandCrowd-fr-fr-Drl8X5uh.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-pt-BXYfTLlD.js → CreatedOnBrandCrowd-pt-br-8Ib7T70i.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-br-BXYfTLlD.js → CreatedOnBrandCrowd-pt-pt-8Ib7T70i.js} +1 -1
- package/lib/src/{CreatedOnDcom-de-de-kX6u1lNK.js → CreatedOnDcom-de-de-FJtydzc5.js} +1 -1
- package/lib/src/{CreatedOnDcom-en-us-Bkq5sgFs.js → CreatedOnDcom-en-us-BMiXru6P.js} +1 -1
- package/lib/src/{CreatedOnDcom-es-es-zCAC9ujj.js → CreatedOnDcom-es-es-B8Srdnku.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-ca-BYz6Wzg5.js → CreatedOnDcom-fr-ca-tcXGiBNt.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-fr-CN-wAaSH.js → CreatedOnDcom-fr-fr-nE1cuUEU.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-br-BrK4Ngg-.js → CreatedOnDcom-pt-br-DxCCXCg8.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-pt-AfI_g7xJ.js → CreatedOnDcom-pt-pt-Csabip9G.js} +1 -1
- package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.d.ts +1 -1
- package/lib/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.d.ts +2 -2
- package/lib/src/brand-page-maker/components/preview/Preview.mixin.d.ts +1 -1
- package/lib/src/brand-page-maker/components/seo-settings/SeoPageSettings.mixin.d.ts +1 -1
- package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.js +17 -12
- package/lib/src/brand-page-maker/stores/history/history-store.d.ts +1 -1
- package/lib/src/index.mjs +1 -1
- package/lib/src/{lib-BYFWAvIq.js → lib-C_UlYMJy.js} +15640 -15612
- package/lib/src/shared/content-settings/components/GenericTextInput.mixin.d.ts +5 -0
- package/lib/src/shared/content-settings/components/GenericTextInput.mixin.js +29 -0
- package/lib/src/shared/content-settings/components/content-settings/ContentSettings.mixin.d.ts +2 -2
- package/lib/src/shared/content-settings/components/content-settings/ReplaceImageSettingsForContent.mixin.d.ts +1 -1
- package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItem.mixin.d.ts +1 -1
- package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.d.ts +1 -1
- package/lib/src/shared/content-settings/components/email-field/EmailField.mixin.d.ts +5 -0
- package/lib/src/shared/content-settings/components/focused-content-item-settings/FocusedContentItemSettings.mixin.d.ts +1 -1
- package/lib/src/shared/content-settings/components/payment-config-field/PaymentConfigField.mixin.d.ts +0 -67
- package/lib/src/shared/content-settings/components/payment-config-field/PaymentConfigField.mixin.js +4 -6
- package/lib/src/shared/content-settings/components/tel-field/TelField.mixin.d.ts +5 -0
- package/lib/src/shared/content-settings/components/text-field/TextField.mixin.d.ts +5 -0
- package/lib/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.d.ts +2 -0
- package/lib/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.js +19 -0
- package/lib/src/shared/content-settings/components/textarea-field/TextareaField.mixin.d.ts +5 -0
- package/lib/src/shared/content-settings/components/url-field/UrlField.mixin.d.ts +5 -0
- package/lib/src/shared/content-settings/components/youtube-video-field/YoutubeVideoField.mixin.d.ts +5 -0
- package/lib/src/shared/content-settings/models.d.ts +11 -0
- package/lib/src/shared/content-settings/store/content-settings-store.d.ts +1 -1
- package/lib/src/shared/content-settings/store/content-settings-store.js +15 -1
- package/lib/src/shared/page-settings/components/page-settings/PageSettings.mixin.d.ts +1 -1
- package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.d.ts +1 -1
- package/package.json +1 -1
- package/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.ts +18 -14
- package/src/shared/content-settings/components/GenericTextInput.mixin.test.ts +119 -0
- package/src/shared/content-settings/components/GenericTextInput.mixin.ts +34 -0
- package/src/shared/content-settings/components/page-dropdown-field/PageDropdownField.vue +10 -5
- package/src/shared/content-settings/components/payment-config-field/PaymentConfigField.mixin.ts +4 -7
- package/src/shared/content-settings/components/text-field/TextField.vue +2 -1
- package/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.test.ts +149 -0
- package/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.ts +26 -0
- package/src/shared/content-settings/components/textarea-field/TextareaField.vue +2 -1
- package/src/shared/content-settings/models.ts +11 -0
- package/src/shared/content-settings/store/content-settings-store.test.ts +110 -0
- package/src/shared/content-settings/store/content-settings-store.ts +17 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { defineComponent } from 'vue';
|
|
2
|
+
import { mount } from '@vue/test-utils';
|
|
3
|
+
import GenericTextInputMixin from './GenericTextInput.mixin';
|
|
4
|
+
|
|
5
|
+
// BP-5810: the RTE attaches `{ isHydration: true }` meta to its canonical
|
|
6
|
+
// write-back emits. GenericTextInput must carry that through its 100ms
|
|
7
|
+
// debounce onto the outgoing payload as `isHydrationWrite` — and downgrade
|
|
8
|
+
// the burst as soon as a single non-hydration write lands in it.
|
|
9
|
+
|
|
10
|
+
const DEBOUNCE_WAIT_MS = 100;
|
|
11
|
+
|
|
12
|
+
interface EmittedPayload {
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
value: string | false;
|
|
16
|
+
isHydrationWrite?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const TestHost = defineComponent({
|
|
20
|
+
mixins: [GenericTextInputMixin],
|
|
21
|
+
template: '<div />',
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const mountHost = () =>
|
|
25
|
+
mount(TestHost, {
|
|
26
|
+
props: {
|
|
27
|
+
id: 'item-1',
|
|
28
|
+
name: 'title',
|
|
29
|
+
value: '<p>seed</p>',
|
|
30
|
+
showRichTextEditor: true,
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const emittedPayload = (
|
|
35
|
+
wrapper: ReturnType<typeof mountHost>,
|
|
36
|
+
call: number,
|
|
37
|
+
): EmittedPayload =>
|
|
38
|
+
wrapper.emitted('update:modelValue')![call][0] as EmittedPayload;
|
|
39
|
+
|
|
40
|
+
describe('GenericTextInput hydration burst flag', () => {
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
vi.useFakeTimers();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
vi.useRealTimers();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('a hydration-only burst emits a payload flagged isHydrationWrite', async () => {
|
|
50
|
+
const wrapper = mountHost();
|
|
51
|
+
|
|
52
|
+
wrapper.vm.onRteModelValue('<p style="text-align: center;"></p>', {
|
|
53
|
+
isHydration: true,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
await vi.advanceTimersByTimeAsync(DEBOUNCE_WAIT_MS + 10);
|
|
57
|
+
|
|
58
|
+
const emitted = wrapper.emitted('update:modelValue');
|
|
59
|
+
|
|
60
|
+
expect(emitted).toHaveLength(1);
|
|
61
|
+
expect(emitted![0][0]).toEqual({
|
|
62
|
+
id: 'item-1',
|
|
63
|
+
name: 'title',
|
|
64
|
+
value: '<p style="text-align: center;"></p>',
|
|
65
|
+
isHydrationWrite: true,
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('coalesced hydration writes in one burst stay flagged', async () => {
|
|
70
|
+
const wrapper = mountHost();
|
|
71
|
+
|
|
72
|
+
wrapper.vm.onRteModelValue('<p>a</p>', { isHydration: true });
|
|
73
|
+
wrapper.vm.onRteModelValue('<p>b</p>', { isHydration: true });
|
|
74
|
+
|
|
75
|
+
await vi.advanceTimersByTimeAsync(DEBOUNCE_WAIT_MS + 10);
|
|
76
|
+
|
|
77
|
+
expect(wrapper.emitted('update:modelValue')).toHaveLength(1);
|
|
78
|
+
expect(emittedPayload(wrapper, 0).isHydrationWrite).toBe(true);
|
|
79
|
+
expect(emittedPayload(wrapper, 0).value).toBe('<p>b</p>');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('a user write in the same burst downgrades it to a normal edit', async () => {
|
|
83
|
+
const wrapper = mountHost();
|
|
84
|
+
|
|
85
|
+
wrapper.vm.onRteModelValue('<p>canonical</p>', { isHydration: true });
|
|
86
|
+
// Focused typing emits without meta.
|
|
87
|
+
wrapper.vm.onRteModelValue('<p>typed</p>');
|
|
88
|
+
|
|
89
|
+
await vi.advanceTimersByTimeAsync(DEBOUNCE_WAIT_MS + 10);
|
|
90
|
+
|
|
91
|
+
expect(wrapper.emitted('update:modelValue')).toHaveLength(1);
|
|
92
|
+
expect(emittedPayload(wrapper, 0).isHydrationWrite).toBeUndefined();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('a plain write (no meta) is never flagged', async () => {
|
|
96
|
+
const wrapper = mountHost();
|
|
97
|
+
|
|
98
|
+
wrapper.vm.text = '<p>typed</p>';
|
|
99
|
+
|
|
100
|
+
await vi.advanceTimersByTimeAsync(DEBOUNCE_WAIT_MS + 10);
|
|
101
|
+
|
|
102
|
+
expect(wrapper.emitted('update:modelValue')).toHaveLength(1);
|
|
103
|
+
expect(emittedPayload(wrapper, 0).isHydrationWrite).toBeUndefined();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('the flag resets between bursts', async () => {
|
|
107
|
+
const wrapper = mountHost();
|
|
108
|
+
|
|
109
|
+
wrapper.vm.onRteModelValue('<p>canonical</p>', { isHydration: true });
|
|
110
|
+
await vi.advanceTimersByTimeAsync(DEBOUNCE_WAIT_MS + 10);
|
|
111
|
+
|
|
112
|
+
wrapper.vm.text = '<p>typed later</p>';
|
|
113
|
+
await vi.advanceTimersByTimeAsync(DEBOUNCE_WAIT_MS + 10);
|
|
114
|
+
|
|
115
|
+
expect(wrapper.emitted('update:modelValue')).toHaveLength(2);
|
|
116
|
+
expect(emittedPayload(wrapper, 0).isHydrationWrite).toBe(true);
|
|
117
|
+
expect(emittedPayload(wrapper, 1).isHydrationWrite).toBeUndefined();
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -108,6 +108,13 @@ export default defineComponent({
|
|
|
108
108
|
isDirty: false,
|
|
109
109
|
emitChangeDebounce: undefined as DebounceHandle | undefined,
|
|
110
110
|
lengthError: false,
|
|
111
|
+
// Whether every write in the current debounce burst was an RTE
|
|
112
|
+
// hydration write-back (see UpdateItemValuePayload.isHydrationWrite).
|
|
113
|
+
// undefined = no burst pending; any user write downgrades it to false.
|
|
114
|
+
burstIsHydration: undefined as boolean | undefined,
|
|
115
|
+
// One-shot channel from onRteModelValue into the `text` setter for
|
|
116
|
+
// the write currently being applied.
|
|
117
|
+
pendingWriteIsHydration: false,
|
|
111
118
|
};
|
|
112
119
|
},
|
|
113
120
|
computed: {
|
|
@@ -144,6 +151,15 @@ export default defineComponent({
|
|
|
144
151
|
return unescapeContentText(this.value);
|
|
145
152
|
},
|
|
146
153
|
set(newVal: string | false): void {
|
|
154
|
+
// A burst is hydration only if EVERY write in it is hydration —
|
|
155
|
+
// one real keystroke downgrades the whole burst to a user edit
|
|
156
|
+
// (the canonicalisation then rides inside that legitimate entry).
|
|
157
|
+
const writeIsHydration = this.pendingWriteIsHydration;
|
|
158
|
+
|
|
159
|
+
this.pendingWriteIsHydration = false;
|
|
160
|
+
this.burstIsHydration =
|
|
161
|
+
this.burstIsHydration !== false && writeIsHydration;
|
|
162
|
+
|
|
147
163
|
if (newVal === false) {
|
|
148
164
|
this._value = false;
|
|
149
165
|
} else {
|
|
@@ -179,12 +195,16 @@ export default defineComponent({
|
|
|
179
195
|
this._value = false;
|
|
180
196
|
}
|
|
181
197
|
|
|
198
|
+
// Toggling is a user action — downgrade any pending hydration burst
|
|
199
|
+
// so this emit commits as a normal entry.
|
|
200
|
+
this.burstIsHydration = false;
|
|
182
201
|
this.emitChange();
|
|
183
202
|
},
|
|
184
203
|
|
|
185
204
|
showRichTextEditor(newVal: boolean) {
|
|
186
205
|
if (!newVal && this._value && typeof this._value === 'string') {
|
|
187
206
|
this._value = encodeContentText(this._value);
|
|
207
|
+
this.burstIsHydration = false;
|
|
188
208
|
this.emitChange();
|
|
189
209
|
}
|
|
190
210
|
},
|
|
@@ -208,11 +228,25 @@ export default defineComponent({
|
|
|
208
228
|
this.isDirty = false;
|
|
209
229
|
},
|
|
210
230
|
|
|
231
|
+
// RTE branch handler: receives the hydration meta the RichTextEditor
|
|
232
|
+
// attaches to its update:modelValue emits (a plain v-model binding
|
|
233
|
+
// would drop the second argument), records it for the `text` setter,
|
|
234
|
+
// then applies the write.
|
|
235
|
+
onRteModelValue(newVal: string, meta?: { isHydration?: boolean }): void {
|
|
236
|
+
this.pendingWriteIsHydration = meta?.isHydration === true;
|
|
237
|
+
this.text = newVal;
|
|
238
|
+
},
|
|
239
|
+
|
|
211
240
|
emitChange() {
|
|
241
|
+
const isHydrationWrite = this.burstIsHydration === true;
|
|
242
|
+
|
|
243
|
+
this.burstIsHydration = undefined;
|
|
244
|
+
|
|
212
245
|
this.$emit('update:modelValue', {
|
|
213
246
|
id: this.id,
|
|
214
247
|
name: this.name,
|
|
215
248
|
value: this._value,
|
|
249
|
+
...(isHydrationWrite ? { isHydrationWrite: true } : {}),
|
|
216
250
|
});
|
|
217
251
|
},
|
|
218
252
|
},
|
|
@@ -24,7 +24,6 @@ import { Button } from '@designcrowd/fe-shared-lib';
|
|
|
24
24
|
import { ref, computed, onMounted } from 'vue';
|
|
25
25
|
import { useUiPageSettingsStore } from '../../../page-settings/store';
|
|
26
26
|
import { useBrandPageMakerUserStore } from '../../../../brand-page-maker/stores';
|
|
27
|
-
import { useHistoryStore } from '../../../../brand-page-maker/stores/history/history-store';
|
|
28
27
|
import { pageSettingTr } from '../../../../composables/useTranslate';
|
|
29
28
|
import PageSelectorDropdown from '../../../page-navigation/PageSelectorDropdown.vue';
|
|
30
29
|
import { usePageNavigationStore } from '../../../page-navigation/store/page-navigation-store';
|
|
@@ -44,14 +43,18 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
44
43
|
const emit = defineEmits<{
|
|
45
44
|
(
|
|
46
45
|
e: 'update:modelValue',
|
|
47
|
-
payload: {
|
|
46
|
+
payload: {
|
|
47
|
+
id: string;
|
|
48
|
+
name: string;
|
|
49
|
+
value: string;
|
|
50
|
+
isHydrationWrite?: boolean;
|
|
51
|
+
},
|
|
48
52
|
): void;
|
|
49
53
|
}>();
|
|
50
54
|
|
|
51
55
|
const pageNavigationStore = usePageNavigationStore();
|
|
52
56
|
const pageSettingsStore = useUiPageSettingsStore();
|
|
53
57
|
const userStore = useBrandPageMakerUserStore();
|
|
54
|
-
const historyStore = useHistoryStore();
|
|
55
58
|
|
|
56
59
|
const selectedPage = ref('');
|
|
57
60
|
|
|
@@ -111,12 +114,14 @@ onMounted(() => {
|
|
|
111
114
|
if (routeByPageId) {
|
|
112
115
|
selectedPage.value = routeByPageId.displayName;
|
|
113
116
|
} else {
|
|
114
|
-
// Stale page-id clear at mount —
|
|
115
|
-
|
|
117
|
+
// Stale page-id clear at mount — a hydration write, not a user edit.
|
|
118
|
+
// The flag makes the content-settings store fold it into the current
|
|
119
|
+
// undo head at write time (see UpdateItemValuePayload.isHydrationWrite).
|
|
116
120
|
emit('update:modelValue', {
|
|
117
121
|
id: props.id,
|
|
118
122
|
name: props.name,
|
|
119
123
|
value: '',
|
|
124
|
+
isHydrationWrite: true,
|
|
120
125
|
});
|
|
121
126
|
}
|
|
122
127
|
}
|
package/src/shared/content-settings/components/payment-config-field/PaymentConfigField.mixin.ts
CHANGED
|
@@ -15,7 +15,6 @@ import { useShopConfigStore } from '@shared/ecommerce-settings/store/shop-settin
|
|
|
15
15
|
import { paymentConfigTr } from '@src/composables/useTranslate';
|
|
16
16
|
import brandPageClient from '../../../../clients/brand-page-client';
|
|
17
17
|
import { useUiPageSettingsStore } from '@shared/page-settings/store';
|
|
18
|
-
import { useHistoryStore } from '@brand-page-maker/stores/history/history-store';
|
|
19
18
|
|
|
20
19
|
export default defineComponent({
|
|
21
20
|
props: {
|
|
@@ -42,7 +41,6 @@ export default defineComponent({
|
|
|
42
41
|
const uiPageSettingsStore = useUiPageSettingsStore();
|
|
43
42
|
const eventManagerStore = useEventManagerStore();
|
|
44
43
|
const shopConfigStore = useShopConfigStore();
|
|
45
|
-
const historyStore = useHistoryStore();
|
|
46
44
|
|
|
47
45
|
return {
|
|
48
46
|
brandPageToken: computed(() => brandPageContainerStore.brandPageToken),
|
|
@@ -53,7 +51,6 @@ export default defineComponent({
|
|
|
53
51
|
eventManagerStore,
|
|
54
52
|
shopConfigStore,
|
|
55
53
|
uiPageSettingsStore,
|
|
56
|
-
historyStore,
|
|
57
54
|
};
|
|
58
55
|
},
|
|
59
56
|
emits: ['update:modelValue'],
|
|
@@ -113,16 +110,16 @@ export default defineComponent({
|
|
|
113
110
|
paymentConfig: this.shopConfigStore.paymentConfig,
|
|
114
111
|
};
|
|
115
112
|
|
|
113
|
+
// Lazy paymentConfig hydration — a hydration write, not a user edit.
|
|
114
|
+
// The flag makes the content-settings store fold it into the current
|
|
115
|
+
// undo head at write time (see UpdateItemValuePayload.isHydrationWrite).
|
|
116
116
|
const payload: UpdateItemValuePayload<PaymentConfig> = {
|
|
117
117
|
id: this.id,
|
|
118
118
|
name: this.name,
|
|
119
119
|
value,
|
|
120
|
+
isHydrationWrite: true,
|
|
120
121
|
};
|
|
121
122
|
|
|
122
|
-
// Lazy paymentConfig hydration — fold into current head instead
|
|
123
|
-
// of pushing a phantom entry.
|
|
124
|
-
this.historyStore.mergeNextMutation();
|
|
125
|
-
|
|
126
123
|
this.$emit('update:modelValue', payload);
|
|
127
124
|
}
|
|
128
125
|
},
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
v-if="showRichTextEditor"
|
|
5
5
|
:id="`${id}-${name}`"
|
|
6
6
|
ref="richTextEditorRef"
|
|
7
|
-
|
|
7
|
+
:model-value="text"
|
|
8
8
|
:attrs="attrs"
|
|
9
9
|
:label="label"
|
|
10
10
|
:placeholder="placeholder"
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
:content-type="contentType"
|
|
18
18
|
@focus="onFocus"
|
|
19
19
|
@blur="onBlur"
|
|
20
|
+
@update:model-value="onRteModelValue"
|
|
20
21
|
/>
|
|
21
22
|
<TextInput
|
|
22
23
|
v-else
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { defineComponent } from 'vue';
|
|
2
|
+
import { mount } from '@vue/test-utils';
|
|
3
|
+
import { createPinia, setActivePinia } from 'pinia';
|
|
4
|
+
import type { Content, LogoTheme, TextItem } from '@brand-page/models';
|
|
5
|
+
import TextItemsFieldMixin from './TextItemsField.mixin';
|
|
6
|
+
|
|
7
|
+
// BP-5810: TextItemsField re-wraps per-item TextareaField payloads into one
|
|
8
|
+
// textItems array payload behind its own 250ms debounce. The hydration flag
|
|
9
|
+
// must survive this second hop (AND rule: any user action in the burst
|
|
10
|
+
// downgrades it).
|
|
11
|
+
|
|
12
|
+
const DEBOUNCE_IN_MS = 250;
|
|
13
|
+
|
|
14
|
+
interface EmittedPayload {
|
|
15
|
+
id: string;
|
|
16
|
+
name: string;
|
|
17
|
+
value: TextItem[];
|
|
18
|
+
isHydrationWrite?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const TestHost = defineComponent({
|
|
22
|
+
mixins: [TextItemsFieldMixin],
|
|
23
|
+
template: '<div />',
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const makeTextItem = (text: string): TextItem =>
|
|
27
|
+
({
|
|
28
|
+
text,
|
|
29
|
+
textStyle: {
|
|
30
|
+
fontSize: '16px',
|
|
31
|
+
textAlign: 'left',
|
|
32
|
+
textStyleType: 'paragraphs',
|
|
33
|
+
applyAutoScale: true,
|
|
34
|
+
},
|
|
35
|
+
}) as TextItem;
|
|
36
|
+
|
|
37
|
+
const mountHost = () =>
|
|
38
|
+
mount(TestHost, {
|
|
39
|
+
props: {
|
|
40
|
+
id: 'item-1',
|
|
41
|
+
name: 'textItems',
|
|
42
|
+
content: { id: 'item-1', type: 'text' } as Content,
|
|
43
|
+
value: [makeTextItem('<p>one</p>'), makeTextItem('<p>two</p>')],
|
|
44
|
+
isAdmin: false,
|
|
45
|
+
logoTheme: {
|
|
46
|
+
logoTextColor: '#000000',
|
|
47
|
+
logoBackgroundColor: '#ffffff',
|
|
48
|
+
} as unknown as LogoTheme,
|
|
49
|
+
showRichTextEditor: true,
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const emittedPayload = (
|
|
54
|
+
wrapper: ReturnType<typeof mountHost>,
|
|
55
|
+
call: number,
|
|
56
|
+
): EmittedPayload =>
|
|
57
|
+
wrapper.emitted('update:model-value')![call][0] as EmittedPayload;
|
|
58
|
+
|
|
59
|
+
describe('TextItemsField hydration burst flag', () => {
|
|
60
|
+
beforeEach(() => {
|
|
61
|
+
setActivePinia(createPinia());
|
|
62
|
+
vi.useFakeTimers();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
afterEach(() => {
|
|
66
|
+
vi.useRealTimers();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('hydration-only item writes emit a flagged textItems payload', async () => {
|
|
70
|
+
const wrapper = mountHost();
|
|
71
|
+
|
|
72
|
+
wrapper.vm.onTextAreaTextChange(
|
|
73
|
+
{
|
|
74
|
+
id: 'item-1',
|
|
75
|
+
name: 'textItems',
|
|
76
|
+
value: '<p style="text-align: center;">one</p>',
|
|
77
|
+
isHydrationWrite: true,
|
|
78
|
+
},
|
|
79
|
+
0,
|
|
80
|
+
);
|
|
81
|
+
wrapper.vm.onTextAreaTextChange(
|
|
82
|
+
{
|
|
83
|
+
id: 'item-1',
|
|
84
|
+
name: 'textItems',
|
|
85
|
+
value: '<p style="text-align: center;">two</p>',
|
|
86
|
+
isHydrationWrite: true,
|
|
87
|
+
},
|
|
88
|
+
1,
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
await vi.advanceTimersByTimeAsync(DEBOUNCE_IN_MS + 10);
|
|
92
|
+
|
|
93
|
+
expect(wrapper.emitted('update:model-value')).toHaveLength(1);
|
|
94
|
+
expect(emittedPayload(wrapper, 0).isHydrationWrite).toBe(true);
|
|
95
|
+
expect(emittedPayload(wrapper, 0).value[0].text).toBe(
|
|
96
|
+
'<p style="text-align: center;">one</p>',
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('an unflagged item write keeps the payload unflagged', async () => {
|
|
101
|
+
const wrapper = mountHost();
|
|
102
|
+
|
|
103
|
+
wrapper.vm.onTextAreaTextChange(
|
|
104
|
+
{ id: 'item-1', name: 'textItems', value: '<p>typed</p>' },
|
|
105
|
+
0,
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
await vi.advanceTimersByTimeAsync(DEBOUNCE_IN_MS + 10);
|
|
109
|
+
|
|
110
|
+
expect(wrapper.emitted('update:model-value')).toHaveLength(1);
|
|
111
|
+
expect(emittedPayload(wrapper, 0).isHydrationWrite).toBeUndefined();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('a user action in the burst downgrades a pending hydration flag', async () => {
|
|
115
|
+
const wrapper = mountHost();
|
|
116
|
+
|
|
117
|
+
wrapper.vm.onTextAreaTextChange(
|
|
118
|
+
{
|
|
119
|
+
id: 'item-1',
|
|
120
|
+
name: 'textItems',
|
|
121
|
+
value: '<p>canonical</p>',
|
|
122
|
+
isHydrationWrite: true,
|
|
123
|
+
},
|
|
124
|
+
0,
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
// Style change is a user action and emits immediately.
|
|
128
|
+
wrapper.vm.setTextStyleOption(
|
|
129
|
+
{
|
|
130
|
+
id: 'item-1',
|
|
131
|
+
name: 'textItems',
|
|
132
|
+
value: { fontSize: '20px', textAlign: 'left' },
|
|
133
|
+
},
|
|
134
|
+
1,
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
expect(wrapper.emitted('update:model-value')).toHaveLength(1);
|
|
138
|
+
expect(emittedPayload(wrapper, 0).isHydrationWrite).toBeUndefined();
|
|
139
|
+
|
|
140
|
+
// The trailing debounced emit must not resurrect the flag either.
|
|
141
|
+
await vi.advanceTimersByTimeAsync(DEBOUNCE_IN_MS + 10);
|
|
142
|
+
|
|
143
|
+
const calls = wrapper.emitted('update:model-value')!;
|
|
144
|
+
|
|
145
|
+
for (let i = 0; i < calls.length; i++) {
|
|
146
|
+
expect(emittedPayload(wrapper, i).isHydrationWrite).toBeUndefined();
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
});
|
|
@@ -84,6 +84,11 @@ export default defineComponent({
|
|
|
84
84
|
textEmptyErrorMessage: TEXT_EMPTY(),
|
|
85
85
|
textMaxLengthErrorMessage: MAX_LENGTH_ERROR_MESSAGE(),
|
|
86
86
|
isInternalWatchEnabled: true,
|
|
87
|
+
// Second burst layer on top of GenericTextInput's (this field re-wraps
|
|
88
|
+
// per-item TextareaField payloads into one textItems array payload
|
|
89
|
+
// behind its own debounce). Hydration only if EVERY write in the burst
|
|
90
|
+
// was hydration; any user action downgrades it. undefined = no burst.
|
|
91
|
+
burstIsHydration: undefined as boolean | undefined,
|
|
87
92
|
}),
|
|
88
93
|
async created() {
|
|
89
94
|
this.options = cloneDeep(this.value);
|
|
@@ -97,6 +102,9 @@ export default defineComponent({
|
|
|
97
102
|
onDelete(index: number) {
|
|
98
103
|
this.options = this.options.filter((_o, i) => i !== index);
|
|
99
104
|
|
|
105
|
+
// Deleting is a user action — downgrade any pending hydration burst.
|
|
106
|
+
this.burstIsHydration = false;
|
|
107
|
+
|
|
100
108
|
if (this.changeDebounce) {
|
|
101
109
|
this.changeDebounce();
|
|
102
110
|
}
|
|
@@ -107,6 +115,8 @@ export default defineComponent({
|
|
|
107
115
|
this.addOption();
|
|
108
116
|
},
|
|
109
117
|
addOption(): void {
|
|
118
|
+
this.burstIsHydration = false;
|
|
119
|
+
|
|
110
120
|
this.options.push({
|
|
111
121
|
text: '',
|
|
112
122
|
textStyle: {
|
|
@@ -131,16 +141,24 @@ export default defineComponent({
|
|
|
131
141
|
return decodeHtmlEntities(text);
|
|
132
142
|
},
|
|
133
143
|
emitChange(): void {
|
|
144
|
+
const isHydrationWrite = this.burstIsHydration === true;
|
|
145
|
+
|
|
146
|
+
this.burstIsHydration = undefined;
|
|
147
|
+
|
|
134
148
|
const payload: UpdateItemValuePayload<TextItem[]> = {
|
|
135
149
|
id: this.id,
|
|
136
150
|
name: this.name,
|
|
137
151
|
value: this.options,
|
|
152
|
+
...(isHydrationWrite ? { isHydrationWrite: true } : {}),
|
|
138
153
|
};
|
|
139
154
|
|
|
140
155
|
this.$emit('update:model-value', payload);
|
|
141
156
|
},
|
|
142
157
|
|
|
143
158
|
onItemsChange(items: TextItem[]) {
|
|
159
|
+
// Reordering is a user action — downgrade any pending hydration burst.
|
|
160
|
+
this.burstIsHydration = false;
|
|
161
|
+
|
|
144
162
|
const payload: UpdateItemValuePayload<TextItem[]> = {
|
|
145
163
|
id: this.id,
|
|
146
164
|
name: this.name,
|
|
@@ -161,9 +179,15 @@ export default defineComponent({
|
|
|
161
179
|
id: string;
|
|
162
180
|
name: string;
|
|
163
181
|
value: string;
|
|
182
|
+
isHydrationWrite?: boolean;
|
|
164
183
|
},
|
|
165
184
|
index: number,
|
|
166
185
|
) {
|
|
186
|
+
// Carry the hydration flag from the per-item TextareaField burst into
|
|
187
|
+
// this field's own burst (AND rule — one user write downgrades it).
|
|
188
|
+
this.burstIsHydration =
|
|
189
|
+
this.burstIsHydration !== false && payload.isHydrationWrite === true;
|
|
190
|
+
|
|
167
191
|
this.options[index].text = payload.value;
|
|
168
192
|
this.isModified[index] = true;
|
|
169
193
|
},
|
|
@@ -177,6 +201,8 @@ export default defineComponent({
|
|
|
177
201
|
index: number,
|
|
178
202
|
) {
|
|
179
203
|
this.options[index].textStyle = payload.value;
|
|
204
|
+
// Style edits are user actions — downgrade any pending hydration burst.
|
|
205
|
+
this.burstIsHydration = false;
|
|
180
206
|
this.emitChange();
|
|
181
207
|
},
|
|
182
208
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
v-if="showRichTextEditor"
|
|
6
6
|
:id="`${id}-${name}`"
|
|
7
7
|
ref="richTextEditorRef"
|
|
8
|
-
|
|
8
|
+
:model-value="text"
|
|
9
9
|
class="tw-bg-white"
|
|
10
10
|
:attrs="attrs"
|
|
11
11
|
:label="label"
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
:content-type="contentType"
|
|
22
22
|
@focus="onFocus"
|
|
23
23
|
@blur="onBlur"
|
|
24
|
+
@update:model-value="onRteModelValue"
|
|
24
25
|
/>
|
|
25
26
|
<Textarea
|
|
26
27
|
v-else
|
|
@@ -123,6 +123,15 @@ export interface UpdateItemValuePayload<T> {
|
|
|
123
123
|
name: string;
|
|
124
124
|
value: T;
|
|
125
125
|
multipleValues?: boolean;
|
|
126
|
+
/**
|
|
127
|
+
* Marks this write as a hydration write-back (e.g. the RTE rewriting the
|
|
128
|
+
* model into canonical HTML on mount) rather than a user edit. The
|
|
129
|
+
* content-settings store arms the undo history's merge flag synchronously
|
|
130
|
+
* before applying a flagged write, so it folds into the current history
|
|
131
|
+
* head instead of pushing a phantom entry (BP-5810). The flag travels
|
|
132
|
+
* with the value so it survives the debounced field → store hops.
|
|
133
|
+
*/
|
|
134
|
+
isHydrationWrite?: boolean;
|
|
126
135
|
}
|
|
127
136
|
|
|
128
137
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
@@ -131,6 +140,8 @@ export interface UpdateMultipleItemValuesPayload<T extends object> {
|
|
|
131
140
|
name: string;
|
|
132
141
|
value: T;
|
|
133
142
|
index?: number;
|
|
143
|
+
/** See UpdateItemValuePayload.isHydrationWrite. */
|
|
144
|
+
isHydrationWrite?: boolean;
|
|
134
145
|
}
|
|
135
146
|
|
|
136
147
|
export interface ValidateItemPayload {
|
|
@@ -5,6 +5,16 @@ import {
|
|
|
5
5
|
useUiContentSettingsStore,
|
|
6
6
|
} from './index';
|
|
7
7
|
|
|
8
|
+
const { mergeNextMutationMock } = vi.hoisted(() => ({
|
|
9
|
+
mergeNextMutationMock: vi.fn(),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
vi.mock('@brand-page-maker/stores/history/history-store', () => ({
|
|
13
|
+
useHistoryStore: vi.fn(() => ({
|
|
14
|
+
mergeNextMutation: mergeNextMutationMock,
|
|
15
|
+
})),
|
|
16
|
+
}));
|
|
17
|
+
|
|
8
18
|
describe('content settings store', () => {
|
|
9
19
|
beforeAll(() => {
|
|
10
20
|
const pinia = createPinia();
|
|
@@ -69,3 +79,103 @@ describe('content settings store', () => {
|
|
|
69
79
|
expect(store.modeStack[0].mode).toBe(DEFAULT_CONSTANT_SETTINGS_MODE);
|
|
70
80
|
});
|
|
71
81
|
});
|
|
82
|
+
|
|
83
|
+
// BP-5810: hydration write-backs (RTE canonical HTML on mount, lazy config
|
|
84
|
+
// hydration, stale-id clears) arrive with `isHydrationWrite` on the payload.
|
|
85
|
+
// The store must arm the undo history's merge flag in the same tick as the
|
|
86
|
+
// write so it folds into the current head instead of pushing a phantom entry.
|
|
87
|
+
describe('content settings store — hydration writes arm the history merge', () => {
|
|
88
|
+
const makeItem = () => ({
|
|
89
|
+
id: 'item-1',
|
|
90
|
+
type: 'social-links',
|
|
91
|
+
settings: {
|
|
92
|
+
content: { title: '<p>old</p>' },
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const setupStore = () => {
|
|
97
|
+
setActivePinia(createPinia());
|
|
98
|
+
|
|
99
|
+
const store = useUiContentSettingsStore();
|
|
100
|
+
|
|
101
|
+
store.items.push(makeItem() as never);
|
|
102
|
+
|
|
103
|
+
// Isolate updateItemValue: validation and the content-changed fan-out
|
|
104
|
+
// reach into maker stores that are out of scope here.
|
|
105
|
+
store.validateItem = vi.fn();
|
|
106
|
+
store.onContentChanged = vi.fn();
|
|
107
|
+
|
|
108
|
+
return store;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
beforeEach(() => {
|
|
112
|
+
mergeNextMutationMock.mockClear();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('updateItemValue with isHydrationWrite arms the merge and applies the write', () => {
|
|
116
|
+
const store = setupStore();
|
|
117
|
+
|
|
118
|
+
store.updateItemValue({
|
|
119
|
+
id: 'item-1',
|
|
120
|
+
name: 'title',
|
|
121
|
+
value: '<p style="text-align: center;"></p>',
|
|
122
|
+
isHydrationWrite: true,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
expect(mergeNextMutationMock).toHaveBeenCalledTimes(1);
|
|
126
|
+
expect((store.items[0].settings.content as { title?: string }).title).toBe(
|
|
127
|
+
'<p style="text-align: center;"></p>',
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('updateItemValue without the flag does not touch the history merge', () => {
|
|
132
|
+
const store = setupStore();
|
|
133
|
+
|
|
134
|
+
store.updateItemValue({
|
|
135
|
+
id: 'item-1',
|
|
136
|
+
name: 'title',
|
|
137
|
+
value: '<p>user edit</p>',
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
expect(mergeNextMutationMock).not.toHaveBeenCalled();
|
|
141
|
+
expect((store.items[0].settings.content as { title?: string }).title).toBe(
|
|
142
|
+
'<p>user edit</p>',
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('updateItemValue for an unknown item does not arm the merge', () => {
|
|
147
|
+
const store = setupStore();
|
|
148
|
+
|
|
149
|
+
store.updateItemValue({
|
|
150
|
+
id: 'missing-item',
|
|
151
|
+
name: 'title',
|
|
152
|
+
value: '<p>x</p>',
|
|
153
|
+
isHydrationWrite: true,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
expect(mergeNextMutationMock).not.toHaveBeenCalled();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('updateMultipleItemValues honours the same contract', () => {
|
|
160
|
+
const store = setupStore();
|
|
161
|
+
|
|
162
|
+
store.updateMultipleItemValues({
|
|
163
|
+
id: 'item-1',
|
|
164
|
+
name: 'title',
|
|
165
|
+
value: { title: '<p>merged</p>' },
|
|
166
|
+
isHydrationWrite: true,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
expect(mergeNextMutationMock).toHaveBeenCalledTimes(1);
|
|
170
|
+
|
|
171
|
+
mergeNextMutationMock.mockClear();
|
|
172
|
+
|
|
173
|
+
store.updateMultipleItemValues({
|
|
174
|
+
id: 'item-1',
|
|
175
|
+
name: 'title',
|
|
176
|
+
value: { title: '<p>user</p>' },
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
expect(mergeNextMutationMock).not.toHaveBeenCalled();
|
|
180
|
+
});
|
|
181
|
+
});
|