@designcrowd/library.brand-page 6.0.6-undo-2 → 6.0.6-undo-4
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-BqD7lX6T.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-en-us-rPQrOY1L.js → CreatedOnBrandCrowd-en-us-BXFZ-Vv_.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-es-es-awd8-QFa.js → CreatedOnBrandCrowd-es-es-qTAVOIsd.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-fr-CwJ4J7Ow.js → CreatedOnBrandCrowd-fr-ca-LVZ7sAKH.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-ca-CwJ4J7Ow.js → CreatedOnBrandCrowd-fr-fr-LVZ7sAKH.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-pt-BXYfTLlD.js → CreatedOnBrandCrowd-pt-br-TSeJN2wb.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-br-BXYfTLlD.js → CreatedOnBrandCrowd-pt-pt-TSeJN2wb.js} +1 -1
- package/lib/src/{CreatedOnDcom-de-de-kX6u1lNK.js → CreatedOnDcom-de-de-BLLcDw_4.js} +1 -1
- package/lib/src/{CreatedOnDcom-en-us-Bkq5sgFs.js → CreatedOnDcom-en-us-BVdWxcIs.js} +1 -1
- package/lib/src/{CreatedOnDcom-es-es-zCAC9ujj.js → CreatedOnDcom-es-es-BZRzR4GZ.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-ca-BYz6Wzg5.js → CreatedOnDcom-fr-ca-B97f7ekB.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-fr-CN-wAaSH.js → CreatedOnDcom-fr-fr-CNWS12Rb.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-br-BrK4Ngg-.js → CreatedOnDcom-pt-br-CsASsHeV.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-pt-AfI_g7xJ.js → CreatedOnDcom-pt-pt-CsYqjPls.js} +1 -1
- package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.d.ts +5 -5
- package/lib/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.d.ts +6 -6
- 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 +23 -13
- 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-BNqCRgBN.js} +15895 -15862
- 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 +6 -6
- package/lib/src/shared/content-settings/components/content-settings/ReplaceImageSettingsForContent.mixin.d.ts +5 -5
- package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItem.mixin.d.ts +5 -5
- package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.d.ts +5 -5
- 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 +5 -5
- 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 +5 -5
- package/lib/src/shared/content-settings/store/content-settings-store.js +39 -5
- package/lib/src/shared/page-settings/components/page-settings/PageSettings.mixin.d.ts +5 -5
- package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.d.ts +5 -5
- package/package.json +1 -1
- package/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.ts +24 -15
- 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 +137 -0
- package/src/shared/content-settings/store/content-settings-store.ts +45 -0
|
@@ -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,130 @@ 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>', titleStyle: { fontSize: '2em' } },
|
|
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
|
+
// The RTE's mount-time style sync (setInitialStyleSettings) writes through
|
|
160
|
+
// updateTextStyles ~100ms BEFORE the canonical text write-back. If it isn't
|
|
161
|
+
// armed, it opens a plain capture burst that swallows the flagged text
|
|
162
|
+
// write — the phantom's second act (BP-5810).
|
|
163
|
+
test('updateTextStyles honours the hydration-write contract', () => {
|
|
164
|
+
const store = setupStore();
|
|
165
|
+
|
|
166
|
+
store.updateTextStyles({
|
|
167
|
+
id: 'item-1',
|
|
168
|
+
name: 'title',
|
|
169
|
+
value: { fontSize: '2.25em' },
|
|
170
|
+
isHydrationWrite: true,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
expect(mergeNextMutationMock).toHaveBeenCalledTimes(1);
|
|
174
|
+
|
|
175
|
+
mergeNextMutationMock.mockClear();
|
|
176
|
+
|
|
177
|
+
store.updateTextStyles({
|
|
178
|
+
id: 'item-1',
|
|
179
|
+
name: 'title',
|
|
180
|
+
value: { fontSize: '3em' },
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
expect(mergeNextMutationMock).not.toHaveBeenCalled();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('updateMultipleItemValues honours the same contract', () => {
|
|
187
|
+
const store = setupStore();
|
|
188
|
+
|
|
189
|
+
store.updateMultipleItemValues({
|
|
190
|
+
id: 'item-1',
|
|
191
|
+
name: 'title',
|
|
192
|
+
value: { title: '<p>merged</p>' },
|
|
193
|
+
isHydrationWrite: true,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
expect(mergeNextMutationMock).toHaveBeenCalledTimes(1);
|
|
197
|
+
|
|
198
|
+
mergeNextMutationMock.mockClear();
|
|
199
|
+
|
|
200
|
+
store.updateMultipleItemValues({
|
|
201
|
+
id: 'item-1',
|
|
202
|
+
name: 'title',
|
|
203
|
+
value: { title: '<p>user</p>' },
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
expect(mergeNextMutationMock).not.toHaveBeenCalled();
|
|
207
|
+
});
|
|
208
|
+
});
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
useBrandPageMakerStore,
|
|
37
37
|
useVideoStore,
|
|
38
38
|
} from '@brand-page-maker/stores';
|
|
39
|
+
import { useHistoryStore } from '@brand-page-maker/stores/history/history-store';
|
|
39
40
|
import { useUiSideBarStore } from '../../side-bar/store';
|
|
40
41
|
import {
|
|
41
42
|
createDefaultContentItemGroupState,
|
|
@@ -389,6 +390,16 @@ export const useUiContentSettingsStore = defineStore('uiContentSettings', {
|
|
|
389
390
|
return;
|
|
390
391
|
}
|
|
391
392
|
|
|
393
|
+
// Hydration write-backs (RTE canonical HTML on mount, lazy config
|
|
394
|
+
// hydration, stale-id clears) fold into the current undo head instead
|
|
395
|
+
// of pushing an entry. Arming here — same tick as the write — is what
|
|
396
|
+
// makes the merge reliable: the flag travelled with the payload
|
|
397
|
+
// through the debounced field chain, so emit-time arming would lapse
|
|
398
|
+
// before the write lands (BP-5810).
|
|
399
|
+
if (payload.isHydrationWrite) {
|
|
400
|
+
useHistoryStore().mergeNextMutation();
|
|
401
|
+
}
|
|
402
|
+
|
|
392
403
|
item.settings.content[payload.name] = payload.value;
|
|
393
404
|
|
|
394
405
|
this.validateItem({
|
|
@@ -402,6 +413,7 @@ export const useUiContentSettingsStore = defineStore('uiContentSettings', {
|
|
|
402
413
|
id,
|
|
403
414
|
name,
|
|
404
415
|
value,
|
|
416
|
+
isHydrationWrite,
|
|
405
417
|
}: UpdateMultipleItemValuesPayload<any>): void {
|
|
406
418
|
const item = this.items.find((i) => i.id === id);
|
|
407
419
|
|
|
@@ -409,6 +421,11 @@ export const useUiContentSettingsStore = defineStore('uiContentSettings', {
|
|
|
409
421
|
return;
|
|
410
422
|
}
|
|
411
423
|
|
|
424
|
+
// Same hydration-write contract as updateItemValue.
|
|
425
|
+
if (isHydrationWrite) {
|
|
426
|
+
useHistoryStore().mergeNextMutation();
|
|
427
|
+
}
|
|
428
|
+
|
|
412
429
|
item.settings.content = vueDeepMerge(item.settings.content, value);
|
|
413
430
|
this.validateItem({ id, name });
|
|
414
431
|
this.onContentChanged();
|
|
@@ -417,6 +434,7 @@ export const useUiContentSettingsStore = defineStore('uiContentSettings', {
|
|
|
417
434
|
id,
|
|
418
435
|
name,
|
|
419
436
|
value,
|
|
437
|
+
isHydrationWrite,
|
|
420
438
|
}: UpdateMultipleItemValuesPayload<any>): void {
|
|
421
439
|
const item = this.items.find((i) => i.id === id);
|
|
422
440
|
|
|
@@ -424,6 +442,11 @@ export const useUiContentSettingsStore = defineStore('uiContentSettings', {
|
|
|
424
442
|
return;
|
|
425
443
|
}
|
|
426
444
|
|
|
445
|
+
// Same hydration-write contract as updateItemValue.
|
|
446
|
+
if (isHydrationWrite) {
|
|
447
|
+
useHistoryStore().mergeNextMutation();
|
|
448
|
+
}
|
|
449
|
+
|
|
427
450
|
item.settings.content[`${name}Style`].textStyleType = vueDeepMerge(
|
|
428
451
|
item.settings.content,
|
|
429
452
|
value,
|
|
@@ -435,6 +458,7 @@ export const useUiContentSettingsStore = defineStore('uiContentSettings', {
|
|
|
435
458
|
id,
|
|
436
459
|
name,
|
|
437
460
|
value,
|
|
461
|
+
isHydrationWrite,
|
|
438
462
|
}: UpdateMultipleItemValuesPayload<any>): void {
|
|
439
463
|
const item = this.items.find((i) => i.id === id);
|
|
440
464
|
|
|
@@ -442,6 +466,15 @@ export const useUiContentSettingsStore = defineStore('uiContentSettings', {
|
|
|
442
466
|
return;
|
|
443
467
|
}
|
|
444
468
|
|
|
469
|
+
// Same hydration-write contract as updateItemValue. The RTE's
|
|
470
|
+
// mount-time style sync (setInitialStyleSettings) lands here ~100ms
|
|
471
|
+
// BEFORE its canonical text write-back — if this write opens a plain
|
|
472
|
+
// capture burst, the flagged text write joins it and both commit as a
|
|
473
|
+
// phantom entry (BP-5810's second act).
|
|
474
|
+
if (isHydrationWrite) {
|
|
475
|
+
useHistoryStore().mergeNextMutation();
|
|
476
|
+
}
|
|
477
|
+
|
|
445
478
|
item.settings.content[`${name}Style`] = vueDeepMerge(
|
|
446
479
|
item.settings.content[`${name}Style`],
|
|
447
480
|
value,
|
|
@@ -463,6 +496,7 @@ export const useUiContentSettingsStore = defineStore('uiContentSettings', {
|
|
|
463
496
|
name,
|
|
464
497
|
value,
|
|
465
498
|
index,
|
|
499
|
+
isHydrationWrite,
|
|
466
500
|
}: UpdateMultipleItemValuesPayload<any>): void {
|
|
467
501
|
const item = this.items.find((i) => i.id === id);
|
|
468
502
|
|
|
@@ -470,6 +504,11 @@ export const useUiContentSettingsStore = defineStore('uiContentSettings', {
|
|
|
470
504
|
return;
|
|
471
505
|
}
|
|
472
506
|
|
|
507
|
+
// Same hydration-write contract as updateItemValue.
|
|
508
|
+
if (isHydrationWrite) {
|
|
509
|
+
useHistoryStore().mergeNextMutation();
|
|
510
|
+
}
|
|
511
|
+
|
|
473
512
|
item.settings.content.textItems[index!].textStyle.textStyleType =
|
|
474
513
|
vueDeepMerge(
|
|
475
514
|
item.settings.content.textItems[index!].textStyle.textStyleType,
|
|
@@ -483,6 +522,7 @@ export const useUiContentSettingsStore = defineStore('uiContentSettings', {
|
|
|
483
522
|
name,
|
|
484
523
|
value,
|
|
485
524
|
index,
|
|
525
|
+
isHydrationWrite,
|
|
486
526
|
}: UpdateMultipleItemValuesPayload<any>): void {
|
|
487
527
|
const item = this.items.find((i) => i.id === id);
|
|
488
528
|
|
|
@@ -490,6 +530,11 @@ export const useUiContentSettingsStore = defineStore('uiContentSettings', {
|
|
|
490
530
|
return;
|
|
491
531
|
}
|
|
492
532
|
|
|
533
|
+
// Same hydration-write contract as updateItemValue.
|
|
534
|
+
if (isHydrationWrite) {
|
|
535
|
+
useHistoryStore().mergeNextMutation();
|
|
536
|
+
}
|
|
537
|
+
|
|
493
538
|
item.settings.content.textItems[index!].textStyle = vueDeepMerge(
|
|
494
539
|
item.settings.content.textItems[index!].textStyle,
|
|
495
540
|
value,
|