@designcrowd/library.brand-page 6.0.6-tracker-1 → 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-DD0OBnij.js → CreatedOnBrandCrowd-de-de-x7BcD7Sj.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-en-us-DysrpsmM.js → CreatedOnBrandCrowd-en-us-CG-ThD_q.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-es-es-B6twGr9h.js → CreatedOnBrandCrowd-es-es-aab4_Re2.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-fr-yxl0ih_4.js → CreatedOnBrandCrowd-fr-ca-Drl8X5uh.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-ca-yxl0ih_4.js → CreatedOnBrandCrowd-fr-fr-Drl8X5uh.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-pt-BtXDrIcx.js → CreatedOnBrandCrowd-pt-br-8Ib7T70i.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-br-BtXDrIcx.js → CreatedOnBrandCrowd-pt-pt-8Ib7T70i.js} +1 -1
- package/lib/src/{CreatedOnDcom-de-de-ho_ROy1L.js → CreatedOnDcom-de-de-FJtydzc5.js} +1 -1
- package/lib/src/{CreatedOnDcom-en-us-C91p9kls.js → CreatedOnDcom-en-us-BMiXru6P.js} +1 -1
- package/lib/src/{CreatedOnDcom-es-es-B3FYmObE.js → CreatedOnDcom-es-es-B8Srdnku.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-ca-D40m7fPx.js → CreatedOnDcom-fr-ca-tcXGiBNt.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-fr-Cd57oYyc.js → CreatedOnDcom-fr-fr-nE1cuUEU.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-br-Du57mWxG.js → CreatedOnDcom-pt-br-DxCCXCg8.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-pt-fYoPZWwr.js → CreatedOnDcom-pt-pt-Csabip9G.js} +1 -1
- package/lib/src/brand-page/utils/logo.util.d.ts +14 -0
- package/lib/src/brand-page/utils/logo.util.js +19 -0
- package/lib/src/brand-page-maker/components/action-bar/ActionBar.mixin.d.ts +1 -1
- package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.d.ts +2 -2
- package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.js +1 -1
- package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMakerTemplateSettings.mixin.d.ts +1 -1
- package/lib/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.d.ts +3 -3
- package/lib/src/brand-page-maker/components/brandpage-settings/ReplaceImageSettingsForBrandPageSettings.mixin.d.ts +1 -1
- 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 +2 -2
- package/lib/src/brand-page-maker/components/seo-settings/UrlSettings.mixin.d.ts +1 -1
- package/lib/src/brand-page-maker/components/social-share/SocialShare.mixin.d.ts +1 -1
- package/lib/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.d.ts +1 -1
- package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.js +25 -4
- package/lib/src/brand-page-maker/composables/useUndoRedoFlag.d.ts +6 -7
- package/lib/src/brand-page-maker/composables/useUndoRedoFlag.js +7 -12
- package/lib/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.d.ts +1 -1
- package/lib/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.js +12 -7
- package/lib/src/brand-page-maker/stores/history/history-store.d.ts +7 -1
- package/lib/src/brand-page-maker/stores/history/history-store.js +11 -1
- package/lib/src/index.mjs +83 -84
- package/lib/src/{lib-lMNy66QO.js → lib-C_UlYMJy.js} +32835 -32458
- package/lib/src/metadata-builder/builders/reorder-pages-builder.d.ts +13 -0
- package/lib/src/metadata-builder/builders/reorder-pages-builder.js +49 -0
- 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 +3 -3
- package/lib/src/shared/content-settings/components/content-settings/ReplaceImageSettingsForContent.mixin.d.ts +2 -2
- 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 +2 -2
- 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/text-style-field/TextStyleField.mixin.d.ts +1 -0
- package/lib/src/shared/content-settings/components/text-style-field/TextStyleField.mixin.js +18 -7
- 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/logo-settings/components/LogoSettings.mixin.d.ts +1 -1
- package/lib/src/shared/page-settings/components/page-settings/PageSettings.mixin.d.ts +2 -2
- package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.d.ts +2 -2
- package/lib/src/shared/publish-brand-page-with-state/PublishBrandPageModalWithState.mixin.d.ts +1 -1
- package/lib/src/shared/style-settings/components/StyleSettings.mixin.d.ts +1 -1
- package/package.json +1 -16
- package/src/brand-page/utils/logo.util.test.ts +54 -1
- package/src/brand-page/utils/logo.util.ts +23 -0
- package/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.ts +1 -1
- package/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.ts +26 -5
- package/src/brand-page-maker/composables/useUndoRedoFlag.ts +7 -13
- package/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.test.ts +53 -0
- package/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.ts +13 -16
- package/src/brand-page-maker/stores/history/history-store.test.ts +51 -0
- package/src/brand-page-maker/stores/history/history-store.ts +19 -0
- package/src/metadata-builder/builders/reorder-pages-builder.ts +80 -0
- 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/text-style-field/TextStyleField.mixin.ts +19 -11
- 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
- package/lib/src/tracker.d.ts +0 -2
- package/lib/src/tracker.js +0 -12
- package/lib/src/tracker.mjs +0 -12
- package/lib/src/useEventTracker-DmQ6AkNn.js +0 -356
- package/src/tracker.ts +0 -28
|
@@ -26,6 +26,12 @@ export interface HistoryEntry {
|
|
|
26
26
|
activePageId: string;
|
|
27
27
|
/** JSON-stringified `editableLogoStore.logoEdits`. Undefined if logo tab never opened. */
|
|
28
28
|
logoEdits?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Selected template id at capture time — drives the templates sidebar
|
|
31
|
+
* highlight. `false` when no template is selected. Sidecar to
|
|
32
|
+
* `brandPageData` (not part of the persisted model), like `activePageId`.
|
|
33
|
+
*/
|
|
34
|
+
selectedTemplateId?: string | false;
|
|
29
35
|
}
|
|
30
36
|
|
|
31
37
|
// Monotonic burst — first mutation starts the timer, subsequent mutations
|
|
@@ -150,6 +156,10 @@ export const useHistoryStore = defineStore('history', () => {
|
|
|
150
156
|
return usePageNavigationStore().activePageId ?? '';
|
|
151
157
|
}
|
|
152
158
|
|
|
159
|
+
function getCurrentSelectedTemplateId(): string | false {
|
|
160
|
+
return useBrandPageMakerStore().selectedTemplateId;
|
|
161
|
+
}
|
|
162
|
+
|
|
153
163
|
function serializeCurrentLogoEdits(): string | undefined {
|
|
154
164
|
try {
|
|
155
165
|
const editableLogoStore = useUiEditableLogoStore();
|
|
@@ -187,6 +197,7 @@ export const useHistoryStore = defineStore('history', () => {
|
|
|
187
197
|
label?: string;
|
|
188
198
|
activePageId: string;
|
|
189
199
|
logoEdits?: string;
|
|
200
|
+
selectedTemplateId?: string | false;
|
|
190
201
|
}): 'committed' | 'oversized' | 'noop' {
|
|
191
202
|
const current = entries.value[currentIndex.value];
|
|
192
203
|
|
|
@@ -219,6 +230,7 @@ export const useHistoryStore = defineStore('history', () => {
|
|
|
219
230
|
sizeBytes: args.snapshot.length,
|
|
220
231
|
activePageId: args.activePageId,
|
|
221
232
|
logoEdits: args.logoEdits,
|
|
233
|
+
selectedTemplateId: args.selectedTemplateId,
|
|
222
234
|
};
|
|
223
235
|
|
|
224
236
|
// FR-11: truncate redo tail, then append.
|
|
@@ -293,6 +305,7 @@ export const useHistoryStore = defineStore('history', () => {
|
|
|
293
305
|
|
|
294
306
|
const activePageId = getCurrentActivePageId();
|
|
295
307
|
const logoEdits = serializeCurrentLogoEdits();
|
|
308
|
+
const selectedTemplateId = getCurrentSelectedTemplateId();
|
|
296
309
|
|
|
297
310
|
// Settle window: fold post-init reactive cascades back into the
|
|
298
311
|
// baseline so undo doesn't light up before the user has done
|
|
@@ -317,6 +330,7 @@ export const useHistoryStore = defineStore('history', () => {
|
|
|
317
330
|
sizeBytes: snapshot.length,
|
|
318
331
|
activePageId,
|
|
319
332
|
logoEdits,
|
|
333
|
+
selectedTemplateId,
|
|
320
334
|
},
|
|
321
335
|
];
|
|
322
336
|
|
|
@@ -333,6 +347,7 @@ export const useHistoryStore = defineStore('history', () => {
|
|
|
333
347
|
sizeBytes: snapshot.length,
|
|
334
348
|
activePageId,
|
|
335
349
|
logoEdits,
|
|
350
|
+
selectedTemplateId,
|
|
336
351
|
};
|
|
337
352
|
entries.value = next;
|
|
338
353
|
|
|
@@ -345,6 +360,7 @@ export const useHistoryStore = defineStore('history', () => {
|
|
|
345
360
|
actor: 'user',
|
|
346
361
|
activePageId,
|
|
347
362
|
logoEdits,
|
|
363
|
+
selectedTemplateId,
|
|
348
364
|
});
|
|
349
365
|
};
|
|
350
366
|
|
|
@@ -402,6 +418,7 @@ export const useHistoryStore = defineStore('history', () => {
|
|
|
402
418
|
sizeBytes: snapshot.length,
|
|
403
419
|
activePageId: getCurrentActivePageId(),
|
|
404
420
|
logoEdits: serializeCurrentLogoEdits(),
|
|
421
|
+
selectedTemplateId: getCurrentSelectedTemplateId(),
|
|
405
422
|
},
|
|
406
423
|
];
|
|
407
424
|
currentIndex.value = 0;
|
|
@@ -427,6 +444,7 @@ export const useHistoryStore = defineStore('history', () => {
|
|
|
427
444
|
snapshot,
|
|
428
445
|
activePageId: getCurrentActivePageId(),
|
|
429
446
|
logoEdits: serializeCurrentLogoEdits(),
|
|
447
|
+
selectedTemplateId: getCurrentSelectedTemplateId(),
|
|
430
448
|
...args,
|
|
431
449
|
});
|
|
432
450
|
}
|
|
@@ -531,6 +549,7 @@ export const useHistoryStore = defineStore('history', () => {
|
|
|
531
549
|
snapshot,
|
|
532
550
|
resolvedActivePageId,
|
|
533
551
|
targetEntry?.logoEdits,
|
|
552
|
+
targetEntry?.selectedTemplateId,
|
|
534
553
|
);
|
|
535
554
|
} finally {
|
|
536
555
|
isApplyingSnapshot.value = false;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { BrandPageModel, NavOrder } from '../../brand-page/models';
|
|
2
|
+
import {
|
|
3
|
+
BaseMetadataBuilder,
|
|
4
|
+
type BuilderResult,
|
|
5
|
+
type MetadataBuilderRequest,
|
|
6
|
+
} from '../models';
|
|
7
|
+
import type { Logger } from '../logger';
|
|
8
|
+
|
|
9
|
+
export interface ReorderPagesPayload {
|
|
10
|
+
pageOrder: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class ReorderPagesBuilder extends BaseMetadataBuilder<ReorderPagesPayload> {
|
|
14
|
+
constructor(
|
|
15
|
+
request: MetadataBuilderRequest<ReorderPagesPayload>,
|
|
16
|
+
logger?: Logger,
|
|
17
|
+
) {
|
|
18
|
+
super(request, logger);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async validateAsync(): Promise<{ errors: string[] }> {
|
|
22
|
+
const errors: string[] = [];
|
|
23
|
+
const payload = this.request.payload;
|
|
24
|
+
|
|
25
|
+
if (!payload?.pageOrder?.length) {
|
|
26
|
+
errors.push('page_order is required for reorder_pages');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return { errors };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async buildAsync(model: BrandPageModel): Promise<BuilderResult> {
|
|
33
|
+
this.log('Build called');
|
|
34
|
+
|
|
35
|
+
const validation = await this.validateAsync();
|
|
36
|
+
|
|
37
|
+
if (validation.errors.length) {
|
|
38
|
+
return { request: this.request, model, errors: validation.errors };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const { pageOrder } = this.request.payload!;
|
|
42
|
+
|
|
43
|
+
// Build pageId → routeId lookup from nav.routes
|
|
44
|
+
const pageIdToRouteId = new Map<string, string>();
|
|
45
|
+
|
|
46
|
+
for (const route of model.nav.routes) {
|
|
47
|
+
pageIdToRouteId.set(route.pageId, route.id);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Build new nav.orders from the flat page_order (array of pageIds)
|
|
51
|
+
const newOrders: NavOrder[] = [];
|
|
52
|
+
const seenRouteIds = new Set<string>();
|
|
53
|
+
|
|
54
|
+
for (const pageId of pageOrder) {
|
|
55
|
+
const routeId = pageIdToRouteId.get(pageId);
|
|
56
|
+
|
|
57
|
+
if (!routeId) continue;
|
|
58
|
+
|
|
59
|
+
newOrders.push({ routeId, subRoutes: [] });
|
|
60
|
+
seenRouteIds.add(routeId);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Safety: append any routes not mentioned (don't lose pages)
|
|
64
|
+
for (const route of model.nav.routes) {
|
|
65
|
+
const routeId = pageIdToRouteId.get(route.pageId);
|
|
66
|
+
|
|
67
|
+
if (routeId && !seenRouteIds.has(routeId)) {
|
|
68
|
+
newOrders.push({ routeId, subRoutes: [] });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
model.nav.orders = newOrders;
|
|
73
|
+
|
|
74
|
+
this.log({
|
|
75
|
+
message: `Reordered pages: ${pageOrder.join(', ')}`,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return { request: this.request, model };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -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
|
+
});
|