@designcrowd/library.brand-page 6.0.6-undo-9 → 6.0.6-undo-11
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-leTOAQPM.js → CreatedOnBrandCrowd-de-de-DGLT4mjc.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-en-us-_9b-3HPl.js → CreatedOnBrandCrowd-en-us-dyPp4NQB.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-es-es-BPeAHmQH.js → CreatedOnBrandCrowd-es-es-pxoT8xpT.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-fr-B7hbXmje.js → CreatedOnBrandCrowd-fr-ca-Bpvv3YSe.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-ca-B7hbXmje.js → CreatedOnBrandCrowd-fr-fr-Bpvv3YSe.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-pt-C0U-9HO7.js → CreatedOnBrandCrowd-pt-br-DqKwinmI.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-br-C0U-9HO7.js → CreatedOnBrandCrowd-pt-pt-DqKwinmI.js} +1 -1
- package/lib/src/{CreatedOnDcom-de-de-Bdn37x7V.js → CreatedOnDcom-de-de-DhhAudIO.js} +1 -1
- package/lib/src/{CreatedOnDcom-en-us-usSC3HOQ.js → CreatedOnDcom-en-us-DZhMl0Ro.js} +1 -1
- package/lib/src/{CreatedOnDcom-es-es-BE0kziRp.js → CreatedOnDcom-es-es-brPE0-Lo.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-ca-CcDaV0KR.js → CreatedOnDcom-fr-ca-CzKZrAVq.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-fr-C1bX4cuW.js → CreatedOnDcom-fr-fr-JLjx-E9X.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-br-DYju5xhk.js → CreatedOnDcom-pt-br-B_4Ot1uS.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-pt-CKYX6Rd0.js → CreatedOnDcom-pt-pt-By5SiPq_.js} +1 -1
- package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.js +1 -1
- package/lib/src/css/library.brand-page-brandCrowd.min.css +8 -2
- package/lib/src/css/library.brand-page-brandPage.min.css +8 -2
- package/lib/src/css/library.brand-page-designCom.min.css +8 -2
- package/lib/src/css/library.brand-page-preview.min.css +8 -2
- package/lib/src/index.mjs +1 -1
- package/lib/src/{lib-FHU3Jvoq.js → lib-C0lwMScz.js} +16056 -16044
- package/lib/src/shared/content-settings/components/GenericTextInput.mixin.d.ts +1 -1
- package/lib/src/shared/content-settings/components/GenericTextInput.mixin.js +1 -1
- package/lib/src/shared/content-settings/components/email-field/EmailField.mixin.d.ts +1 -1
- package/lib/src/shared/content-settings/components/tel-field/TelField.mixin.d.ts +1 -1
- package/lib/src/shared/content-settings/components/text-field/TextField.mixin.d.ts +1 -1
- package/lib/src/shared/content-settings/components/textarea-field/TextareaField.mixin.d.ts +1 -1
- package/lib/src/shared/content-settings/components/url-field/UrlField.mixin.d.ts +1 -1
- package/lib/src/shared/content-settings/components/youtube-video-field/YoutubeVideoField.mixin.d.ts +1 -1
- package/lib/src/shared/content-settings/store/content-settings-store.js +27 -33
- package/package.json +1 -1
- package/src/brand-page-maker/components/quick-link-bar/RedoButton.vue +15 -1
- package/src/brand-page-maker/components/quick-link-bar/UndoButton.vue +15 -1
- package/src/brand-page-maker/components/quick-link-bar/undo-redo-buttons.test.ts +7 -0
- package/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.ts +1 -1
- package/src/shared/content-settings/components/GenericTextInput.mixin.test.ts +9 -9
- package/src/shared/content-settings/components/GenericTextInput.mixin.ts +5 -2
- package/src/shared/content-settings/store/content-settings-store.ts +28 -33
|
@@ -99,7 +99,7 @@ declare const _default: import("vue").DefineComponent<import("vue").ExtractPropT
|
|
|
99
99
|
onFocus(): void;
|
|
100
100
|
onBlur(): void;
|
|
101
101
|
onRteModelValue(newVal: string, meta?: {
|
|
102
|
-
|
|
102
|
+
isHydrationWrite?: boolean;
|
|
103
103
|
}): void;
|
|
104
104
|
emitChange(): void;
|
|
105
105
|
}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, ("update:modelValue" | "on-toggle-state-change")[], "update:modelValue" | "on-toggle-state-change", import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
@@ -209,7 +209,7 @@ export default defineComponent({
|
|
|
209
209
|
// would drop the second argument), records it for the `text` setter,
|
|
210
210
|
// then applies the write.
|
|
211
211
|
onRteModelValue(newVal, meta) {
|
|
212
|
-
this.pendingWriteIsHydration = meta?.
|
|
212
|
+
this.pendingWriteIsHydration = meta?.isHydrationWrite === true;
|
|
213
213
|
this.text = newVal;
|
|
214
214
|
},
|
|
215
215
|
emitChange() {
|
|
@@ -96,7 +96,7 @@ declare const _default: import("vue").DefineComponent<{}, {}, {}, {}, {}, import
|
|
|
96
96
|
onFocus(): void;
|
|
97
97
|
onBlur(): void;
|
|
98
98
|
onRteModelValue(newVal: string, meta?: {
|
|
99
|
-
|
|
99
|
+
isHydrationWrite?: boolean;
|
|
100
100
|
}): void;
|
|
101
101
|
emitChange(): void;
|
|
102
102
|
}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, ("update:modelValue" | "on-toggle-state-change")[], "update:modelValue" | "on-toggle-state-change", import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
@@ -96,7 +96,7 @@ declare const _default: import("vue").DefineComponent<{}, {}, {}, {}, {}, import
|
|
|
96
96
|
onFocus(): void;
|
|
97
97
|
onBlur(): void;
|
|
98
98
|
onRteModelValue(newVal: string, meta?: {
|
|
99
|
-
|
|
99
|
+
isHydrationWrite?: boolean;
|
|
100
100
|
}): void;
|
|
101
101
|
emitChange(): void;
|
|
102
102
|
}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, ("update:modelValue" | "on-toggle-state-change")[], "update:modelValue" | "on-toggle-state-change", import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
@@ -96,7 +96,7 @@ declare const _default: import("vue").DefineComponent<{}, {}, {}, {}, {}, import
|
|
|
96
96
|
onFocus(): void;
|
|
97
97
|
onBlur(): void;
|
|
98
98
|
onRteModelValue(newVal: string, meta?: {
|
|
99
|
-
|
|
99
|
+
isHydrationWrite?: boolean;
|
|
100
100
|
}): void;
|
|
101
101
|
emitChange(): void;
|
|
102
102
|
}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, ("update:modelValue" | "on-toggle-state-change")[], "update:modelValue" | "on-toggle-state-change", import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
@@ -110,7 +110,7 @@ declare const _default: import("vue").DefineComponent<import("vue").ExtractPropT
|
|
|
110
110
|
onFocus(): void;
|
|
111
111
|
onBlur(): void;
|
|
112
112
|
onRteModelValue(newVal: string, meta?: {
|
|
113
|
-
|
|
113
|
+
isHydrationWrite?: boolean;
|
|
114
114
|
}): void;
|
|
115
115
|
emitChange(): void;
|
|
116
116
|
}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, ("update:modelValue" | "on-toggle-state-change")[], "update:modelValue" | "on-toggle-state-change", import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
@@ -103,7 +103,7 @@ declare const _default: import("vue").DefineComponent<{}, {}, Record<string, any
|
|
|
103
103
|
onFocus(): void;
|
|
104
104
|
onBlur(): void;
|
|
105
105
|
onRteModelValue(newVal: string, meta?: {
|
|
106
|
-
|
|
106
|
+
isHydrationWrite?: boolean;
|
|
107
107
|
}): void;
|
|
108
108
|
emitChange(): void;
|
|
109
109
|
}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, ("update:modelValue" | "on-toggle-state-change")[], "update:modelValue" | "on-toggle-state-change", import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
package/lib/src/shared/content-settings/components/youtube-video-field/YoutubeVideoField.mixin.d.ts
CHANGED
|
@@ -99,7 +99,7 @@ declare const _default: import("vue").DefineComponent<{}, {}, Record<string, any
|
|
|
99
99
|
onFocus(): void;
|
|
100
100
|
onBlur(): void;
|
|
101
101
|
onRteModelValue(newVal: string, meta?: {
|
|
102
|
-
|
|
102
|
+
isHydrationWrite?: boolean;
|
|
103
103
|
}): void;
|
|
104
104
|
emitChange(): void;
|
|
105
105
|
}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, ("update:modelValue" | "on-toggle-state-change")[], "update:modelValue" | "on-toggle-state-change", import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
@@ -22,6 +22,23 @@ const DEFAULT_TEXT_COLOR_DEPENDENT_TYPES = [
|
|
|
22
22
|
// result[i].x = v`), which would leave the payload byte-identical to
|
|
23
23
|
// the store while state actually changed.
|
|
24
24
|
const isSameValue = (a, b) => a === b;
|
|
25
|
+
/**
|
|
26
|
+
* Arms the undo history to fold the next mutation into the current head
|
|
27
|
+
* instead of pushing an entry — the hydration-write contract (BP-5810).
|
|
28
|
+
* Framework-originated writes (RTE canonical HTML on mount, style
|
|
29
|
+
* re-derivation, lazy config hydration) reach this store through debounced
|
|
30
|
+
* field chains, so the flag travels WITH the payload and the arm happens
|
|
31
|
+
* here, in the same tick as the write — arming at emit time would lapse
|
|
32
|
+
* before the write lands. Call only AFTER an action's drop guards (missing
|
|
33
|
+
* item, value-identical): a dropped write must never arm. See
|
|
34
|
+
* UpdateItemValuePayload.isHydrationWrite (models.ts) for the payload
|
|
35
|
+
* contract.
|
|
36
|
+
*/
|
|
37
|
+
const armHistoryMergeForHydrationWrite = (isHydrationWrite) => {
|
|
38
|
+
if (!isHydrationWrite)
|
|
39
|
+
return;
|
|
40
|
+
useHistoryStore().mergeNextMutation();
|
|
41
|
+
};
|
|
25
42
|
export const CONTENT_SETTINGS_MODES = {
|
|
26
43
|
edit: 'edit',
|
|
27
44
|
add: 'add',
|
|
@@ -222,15 +239,7 @@ export const useUiContentSettingsStore = defineStore('uiContentSettings', {
|
|
|
222
239
|
if (isSameValue(item.settings.content[payload.name], payload.value)) {
|
|
223
240
|
return; // no-op write: don't validate, don't flip the modified flag
|
|
224
241
|
}
|
|
225
|
-
|
|
226
|
-
// hydration, stale-id clears) fold into the current undo head instead
|
|
227
|
-
// of pushing an entry. Arming here — same tick as the write — is what
|
|
228
|
-
// makes the merge reliable: the flag travelled with the payload
|
|
229
|
-
// through the debounced field chain, so emit-time arming would lapse
|
|
230
|
-
// before the write lands (BP-5810).
|
|
231
|
-
if (payload.isHydrationWrite) {
|
|
232
|
-
useHistoryStore().mergeNextMutation();
|
|
233
|
-
}
|
|
242
|
+
armHistoryMergeForHydrationWrite(payload.isHydrationWrite);
|
|
234
243
|
item.settings.content[payload.name] = payload.value;
|
|
235
244
|
this.validateItem({
|
|
236
245
|
id: payload.id,
|
|
@@ -246,10 +255,7 @@ export const useUiContentSettingsStore = defineStore('uiContentSettings', {
|
|
|
246
255
|
if (Object.keys(value ?? {}).every((k) => isSameValue(item.settings.content[k], value[k]))) {
|
|
247
256
|
return; // every field a no-op — see isSameValue
|
|
248
257
|
}
|
|
249
|
-
|
|
250
|
-
if (isHydrationWrite) {
|
|
251
|
-
useHistoryStore().mergeNextMutation();
|
|
252
|
-
}
|
|
258
|
+
armHistoryMergeForHydrationWrite(isHydrationWrite);
|
|
253
259
|
item.settings.content = vueDeepMerge(item.settings.content, value);
|
|
254
260
|
this.validateItem({ id, name });
|
|
255
261
|
this.onContentChanged();
|
|
@@ -259,10 +265,7 @@ export const useUiContentSettingsStore = defineStore('uiContentSettings', {
|
|
|
259
265
|
if (!item) {
|
|
260
266
|
return;
|
|
261
267
|
}
|
|
262
|
-
|
|
263
|
-
if (isHydrationWrite) {
|
|
264
|
-
useHistoryStore().mergeNextMutation();
|
|
265
|
-
}
|
|
268
|
+
armHistoryMergeForHydrationWrite(isHydrationWrite);
|
|
266
269
|
item.settings.content[`${name}Style`].textStyleType = vueDeepMerge(item.settings.content, value);
|
|
267
270
|
this.validateItem({ id, name });
|
|
268
271
|
this.onContentChanged();
|
|
@@ -272,14 +275,11 @@ export const useUiContentSettingsStore = defineStore('uiContentSettings', {
|
|
|
272
275
|
if (!item) {
|
|
273
276
|
return;
|
|
274
277
|
}
|
|
275
|
-
//
|
|
276
|
-
//
|
|
277
|
-
//
|
|
278
|
-
//
|
|
279
|
-
|
|
280
|
-
if (isHydrationWrite) {
|
|
281
|
-
useHistoryStore().mergeNextMutation();
|
|
282
|
-
}
|
|
278
|
+
// The RTE's mount-time style sync (setInitialStyleSettings) lands here
|
|
279
|
+
// ~100ms BEFORE its canonical text write-back — unflagged, it would
|
|
280
|
+
// open a plain capture burst that swallows the flagged text write
|
|
281
|
+
// (BP-5810's second act).
|
|
282
|
+
armHistoryMergeForHydrationWrite(isHydrationWrite);
|
|
283
283
|
item.settings.content[`${name}Style`] = vueDeepMerge(item.settings.content[`${name}Style`], value);
|
|
284
284
|
this.validateItem({ id, name });
|
|
285
285
|
this.onContentChanged();
|
|
@@ -296,10 +296,7 @@ export const useUiContentSettingsStore = defineStore('uiContentSettings', {
|
|
|
296
296
|
if (!item) {
|
|
297
297
|
return;
|
|
298
298
|
}
|
|
299
|
-
|
|
300
|
-
if (isHydrationWrite) {
|
|
301
|
-
useHistoryStore().mergeNextMutation();
|
|
302
|
-
}
|
|
299
|
+
armHistoryMergeForHydrationWrite(isHydrationWrite);
|
|
303
300
|
item.settings.content.textItems[index].textStyle.textStyleType =
|
|
304
301
|
vueDeepMerge(item.settings.content.textItems[index].textStyle.textStyleType, value);
|
|
305
302
|
this.validateItem({ id, name });
|
|
@@ -310,10 +307,7 @@ export const useUiContentSettingsStore = defineStore('uiContentSettings', {
|
|
|
310
307
|
if (!item) {
|
|
311
308
|
return;
|
|
312
309
|
}
|
|
313
|
-
|
|
314
|
-
if (isHydrationWrite) {
|
|
315
|
-
useHistoryStore().mergeNextMutation();
|
|
316
|
-
}
|
|
310
|
+
armHistoryMergeForHydrationWrite(isHydrationWrite);
|
|
317
311
|
item.settings.content.textItems[index].textStyle = vueDeepMerge(item.settings.content.textItems[index].textStyle, value);
|
|
318
312
|
this.validateItem({ id, name });
|
|
319
313
|
this.onContentChanged();
|
package/package.json
CHANGED
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
direction="top"
|
|
4
4
|
alignment="center"
|
|
5
5
|
:should-show-icon="false"
|
|
6
|
-
class="
|
|
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="
|
|
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', () => {
|
|
@@ -523,7 +523,7 @@ export const useRichTextEditor = (
|
|
|
523
523
|
// focuses it before formatting, so this doesn't occur — revisit if a
|
|
524
524
|
// consumer ever drives toolbar actions on an unfocused editor.
|
|
525
525
|
emit('update:modelValue', sanitizedData.sanitizedHtml, {
|
|
526
|
-
|
|
526
|
+
isHydrationWrite: !editor.isFocused,
|
|
527
527
|
});
|
|
528
528
|
});
|
|
529
529
|
|
|
@@ -2,10 +2,10 @@ import { defineComponent } from 'vue';
|
|
|
2
2
|
import { mount } from '@vue/test-utils';
|
|
3
3
|
import GenericTextInputMixin from './GenericTextInput.mixin';
|
|
4
4
|
|
|
5
|
-
// BP-5810: the RTE attaches `{
|
|
6
|
-
// write-back emits. GenericTextInput must carry that through its
|
|
7
|
-
// debounce onto the outgoing payload
|
|
8
|
-
// the burst as soon as a single non-hydration write lands in it.
|
|
5
|
+
// BP-5810: the RTE attaches `{ isHydrationWrite: true }` meta to its
|
|
6
|
+
// canonical write-back emits. GenericTextInput must carry that through its
|
|
7
|
+
// 100ms debounce onto the outgoing payload under the same name — and
|
|
8
|
+
// downgrade the burst as soon as a single non-hydration write lands in it.
|
|
9
9
|
|
|
10
10
|
const DEBOUNCE_WAIT_MS = 100;
|
|
11
11
|
|
|
@@ -50,7 +50,7 @@ describe('GenericTextInput hydration burst flag', () => {
|
|
|
50
50
|
const wrapper = mountHost();
|
|
51
51
|
|
|
52
52
|
wrapper.vm.onRteModelValue('<p style="text-align: center;"></p>', {
|
|
53
|
-
|
|
53
|
+
isHydrationWrite: true,
|
|
54
54
|
});
|
|
55
55
|
|
|
56
56
|
await vi.advanceTimersByTimeAsync(DEBOUNCE_WAIT_MS + 10);
|
|
@@ -69,8 +69,8 @@ describe('GenericTextInput hydration burst flag', () => {
|
|
|
69
69
|
test('coalesced hydration writes in one burst stay flagged', async () => {
|
|
70
70
|
const wrapper = mountHost();
|
|
71
71
|
|
|
72
|
-
wrapper.vm.onRteModelValue('<p>a</p>', {
|
|
73
|
-
wrapper.vm.onRteModelValue('<p>b</p>', {
|
|
72
|
+
wrapper.vm.onRteModelValue('<p>a</p>', { isHydrationWrite: true });
|
|
73
|
+
wrapper.vm.onRteModelValue('<p>b</p>', { isHydrationWrite: true });
|
|
74
74
|
|
|
75
75
|
await vi.advanceTimersByTimeAsync(DEBOUNCE_WAIT_MS + 10);
|
|
76
76
|
|
|
@@ -82,7 +82,7 @@ describe('GenericTextInput hydration burst flag', () => {
|
|
|
82
82
|
test('a user write in the same burst downgrades it to a normal edit', async () => {
|
|
83
83
|
const wrapper = mountHost();
|
|
84
84
|
|
|
85
|
-
wrapper.vm.onRteModelValue('<p>canonical</p>', {
|
|
85
|
+
wrapper.vm.onRteModelValue('<p>canonical</p>', { isHydrationWrite: true });
|
|
86
86
|
// Focused typing emits without meta.
|
|
87
87
|
wrapper.vm.onRteModelValue('<p>typed</p>');
|
|
88
88
|
|
|
@@ -106,7 +106,7 @@ describe('GenericTextInput hydration burst flag', () => {
|
|
|
106
106
|
test('the flag resets between bursts', async () => {
|
|
107
107
|
const wrapper = mountHost();
|
|
108
108
|
|
|
109
|
-
wrapper.vm.onRteModelValue('<p>canonical</p>', {
|
|
109
|
+
wrapper.vm.onRteModelValue('<p>canonical</p>', { isHydrationWrite: true });
|
|
110
110
|
await vi.advanceTimersByTimeAsync(DEBOUNCE_WAIT_MS + 10);
|
|
111
111
|
|
|
112
112
|
wrapper.vm.text = '<p>typed later</p>';
|
|
@@ -232,8 +232,11 @@ export default defineComponent({
|
|
|
232
232
|
// attaches to its update:modelValue emits (a plain v-model binding
|
|
233
233
|
// would drop the second argument), records it for the `text` setter,
|
|
234
234
|
// then applies the write.
|
|
235
|
-
onRteModelValue(
|
|
236
|
-
|
|
235
|
+
onRteModelValue(
|
|
236
|
+
newVal: string,
|
|
237
|
+
meta?: { isHydrationWrite?: boolean },
|
|
238
|
+
): void {
|
|
239
|
+
this.pendingWriteIsHydration = meta?.isHydrationWrite === true;
|
|
237
240
|
this.text = newVal;
|
|
238
241
|
},
|
|
239
242
|
|
|
@@ -69,6 +69,24 @@ const DEFAULT_TEXT_COLOR_DEPENDENT_TYPES: readonly ContentType[] = [
|
|
|
69
69
|
// the store while state actually changed.
|
|
70
70
|
const isSameValue = (a: unknown, b: unknown): boolean => a === b;
|
|
71
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Arms the undo history to fold the next mutation into the current head
|
|
74
|
+
* instead of pushing an entry — the hydration-write contract (BP-5810).
|
|
75
|
+
* Framework-originated writes (RTE canonical HTML on mount, style
|
|
76
|
+
* re-derivation, lazy config hydration) reach this store through debounced
|
|
77
|
+
* field chains, so the flag travels WITH the payload and the arm happens
|
|
78
|
+
* here, in the same tick as the write — arming at emit time would lapse
|
|
79
|
+
* before the write lands. Call only AFTER an action's drop guards (missing
|
|
80
|
+
* item, value-identical): a dropped write must never arm. See
|
|
81
|
+
* UpdateItemValuePayload.isHydrationWrite (models.ts) for the payload
|
|
82
|
+
* contract.
|
|
83
|
+
*/
|
|
84
|
+
const armHistoryMergeForHydrationWrite = (isHydrationWrite?: boolean): void => {
|
|
85
|
+
if (!isHydrationWrite) return;
|
|
86
|
+
|
|
87
|
+
useHistoryStore().mergeNextMutation();
|
|
88
|
+
};
|
|
89
|
+
|
|
72
90
|
export const CONTENT_SETTINGS_MODES = {
|
|
73
91
|
edit: 'edit',
|
|
74
92
|
add: 'add',
|
|
@@ -403,15 +421,7 @@ export const useUiContentSettingsStore = defineStore('uiContentSettings', {
|
|
|
403
421
|
return; // no-op write: don't validate, don't flip the modified flag
|
|
404
422
|
}
|
|
405
423
|
|
|
406
|
-
|
|
407
|
-
// hydration, stale-id clears) fold into the current undo head instead
|
|
408
|
-
// of pushing an entry. Arming here — same tick as the write — is what
|
|
409
|
-
// makes the merge reliable: the flag travelled with the payload
|
|
410
|
-
// through the debounced field chain, so emit-time arming would lapse
|
|
411
|
-
// before the write lands (BP-5810).
|
|
412
|
-
if (payload.isHydrationWrite) {
|
|
413
|
-
useHistoryStore().mergeNextMutation();
|
|
414
|
-
}
|
|
424
|
+
armHistoryMergeForHydrationWrite(payload.isHydrationWrite);
|
|
415
425
|
|
|
416
426
|
item.settings.content[payload.name] = payload.value;
|
|
417
427
|
|
|
@@ -445,10 +455,7 @@ export const useUiContentSettingsStore = defineStore('uiContentSettings', {
|
|
|
445
455
|
return; // every field a no-op — see isSameValue
|
|
446
456
|
}
|
|
447
457
|
|
|
448
|
-
|
|
449
|
-
if (isHydrationWrite) {
|
|
450
|
-
useHistoryStore().mergeNextMutation();
|
|
451
|
-
}
|
|
458
|
+
armHistoryMergeForHydrationWrite(isHydrationWrite);
|
|
452
459
|
|
|
453
460
|
item.settings.content = vueDeepMerge(item.settings.content, value);
|
|
454
461
|
this.validateItem({ id, name });
|
|
@@ -466,10 +473,7 @@ export const useUiContentSettingsStore = defineStore('uiContentSettings', {
|
|
|
466
473
|
return;
|
|
467
474
|
}
|
|
468
475
|
|
|
469
|
-
|
|
470
|
-
if (isHydrationWrite) {
|
|
471
|
-
useHistoryStore().mergeNextMutation();
|
|
472
|
-
}
|
|
476
|
+
armHistoryMergeForHydrationWrite(isHydrationWrite);
|
|
473
477
|
|
|
474
478
|
item.settings.content[`${name}Style`].textStyleType = vueDeepMerge(
|
|
475
479
|
item.settings.content,
|
|
@@ -490,14 +494,11 @@ export const useUiContentSettingsStore = defineStore('uiContentSettings', {
|
|
|
490
494
|
return;
|
|
491
495
|
}
|
|
492
496
|
|
|
493
|
-
//
|
|
494
|
-
//
|
|
495
|
-
//
|
|
496
|
-
//
|
|
497
|
-
|
|
498
|
-
if (isHydrationWrite) {
|
|
499
|
-
useHistoryStore().mergeNextMutation();
|
|
500
|
-
}
|
|
497
|
+
// The RTE's mount-time style sync (setInitialStyleSettings) lands here
|
|
498
|
+
// ~100ms BEFORE its canonical text write-back — unflagged, it would
|
|
499
|
+
// open a plain capture burst that swallows the flagged text write
|
|
500
|
+
// (BP-5810's second act).
|
|
501
|
+
armHistoryMergeForHydrationWrite(isHydrationWrite);
|
|
501
502
|
|
|
502
503
|
item.settings.content[`${name}Style`] = vueDeepMerge(
|
|
503
504
|
item.settings.content[`${name}Style`],
|
|
@@ -528,10 +529,7 @@ export const useUiContentSettingsStore = defineStore('uiContentSettings', {
|
|
|
528
529
|
return;
|
|
529
530
|
}
|
|
530
531
|
|
|
531
|
-
|
|
532
|
-
if (isHydrationWrite) {
|
|
533
|
-
useHistoryStore().mergeNextMutation();
|
|
534
|
-
}
|
|
532
|
+
armHistoryMergeForHydrationWrite(isHydrationWrite);
|
|
535
533
|
|
|
536
534
|
item.settings.content.textItems[index!].textStyle.textStyleType =
|
|
537
535
|
vueDeepMerge(
|
|
@@ -554,10 +552,7 @@ export const useUiContentSettingsStore = defineStore('uiContentSettings', {
|
|
|
554
552
|
return;
|
|
555
553
|
}
|
|
556
554
|
|
|
557
|
-
|
|
558
|
-
if (isHydrationWrite) {
|
|
559
|
-
useHistoryStore().mergeNextMutation();
|
|
560
|
-
}
|
|
555
|
+
armHistoryMergeForHydrationWrite(isHydrationWrite);
|
|
561
556
|
|
|
562
557
|
item.settings.content.textItems[index!].textStyle = vueDeepMerge(
|
|
563
558
|
item.settings.content.textItems[index!].textStyle,
|