@designcrowd/library.brand-page 6.2.2 → 6.2.3-ast-faq
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-7P2gNr-R.js → CreatedOnBrandCrowd-de-de-BRisMzFY.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-en-us-Dl7BymW6.js → CreatedOnBrandCrowd-en-us-CDDsL9GX.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-es-es-CO4FvWVw.js → CreatedOnBrandCrowd-es-es-BkZrEo4j.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-fr-DHNqakQu.js → CreatedOnBrandCrowd-fr-ca-sx_ury7R.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-ca-DHNqakQu.js → CreatedOnBrandCrowd-fr-fr-sx_ury7R.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-pt-CGUC-ZbP.js → CreatedOnBrandCrowd-pt-br-DCWiAfkU.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-br-CGUC-ZbP.js → CreatedOnBrandCrowd-pt-pt-DCWiAfkU.js} +1 -1
- package/lib/src/{CreatedOnDcom-de-de-Dvds-Vmy.js → CreatedOnDcom-de-de-C1S7f22L.js} +1 -1
- package/lib/src/{CreatedOnDcom-en-us-fd3C-8Tp.js → CreatedOnDcom-en-us-DgTlWaXE.js} +1 -1
- package/lib/src/{CreatedOnDcom-es-es-DTXZW35-.js → CreatedOnDcom-es-es-CxSQJe_V.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-ca-DRCYOKPs.js → CreatedOnDcom-fr-ca-Bbam2tt6.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-fr-DpIB8x2x.js → CreatedOnDcom-fr-fr-Cb0XDRPL.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-br-Esnxy8wX.js → CreatedOnDcom-pt-br-BjdX3-E3.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-pt-CaiVHFXR.js → CreatedOnDcom-pt-pt-BXpbfopB.js} +1 -1
- package/lib/src/brand-page/constants/misc.constants.d.ts +8 -0
- package/lib/src/brand-page/constants/misc.constants.js +7 -0
- package/lib/src/brand-page/constants/styles.constants.d.ts +2 -0
- package/lib/src/brand-page/constants/styles.constants.js +2 -0
- package/lib/src/brand-page/models/brand-page-context.model.d.ts +0 -2
- package/lib/src/brand-page/models/brand-page-context.model.js +0 -12
- package/lib/src/brand-page/models/content.model.d.ts +1 -0
- package/lib/src/brand-page/models/content.model.js +1 -0
- package/lib/src/brand-page/models/index.d.ts +19 -0
- package/lib/src/brand-page/utils/content-callback-util.d.ts +2 -1
- package/lib/src/brand-page/utils/content-callback-util.js +3 -0
- package/lib/src/brand-page/utils/model.util.d.ts +2 -1
- package/lib/src/brand-page/utils/model.util.js +3 -0
- package/lib/src/brand-page/utils/view-model.util.d.ts +2 -0
- package/lib/src/brand-page/utils/view-model.util.js +24 -0
- package/lib/src/brand-page/view-models.d.ts +9 -2
- package/lib/src/brand-page-maker/components/action-bar/ActionBar.mixin.d.ts +0 -1
- package/lib/src/brand-page-maker/components/action-bar/ActionBar.mixin.js +0 -9
- package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.d.ts +10 -42
- package/lib/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.d.ts +10 -42
- package/lib/src/brand-page-maker/components/preview/Preview.mixin.d.ts +0 -14
- package/lib/src/brand-page-maker/components/quick-link-bar/QuickLinkBar.mixin.d.ts +0 -42
- package/lib/src/brand-page-maker/components/seo-settings/SeoPageSettings.mixin.d.ts +0 -42
- package/lib/src/brand-page-maker/components/seo-settings/UrlSettings.mixin.d.ts +0 -42
- package/lib/src/brand-page-maker/components/seo-settings/UseExistingDomainModal.mixin.d.ts +0 -42
- package/lib/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.d.ts +0 -42
- package/lib/src/brand-page-maker/components/upgrade-to-premium-button/UpgradeToPremiumButton.mixin.d.ts +0 -42
- package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.d.ts +13 -0
- package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.js +64 -30
- package/lib/src/brand-page-maker/constants/config-settings-constant.d.ts +2 -1
- package/lib/src/brand-page-maker/constants/config-settings-constant.js +101 -2
- package/lib/src/brand-page-maker/convertors/faq-display-style.convertor.d.ts +3 -0
- package/lib/src/brand-page-maker/convertors/faq-display-style.convertor.js +47 -0
- package/lib/src/brand-page-maker/models/content-settings-configs.d.ts +22 -1
- package/lib/src/brand-page-maker/models/content-settings-configs.js +1 -0
- package/lib/src/brand-page-maker/models/content-settings.d.ts +4 -1
- package/lib/src/brand-page-maker/models/content-settings.js +45 -1
- package/lib/src/brand-page-maker/stores/brandpage-container/brand-page-container-store.d.ts +0 -42
- package/lib/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.js +1 -0
- package/lib/src/brand-page-maker/utils/apply-color-strategy.js +47 -17
- package/lib/src/brand-page-maker/utils/rich-text-editor.util.d.ts +6 -0
- package/lib/src/brand-page-maker/utils/rich-text-editor.util.js +24 -1
- package/lib/src/brand-page-maker/validators/field.validator.js +2 -0
- package/lib/src/bundled-translations-DY9YvuL-.js +179 -0
- package/lib/src/bundles/bundled-translations.json +20 -2
- package/lib/src/composables/useTranslate.d.ts +1 -1
- package/lib/src/composables/useTranslate.js +1 -1
- package/lib/src/css/library.brand-page-brandCrowd.min.css +143 -36
- package/lib/src/css/library.brand-page-brandPage.min.css +143 -36
- package/lib/src/css/library.brand-page-designCom.min.css +143 -36
- package/lib/src/css/library.brand-page-preview.min.css +143 -36
- package/lib/src/index.mjs +159 -158
- package/lib/src/{lib-V7aqfuiB.js → lib-DwpSAbLZ.js} +26889 -26178
- package/lib/src/metadata-builder/parsers/section-tool-arguments.d.ts +1 -1
- package/lib/src/shared/color-strategies/color-strategies.js +4 -0
- package/lib/src/shared/color-strategies/rte-field-pairs.d.ts +12 -0
- package/lib/src/shared/color-strategies/rte-field-pairs.js +10 -0
- package/lib/src/shared/content-settings/components/content-settings/ContentSettings.mixin.d.ts +10 -0
- package/lib/src/shared/content-settings/components/content-settings/ReplaceImageSettingsForContent.mixin.d.ts +11 -1
- package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItem.mixin.d.ts +10 -0
- package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.d.ts +10 -0
- package/lib/src/shared/content-settings/components/focused-content-item-settings/FocusedContentItemSettings.mixin.d.ts +10 -0
- package/lib/src/shared/content-settings/components/items-field/ItemsField.mixin.d.ts +14 -1
- package/lib/src/shared/content-settings/components/items-field/ItemsField.mixin.js +11 -1
- package/lib/src/shared/content-settings/components/payment-config-field/PaymentConfigField.mixin.d.ts +0 -42
- package/lib/src/shared/content-settings/components/textarea-field/TextareaField.mixin.d.ts +12 -0
- package/lib/src/shared/content-settings/components/textarea-field/TextareaField.mixin.js +5 -0
- package/lib/src/shared/content-settings/models.d.ts +6 -0
- package/lib/src/shared/content-settings/store/content-settings-store.d.ts +14 -1
- package/lib/src/shared/content-settings/store/content-settings-store.js +24 -2
- package/lib/src/shared/logo-settings/components/LogoSettings.mixin.d.ts +0 -42
- package/lib/src/shared/page-settings/components/page-settings/PageSettings.mixin.d.ts +10 -42
- package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.d.ts +10 -42
- package/lib/src/shared/publish-brand-page-with-state/PublishBrandPageModalWithState.mixin.d.ts +0 -56
- package/lib/src/shared/side-bar/components/SideBar.mixin.d.ts +0 -42
- package/lib/src/shared/style-settings/components/StyleSettings.mixin.d.ts +0 -42
- package/package.json +2 -2
- package/src/brand-page/components/faq-section/FaqSection.test.ts +292 -0
- package/src/brand-page/components/faq-section/FaqSection.vue +299 -0
- package/src/brand-page/components/page/PageComponent.vue +2 -0
- package/src/brand-page/components/section-heading/SectionHeading.vue +52 -0
- package/src/brand-page/components/text-wrapper/TextWrapper.test.ts +33 -0
- package/src/brand-page/constants/misc.constants.ts +12 -0
- package/src/brand-page/constants/styles.constants.ts +2 -0
- package/src/brand-page/models/brand-page-context.model.ts +0 -23
- package/src/brand-page/models/content.model.ts +1 -0
- package/src/brand-page/models/index.ts +21 -0
- package/src/brand-page/utils/content-callback-util.ts +5 -0
- package/src/brand-page/utils/model.util.ts +7 -0
- package/src/brand-page/utils/view-model.util.test.ts +96 -0
- package/src/brand-page/utils/view-model.util.ts +49 -0
- package/src/brand-page/view-models.ts +12 -1
- package/src/brand-page-maker/components/action-bar/ActionBar.mixin.ts +0 -10
- package/src/brand-page-maker/components/action-bar/ActionBarNavigation.test.ts +8 -49
- package/src/brand-page-maker/components/action-bar/ActionBarNavigation.vue +1 -1
- package/src/brand-page-maker/components/rich-text-editor/RichTextEditor.test.ts +165 -0
- package/src/brand-page-maker/components/rich-text-editor/RichTextEditor.vue +37 -49
- package/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.ts +87 -30
- package/src/brand-page-maker/constants/config-settings-constant.ts +108 -0
- package/src/brand-page-maker/constants/faq-section-config.test.ts +151 -0
- package/src/brand-page-maker/convertors/faq-display-style.convertor.ts +59 -0
- package/src/brand-page-maker/convertors/i18n/converter.json +4 -1
- package/src/brand-page-maker/models/content-settings-configs.ts +24 -0
- package/src/brand-page-maker/models/content-settings.ts +57 -0
- package/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.ts +2 -0
- package/src/brand-page-maker/utils/__snapshots__/apply-color-strategy.golden.test.ts.snap +4063 -895
- package/src/brand-page-maker/utils/apply-color-strategy.test.ts +104 -2
- package/src/brand-page-maker/utils/apply-color-strategy.ts +77 -19
- package/src/brand-page-maker/utils/rich-text-editor.util.test.ts +110 -0
- package/src/brand-page-maker/utils/rich-text-editor.util.ts +66 -0
- package/src/brand-page-maker/validators/field.validator.ts +2 -0
- package/src/bundles/bundled-translations.json +20 -2
- package/src/composables/useTranslate.ts +2 -1
- package/src/metadata-builder/builders/item-id-utils.test.ts +24 -0
- package/src/metadata-builder/parsers/section-tool-arguments.ts +2 -1
- package/src/metadata-builder/parsers/section-tool-map.test.ts +12 -0
- package/src/shared/color-strategies/color-strategies.ts +4 -0
- package/src/shared/color-strategies/rte-field-pairs.ts +23 -0
- package/src/shared/content-settings/components/content-settings/i18n/content-settings.json +17 -1
- package/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.vue +3 -0
- package/src/shared/content-settings/components/items-field/ItemsField.mixin.ts +11 -1
- package/src/shared/content-settings/components/items-field/ItemsField.vue +8 -2
- package/src/shared/content-settings/components/list-items-field/ListItemsField.test.ts +429 -0
- package/src/shared/content-settings/components/list-items-field/ListItemsField.vue +262 -0
- package/src/shared/content-settings/components/textarea-field/TextareaField.mixin.ts +6 -0
- package/src/shared/content-settings/components/textarea-field/TextareaField.vue +1 -0
- package/src/shared/content-settings/models.ts +7 -0
- package/src/shared/content-settings/store/content-settings-store.test.ts +70 -0
- package/src/shared/content-settings/store/content-settings-store.ts +37 -3
- package/src/shared/ecommerce-settings/util/refreshDuplicatedMetadata.test.ts +19 -0
- package/src/shared/page-settings/store/page-settings-store.test.ts +0 -71
- package/src/utils/minify-brand-page-metadata.test.ts +35 -0
- package/lib/src/bundled-translations-BYUfWBT3.js +0 -179
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div data-component="list-items-field" class="tw-flex tw-flex-col tw-gap-2">
|
|
3
|
+
<ItemsField
|
|
4
|
+
ref="itemsFieldRef"
|
|
5
|
+
:items="items"
|
|
6
|
+
:can-collapse="true"
|
|
7
|
+
:show-preview-image="false"
|
|
8
|
+
:collapsed-by-default="true"
|
|
9
|
+
:delete-label="contentSettingsTr(`listItemsDeleteLabel`)"
|
|
10
|
+
@items-change="onReorder"
|
|
11
|
+
@items-delete="onDelete"
|
|
12
|
+
@item-toggle="onToggle"
|
|
13
|
+
>
|
|
14
|
+
<template #preview="{ item }">
|
|
15
|
+
<p class="tw-w-full tw-truncate tw-m-0" data-test-list-item-label>
|
|
16
|
+
{{ rowLabel(item) }}
|
|
17
|
+
</p>
|
|
18
|
+
</template>
|
|
19
|
+
<template #form="{ index }">
|
|
20
|
+
<div v-for="schema in itemFields" :key="schema.key" class="tw-mt-3">
|
|
21
|
+
<FieldSubHeader :title="schema.title" />
|
|
22
|
+
<template v-if="isRichText(schema)">
|
|
23
|
+
<!-- The same editor as a section title; its toolbar writes this item's own style. -->
|
|
24
|
+
<TextareaField
|
|
25
|
+
:id="id"
|
|
26
|
+
:key="`${items[index].id}-${schema.key}`"
|
|
27
|
+
:name="schema.key"
|
|
28
|
+
:value="items[index][schema.key]"
|
|
29
|
+
:styles="items[index][schema.styleKey!]"
|
|
30
|
+
:item="items[index]"
|
|
31
|
+
:item-style-target="{
|
|
32
|
+
itemsField: name,
|
|
33
|
+
index,
|
|
34
|
+
styleField: schema.styleKey!,
|
|
35
|
+
}"
|
|
36
|
+
:placeholder="schema.placeholder"
|
|
37
|
+
:max-length="schema.maxLength"
|
|
38
|
+
:content-type="content.type"
|
|
39
|
+
:show-rich-text-editor="true"
|
|
40
|
+
:attrs="{ 'data-test-tag': `list-item-${schema.key}` }"
|
|
41
|
+
@update:model-value="onFieldInput(index, schema.key, $event)"
|
|
42
|
+
/>
|
|
43
|
+
</template>
|
|
44
|
+
<template v-else>
|
|
45
|
+
<!-- The section fields' plain inputs: they encode on write, as saved text expects. -->
|
|
46
|
+
<component
|
|
47
|
+
:is="schema.kind === 'text' ? TextField : TextareaField"
|
|
48
|
+
:id="id"
|
|
49
|
+
:key="`${items[index].id}-${schema.key}`"
|
|
50
|
+
:name="schema.key"
|
|
51
|
+
:value="items[index][schema.key]"
|
|
52
|
+
v-bind="schema.kind === 'text' ? {} : { item: items[index] }"
|
|
53
|
+
:placeholder="schema.placeholder"
|
|
54
|
+
:max-length="schema.maxLength"
|
|
55
|
+
:attrs="{ 'data-test-tag': `list-item-${schema.key}` }"
|
|
56
|
+
@update:model-value="onFieldInput(index, schema.key, $event)"
|
|
57
|
+
/>
|
|
58
|
+
<TextStyleField
|
|
59
|
+
v-if="schema.styleKey"
|
|
60
|
+
:id="`${id}-${items[index].id}-${schema.styleKey}`"
|
|
61
|
+
:name="schema.styleKey"
|
|
62
|
+
:value="items[index][schema.styleKey]"
|
|
63
|
+
:is-admin-maker="isAdminMaker"
|
|
64
|
+
:logo-theme="logoTheme"
|
|
65
|
+
:show-font-style-only="true"
|
|
66
|
+
:is-text-content="true"
|
|
67
|
+
@update:model-value="onStyleInput(index, schema.styleKey, $event)"
|
|
68
|
+
/>
|
|
69
|
+
</template>
|
|
70
|
+
</div>
|
|
71
|
+
</template>
|
|
72
|
+
</ItemsField>
|
|
73
|
+
<Button
|
|
74
|
+
:label="
|
|
75
|
+
isAtMax
|
|
76
|
+
? contentSettingsTr(`listItemsMaxReached`, { max: maxLength })
|
|
77
|
+
: contentSettingsTr(`listItemsAddItem`)
|
|
78
|
+
"
|
|
79
|
+
:disabled="isAtMax"
|
|
80
|
+
variant="outline"
|
|
81
|
+
size="small-medium"
|
|
82
|
+
:full-width="true"
|
|
83
|
+
:attrs="{ 'data-test-list-items-add': '' }"
|
|
84
|
+
@on-click.stop="onAdd"
|
|
85
|
+
/>
|
|
86
|
+
</div>
|
|
87
|
+
</template>
|
|
88
|
+
<script setup lang="ts">
|
|
89
|
+
import type { PropType } from 'vue';
|
|
90
|
+
import { computed, nextTick, ref, watch } from 'vue';
|
|
91
|
+
import { Button } from '@designcrowd/fe-shared-lib';
|
|
92
|
+
import * as uuid from 'uuid';
|
|
93
|
+
import cloneDeep from 'lodash.clonedeep';
|
|
94
|
+
import type { Content, LogoTheme, TextStyleDeclaration } from '@brand-page';
|
|
95
|
+
import type { ListItemFieldSchema } from '@brand-page-maker/models/content-settings-configs';
|
|
96
|
+
import {
|
|
97
|
+
getPlainTextFromRTE,
|
|
98
|
+
switchRteItemFieldsToRTE,
|
|
99
|
+
} from '@brand-page-maker/utils';
|
|
100
|
+
import { useExternalRichTextEditor } from '@brand-page-maker/composables/rich-text-editor/useRichTextEditor';
|
|
101
|
+
import { useUiStyleSettingsStore } from '@shared/style-settings/store';
|
|
102
|
+
import { useUiContentSettingsStore } from '../../store/content-settings-store';
|
|
103
|
+
import { contentSettingsTr } from '@src/composables/useTranslate';
|
|
104
|
+
import ItemsField from '../items-field/ItemsField.vue';
|
|
105
|
+
import TextField from '../text-field/TextField.vue';
|
|
106
|
+
import TextareaField from '../textarea-field/TextareaField.vue';
|
|
107
|
+
import TextStyleField from '../text-style-field/TextStyleField.vue';
|
|
108
|
+
import FieldSubHeader from '../field-sub-header/FieldSubHeader.vue';
|
|
109
|
+
import type { UpdateItemValuePayload } from '../../models';
|
|
110
|
+
|
|
111
|
+
type ListItem = { id: string } & Record<string, any>;
|
|
112
|
+
|
|
113
|
+
defineOptions({ inheritAttrs: false });
|
|
114
|
+
|
|
115
|
+
const props = defineProps({
|
|
116
|
+
id: { type: String, required: true },
|
|
117
|
+
name: { type: String, required: true },
|
|
118
|
+
value: { type: Array as PropType<ListItem[]>, required: true },
|
|
119
|
+
content: { type: Object as PropType<Content>, required: true },
|
|
120
|
+
itemFields: {
|
|
121
|
+
type: Array as PropType<ListItemFieldSchema[]>,
|
|
122
|
+
required: true,
|
|
123
|
+
},
|
|
124
|
+
maxLength: { type: Number, required: false, default: undefined },
|
|
125
|
+
// From the host field: false when the user lacks rich text, so every input stays plain.
|
|
126
|
+
showRichTextEditor: { type: Boolean, required: false, default: false },
|
|
127
|
+
isAdminMaker: { type: Boolean, required: false, default: false },
|
|
128
|
+
logoTheme: {
|
|
129
|
+
type: Object as PropType<LogoTheme>,
|
|
130
|
+
required: false,
|
|
131
|
+
default: undefined,
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const emit = defineEmits<{
|
|
136
|
+
'update:modelValue': [payload: UpdateItemValuePayload<ListItem[]>];
|
|
137
|
+
}>();
|
|
138
|
+
|
|
139
|
+
const contentSettingsStore = useUiContentSettingsStore();
|
|
140
|
+
const styleSettingsStore = useUiStyleSettingsStore();
|
|
141
|
+
const itemsFieldRef = ref<{ expand: (index: number) => void } | null>(null);
|
|
142
|
+
const items = ref<ListItem[]>(cloneDeep(props.value ?? []));
|
|
143
|
+
|
|
144
|
+
const isAtMax = computed(
|
|
145
|
+
(): boolean =>
|
|
146
|
+
props.maxLength !== undefined && items.value.length >= props.maxLength,
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const labelKey = computed(
|
|
150
|
+
(): string | undefined =>
|
|
151
|
+
props.itemFields.find((schema) => schema.kind === 'text')?.key,
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const rowLabel = (item: ListItem): string =>
|
|
155
|
+
labelKey.value ? getPlainTextFromRTE(item[labelKey.value] ?? '') : '';
|
|
156
|
+
|
|
157
|
+
const isRichText = (schema: ListItemFieldSchema): boolean =>
|
|
158
|
+
props.showRichTextEditor && !!schema.styleKey;
|
|
159
|
+
|
|
160
|
+
// Always a new array: the undo store drops same-reference writes (undo-redo.md).
|
|
161
|
+
const emitChange = (isHydrationWrite = false): void => {
|
|
162
|
+
emit('update:modelValue', {
|
|
163
|
+
id: props.id,
|
|
164
|
+
name: props.name,
|
|
165
|
+
value: cloneDeep(items.value),
|
|
166
|
+
...(isHydrationWrite ? { isHydrationWrite: true } : {}),
|
|
167
|
+
});
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// The text fields debounce their own emits; the rich-text editor also marks its
|
|
171
|
+
// mount-time rewrite as a hydration write so the store folds it into the current
|
|
172
|
+
// undo entry.
|
|
173
|
+
const onFieldInput = (
|
|
174
|
+
index: number,
|
|
175
|
+
key: string,
|
|
176
|
+
payload: UpdateItemValuePayload<unknown>,
|
|
177
|
+
): void => {
|
|
178
|
+
items.value[index] = { ...items.value[index], [key]: payload.value ?? '' };
|
|
179
|
+
emitChange(payload.isHydrationWrite === true);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const onStyleInput = (
|
|
183
|
+
index: number,
|
|
184
|
+
key: string,
|
|
185
|
+
payload: UpdateItemValuePayload<TextStyleDeclaration>,
|
|
186
|
+
): void => {
|
|
187
|
+
items.value[index] = { ...items.value[index], [key]: payload.value };
|
|
188
|
+
emitChange();
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const onReorder = (reordered: ListItem[]): void => {
|
|
192
|
+
items.value = [...reordered];
|
|
193
|
+
emitChange();
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const onDelete = (index: number): void => {
|
|
197
|
+
items.value = items.value.filter((_item, i) => i !== index);
|
|
198
|
+
emitChange();
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const onToggle = (index: number, isCollapsed: boolean): void => {
|
|
202
|
+
if (!isCollapsed && items.value[index]) {
|
|
203
|
+
contentSettingsStore.setActiveItem(props.content.id, items.value[index].id);
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// A new item takes the last item's styles, so it looks like its siblings.
|
|
208
|
+
const createItem = (): ListItem => {
|
|
209
|
+
const lastItem = items.value[items.value.length - 1];
|
|
210
|
+
const newItem: ListItem = { id: uuid.v4() };
|
|
211
|
+
|
|
212
|
+
props.itemFields.forEach((schema) => {
|
|
213
|
+
newItem[schema.key] = schema.defaultValue?.() ?? '';
|
|
214
|
+
|
|
215
|
+
if (schema.styleKey) {
|
|
216
|
+
newItem[schema.styleKey] = cloneDeep(
|
|
217
|
+
lastItem?.[schema.styleKey] ?? schema.defaultStyle?.() ?? {},
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
if (props.showRichTextEditor) {
|
|
223
|
+
const { externalEditor } = useExternalRichTextEditor();
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
switchRteItemFieldsToRTE(
|
|
227
|
+
props.content.type,
|
|
228
|
+
newItem,
|
|
229
|
+
externalEditor,
|
|
230
|
+
styleSettingsStore,
|
|
231
|
+
);
|
|
232
|
+
} finally {
|
|
233
|
+
externalEditor.destroy();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return newItem;
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const onAdd = async (): Promise<void> => {
|
|
241
|
+
if (isAtMax.value) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
items.value = [...items.value, createItem()];
|
|
246
|
+
emitChange();
|
|
247
|
+
|
|
248
|
+
await nextTick();
|
|
249
|
+
itemsFieldRef.value?.expand(items.value.length - 1);
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
// External writes (undo/redo, AI, an editor's toolbar style write) replace the local copy.
|
|
253
|
+
watch(
|
|
254
|
+
() => props.value,
|
|
255
|
+
(value) => {
|
|
256
|
+
if (JSON.stringify(value ?? []) !== JSON.stringify(items.value)) {
|
|
257
|
+
items.value = cloneDeep(value ?? []);
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
{ deep: true },
|
|
261
|
+
);
|
|
262
|
+
</script>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { defineComponent, type PropType } from 'vue';
|
|
2
2
|
import GenericTextInputMixin from '../GenericTextInput.mixin';
|
|
3
3
|
import type { TextItem } from '@brand-page';
|
|
4
|
+
import type { RteItemStyleTarget } from '../../models';
|
|
4
5
|
|
|
5
6
|
export default defineComponent({
|
|
6
7
|
mixins: [GenericTextInputMixin],
|
|
@@ -14,6 +15,11 @@ export default defineComponent({
|
|
|
14
15
|
type: Object as PropType<TextItem>,
|
|
15
16
|
required: true,
|
|
16
17
|
},
|
|
18
|
+
itemStyleTarget: {
|
|
19
|
+
type: Object as PropType<RteItemStyleTarget>,
|
|
20
|
+
required: false,
|
|
21
|
+
default: undefined,
|
|
22
|
+
},
|
|
17
23
|
},
|
|
18
24
|
methods: {
|
|
19
25
|
onForceUpdate() {
|
|
@@ -144,6 +144,13 @@ export interface UpdateMultipleItemValuesPayload<T extends object> {
|
|
|
144
144
|
isHydrationWrite?: boolean;
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
/** The list item whose style an item-level RTE writes (e.g. `items[2].questionStyle`). */
|
|
148
|
+
export interface RteItemStyleTarget {
|
|
149
|
+
itemsField: string;
|
|
150
|
+
index: number;
|
|
151
|
+
styleField: string;
|
|
152
|
+
}
|
|
153
|
+
|
|
147
154
|
export interface ValidateItemPayload {
|
|
148
155
|
id: string;
|
|
149
156
|
name: string;
|
|
@@ -528,4 +528,74 @@ describe('re-linking a text style clears the element colour override', () => {
|
|
|
528
528
|
expect(textStyle.textStyleType).toBe('titles');
|
|
529
529
|
expect(textStyle.color).toBeUndefined();
|
|
530
530
|
});
|
|
531
|
+
|
|
532
|
+
test("updateListItemTextStyles merges into one list item's own style only", () => {
|
|
533
|
+
const store = setupStore({
|
|
534
|
+
items: [
|
|
535
|
+
{
|
|
536
|
+
id: 'q1',
|
|
537
|
+
questionStyle: { textStyleType: 'headings', fontSize: '2em' },
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
id: 'q2',
|
|
541
|
+
questionStyle: { textStyleType: 'headings', color: '#ff0000' },
|
|
542
|
+
},
|
|
543
|
+
],
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
store.updateListItemTextStyles({
|
|
547
|
+
id: 'item-1',
|
|
548
|
+
name: 'question',
|
|
549
|
+
value: { textStyleType: undefined, color: undefined, fontWeight: 'bold' },
|
|
550
|
+
target: { itemsField: 'items', index: 1, styleField: 'questionStyle' },
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
const items = (
|
|
554
|
+
store.items[0].settings.content as unknown as {
|
|
555
|
+
items: { questionStyle: Record<string, unknown> }[];
|
|
556
|
+
}
|
|
557
|
+
).items;
|
|
558
|
+
|
|
559
|
+
expect(items[1].questionStyle).toMatchObject({ fontWeight: 'bold' });
|
|
560
|
+
expect(items[1].questionStyle.textStyleType).toBeUndefined();
|
|
561
|
+
expect(items[1].questionStyle.color).toBeUndefined();
|
|
562
|
+
expect(items[0].questionStyle).toEqual({
|
|
563
|
+
textStyleType: 'headings',
|
|
564
|
+
fontSize: '2em',
|
|
565
|
+
});
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
test('updateListItemTextStyles on a missing item is a no-op', () => {
|
|
569
|
+
const store = setupStore({ items: [] });
|
|
570
|
+
|
|
571
|
+
store.updateListItemTextStyles({
|
|
572
|
+
id: 'item-1',
|
|
573
|
+
name: 'question',
|
|
574
|
+
value: { fontWeight: 'bold' },
|
|
575
|
+
target: { itemsField: 'items', index: 3, styleField: 'questionStyle' },
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
expect(store.onContentChanged).not.toHaveBeenCalled();
|
|
579
|
+
});
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
describe('content settings store — active list item', () => {
|
|
583
|
+
beforeEach(() => {
|
|
584
|
+
setActivePinia(createPinia());
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
test('setActiveItem records the open item per content without touching content items', () => {
|
|
588
|
+
const store = useUiContentSettingsStore();
|
|
589
|
+
const itemsBefore = store.items;
|
|
590
|
+
|
|
591
|
+
store.setActiveItem('faq-1', 'q2');
|
|
592
|
+
store.setActiveItem('faq-2', 'q9');
|
|
593
|
+
store.setActiveItem('faq-1', 'q3');
|
|
594
|
+
|
|
595
|
+
expect(store.activeItemIdByContentId).toEqual({
|
|
596
|
+
'faq-1': 'q3',
|
|
597
|
+
'faq-2': 'q9',
|
|
598
|
+
});
|
|
599
|
+
expect(store.items).toBe(itemsBefore);
|
|
600
|
+
});
|
|
531
601
|
});
|
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
SetItemSettingsFieldErrorMessagePayload,
|
|
10
10
|
UpdateItemValuePayload,
|
|
11
11
|
UpdateMultipleItemValuesPayload,
|
|
12
|
+
RteItemStyleTarget,
|
|
12
13
|
ValidateItemPayload,
|
|
13
14
|
} from '../models';
|
|
14
15
|
import type { ColorReplacement, Content } from '@brand-page';
|
|
@@ -113,6 +114,8 @@ export interface ContentSettingsState {
|
|
|
113
114
|
selectedItemValue: string | null;
|
|
114
115
|
selectedItemIndex: number | null;
|
|
115
116
|
activeCategoryTab: ItemCategory;
|
|
117
|
+
/** Content id → the list item its settings panel has open; the maker canvas opens it too. UI-only, never saved. */
|
|
118
|
+
activeItemIdByContentId: Record<string, string | undefined>;
|
|
116
119
|
}
|
|
117
120
|
|
|
118
121
|
export const useUiContentSettingsStore = defineStore('uiContentSettings', {
|
|
@@ -127,8 +130,15 @@ export const useUiContentSettingsStore = defineStore('uiContentSettings', {
|
|
|
127
130
|
selectedItemIndex: null,
|
|
128
131
|
selectedItemValue: null,
|
|
129
132
|
activeCategoryTab: 'all',
|
|
133
|
+
activeItemIdByContentId: {},
|
|
130
134
|
}),
|
|
131
135
|
actions: {
|
|
136
|
+
setActiveItem(contentId: string, itemId: string | undefined): void {
|
|
137
|
+
this.activeItemIdByContentId = {
|
|
138
|
+
...this.activeItemIdByContentId,
|
|
139
|
+
[contentId]: itemId,
|
|
140
|
+
};
|
|
141
|
+
},
|
|
132
142
|
onContentChanged() {
|
|
133
143
|
const uiStore = useUiStore();
|
|
134
144
|
|
|
@@ -566,16 +576,40 @@ export const useUiContentSettingsStore = defineStore('uiContentSettings', {
|
|
|
566
576
|
index,
|
|
567
577
|
isHydrationWrite,
|
|
568
578
|
}: UpdateMultipleItemValuesPayload<any>): void {
|
|
579
|
+
this.updateListItemTextStyles({
|
|
580
|
+
id,
|
|
581
|
+
name,
|
|
582
|
+
value,
|
|
583
|
+
isHydrationWrite,
|
|
584
|
+
target: {
|
|
585
|
+
itemsField: 'textItems',
|
|
586
|
+
index: index!,
|
|
587
|
+
styleField: 'textStyle',
|
|
588
|
+
},
|
|
589
|
+
});
|
|
590
|
+
},
|
|
591
|
+
/** Merge into the style of one list item, e.g. `items[2].questionStyle`. */
|
|
592
|
+
updateListItemTextStyles({
|
|
593
|
+
id,
|
|
594
|
+
name,
|
|
595
|
+
value,
|
|
596
|
+
target,
|
|
597
|
+
isHydrationWrite,
|
|
598
|
+
}: Omit<UpdateMultipleItemValuesPayload<any>, 'index'> & {
|
|
599
|
+
target: RteItemStyleTarget;
|
|
600
|
+
}): void {
|
|
569
601
|
const item = this.items.find((i) => i.id === id);
|
|
602
|
+
const listItem =
|
|
603
|
+
item?.settings.content[target.itemsField]?.[target.index];
|
|
570
604
|
|
|
571
|
-
if (!item) {
|
|
605
|
+
if (!item || !listItem) {
|
|
572
606
|
return;
|
|
573
607
|
}
|
|
574
608
|
|
|
575
609
|
armHistoryMergeForHydrationWrite(isHydrationWrite);
|
|
576
610
|
|
|
577
|
-
|
|
578
|
-
|
|
611
|
+
listItem[target.styleField] = vueDeepMerge(
|
|
612
|
+
listItem[target.styleField],
|
|
579
613
|
value,
|
|
580
614
|
);
|
|
581
615
|
this.validateItem({ id, name });
|
|
@@ -397,3 +397,22 @@ describe('refreshDuplicatedMetadata — payment connection reset', () => {
|
|
|
397
397
|
expect(model.paymentConfig).toBeUndefined();
|
|
398
398
|
});
|
|
399
399
|
});
|
|
400
|
+
|
|
401
|
+
describe('refreshDuplicatedMetadata — FAQ section', () => {
|
|
402
|
+
it('copies an FAQ section unchanged (it holds no external ids)', () => {
|
|
403
|
+
const faq = {
|
|
404
|
+
id: 'faq-1',
|
|
405
|
+
type: 'faq-section',
|
|
406
|
+
title: 'FAQ',
|
|
407
|
+
items: [
|
|
408
|
+
{ id: 'q1', question: 'Q1', answer: 'A1' },
|
|
409
|
+
{ id: 'q2', question: 'Q2', answer: 'A2' },
|
|
410
|
+
],
|
|
411
|
+
questionStyle: { textStyleType: 'headings' },
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
const out = refreshDuplicatedMetadata(modelWith([structuredClone(faq)]));
|
|
415
|
+
|
|
416
|
+
expect(out.pages[0].contents[0]).toEqual(faq);
|
|
417
|
+
});
|
|
418
|
+
});
|
|
@@ -22,7 +22,6 @@ import { setLocaleAsync } from '@src/composables/useTranslate';
|
|
|
22
22
|
import { usePageNavigationStore } from '@src/shared/page-navigation/store/page-navigation-store';
|
|
23
23
|
import { useShopConfigStore } from '@shared/ecommerce-settings/store/shop-settings-store';
|
|
24
24
|
import { useEventTracker } from '@brand-page-maker/composables/useEventTracker';
|
|
25
|
-
import { useHistoryStore } from '@brand-page-maker/stores/history/history-store';
|
|
26
25
|
import { toPageModels } from '../utils';
|
|
27
26
|
|
|
28
27
|
vi.mock('../../../clients/brand-page-client', () => ({
|
|
@@ -678,53 +677,6 @@ describe('duplicatePage', () => {
|
|
|
678
677
|
).toBe(true);
|
|
679
678
|
});
|
|
680
679
|
|
|
681
|
-
test("deleting the copy's product leaves the source's product alone", async () => {
|
|
682
|
-
// The consequence the id rotation exists to prevent. `deleteProduct` filters
|
|
683
|
-
// the site-wide catalogue BY ID, so a copy that kept the source's ids would
|
|
684
|
-
// take the original's row down with it - silently, until the next save.
|
|
685
|
-
const { pageSettingsStore } = await initialiseAsync();
|
|
686
|
-
const shopConfigStore = useShopConfigStore();
|
|
687
|
-
|
|
688
|
-
const source = pageSettingsStore.addNewPage({ title: 'Shop' });
|
|
689
|
-
|
|
690
|
-
pageSettingsStore.setPageContents({
|
|
691
|
-
pageId: source.id,
|
|
692
|
-
contents: [
|
|
693
|
-
{
|
|
694
|
-
id: 'shop-1',
|
|
695
|
-
type: 'shop-section',
|
|
696
|
-
settings: {
|
|
697
|
-
content: {
|
|
698
|
-
id: 'shop-1',
|
|
699
|
-
type: 'shop-section',
|
|
700
|
-
products: [
|
|
701
|
-
{ id: 'product-1', name: 'Latte', variant: [], skus: [] },
|
|
702
|
-
],
|
|
703
|
-
shopPaymentConfig: { paymentConfig: { id: 'stripe-1' } },
|
|
704
|
-
},
|
|
705
|
-
},
|
|
706
|
-
} as unknown as ContentItem<Content>,
|
|
707
|
-
],
|
|
708
|
-
});
|
|
709
|
-
|
|
710
|
-
shopConfigStore.initialiseShopStore({
|
|
711
|
-
paymentConfig: { id: 'stripe-1' } as never,
|
|
712
|
-
pages: toPageModels(pageSettingsStore.pages) as never,
|
|
713
|
-
});
|
|
714
|
-
|
|
715
|
-
const copy = await pageSettingsStore.duplicatePage({ pageId: source.id });
|
|
716
|
-
|
|
717
|
-
const copyProductId = (
|
|
718
|
-
copy?.contents[0].settings.content as unknown as {
|
|
719
|
-
products: { id: string }[];
|
|
720
|
-
}
|
|
721
|
-
).products[0].id;
|
|
722
|
-
|
|
723
|
-
shopConfigStore.deleteProduct(copyProductId);
|
|
724
|
-
|
|
725
|
-
expect(shopConfigStore.products.map((p) => p.id)).toEqual(['product-1']);
|
|
726
|
-
});
|
|
727
|
-
|
|
728
680
|
test('adds the copy shop products to the site-wide catalogue', async () => {
|
|
729
681
|
// The catalogue is derived from the shop blocks and is what the save
|
|
730
682
|
// upserts. Without a re-derive the copy's fresh product ids never reach the
|
|
@@ -771,29 +723,6 @@ describe('duplicatePage', () => {
|
|
|
771
723
|
expect(shopConfigStore.paymentConfig?.id).toEqual('stripe-1');
|
|
772
724
|
});
|
|
773
725
|
|
|
774
|
-
test('undo removes both halves of a duplicate', async () => {
|
|
775
|
-
// duplicatePage writes to two stores - the page into page-settings, the
|
|
776
|
-
// route into page-navigation. A snapshot that caught only one would leave
|
|
777
|
-
// an orphan behind on undo.
|
|
778
|
-
const { pageSettingsStore, pageNavigationStore } = await initialiseAsync();
|
|
779
|
-
const historyStore = useHistoryStore();
|
|
780
|
-
|
|
781
|
-
const source = pageSettingsStore.addNewPage({ title: 'About' });
|
|
782
|
-
|
|
783
|
-
historyStore.captureBaseline();
|
|
784
|
-
|
|
785
|
-
const copy = await pageSettingsStore.duplicatePage({ pageId: source.id });
|
|
786
|
-
|
|
787
|
-
historyStore.capture({ source: 'manual_edit', actor: 'user' });
|
|
788
|
-
|
|
789
|
-
await historyStore.undo();
|
|
790
|
-
|
|
791
|
-
expect(pageSettingsStore.pages.map((p) => p.id)).not.toContain(copy?.id);
|
|
792
|
-
expect(pageNavigationStore.routes.some((r) => r.pageId === copy?.id)).toBe(
|
|
793
|
-
false,
|
|
794
|
-
);
|
|
795
|
-
});
|
|
796
|
-
|
|
797
726
|
test('is a no-op for an unknown page id', async () => {
|
|
798
727
|
const { pageSettingsStore } = await initialiseAsync();
|
|
799
728
|
|
|
@@ -169,3 +169,38 @@ describe('minifyBrandPageMetadata — palette resolution', () => {
|
|
|
169
169
|
expect(sut.styles.colors).toHaveLength(3);
|
|
170
170
|
});
|
|
171
171
|
});
|
|
172
|
+
|
|
173
|
+
describe('minifyBrandPageMetadata — FAQ is not AI-editable', () => {
|
|
174
|
+
test('an FAQ section reaches the AI as id + type + padding only', () => {
|
|
175
|
+
const model = new BrandPageModel({
|
|
176
|
+
...BRAND_SITE,
|
|
177
|
+
pages: [
|
|
178
|
+
{
|
|
179
|
+
id: 'p1',
|
|
180
|
+
title: 'Home',
|
|
181
|
+
contents: [
|
|
182
|
+
{
|
|
183
|
+
id: 'faq-1',
|
|
184
|
+
type: 'faq-section',
|
|
185
|
+
title: 'FAQ',
|
|
186
|
+
description: 'Answers',
|
|
187
|
+
displayStyle: 'faq-centered',
|
|
188
|
+
items: [{ id: 'q1', question: 'Q1', answer: 'A1' }],
|
|
189
|
+
topPadding: 32,
|
|
190
|
+
bottomPadding: 32,
|
|
191
|
+
},
|
|
192
|
+
],
|
|
193
|
+
} as any,
|
|
194
|
+
],
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const [faq] = (minifyBrandPageMetadata(model).pages[0] as any).contents;
|
|
198
|
+
|
|
199
|
+
expect(faq).toEqual({
|
|
200
|
+
id: 'faq-1',
|
|
201
|
+
type: 'faq-section',
|
|
202
|
+
topPadding: 32,
|
|
203
|
+
bottomPadding: 32,
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
});
|