@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.
Files changed (147) hide show
  1. package/lib/src/{CreatedOnBrandCrowd-de-de-7P2gNr-R.js → CreatedOnBrandCrowd-de-de-BRisMzFY.js} +1 -1
  2. package/lib/src/{CreatedOnBrandCrowd-en-us-Dl7BymW6.js → CreatedOnBrandCrowd-en-us-CDDsL9GX.js} +1 -1
  3. package/lib/src/{CreatedOnBrandCrowd-es-es-CO4FvWVw.js → CreatedOnBrandCrowd-es-es-BkZrEo4j.js} +1 -1
  4. package/lib/src/{CreatedOnBrandCrowd-fr-fr-DHNqakQu.js → CreatedOnBrandCrowd-fr-ca-sx_ury7R.js} +1 -1
  5. package/lib/src/{CreatedOnBrandCrowd-fr-ca-DHNqakQu.js → CreatedOnBrandCrowd-fr-fr-sx_ury7R.js} +1 -1
  6. package/lib/src/{CreatedOnBrandCrowd-pt-pt-CGUC-ZbP.js → CreatedOnBrandCrowd-pt-br-DCWiAfkU.js} +1 -1
  7. package/lib/src/{CreatedOnBrandCrowd-pt-br-CGUC-ZbP.js → CreatedOnBrandCrowd-pt-pt-DCWiAfkU.js} +1 -1
  8. package/lib/src/{CreatedOnDcom-de-de-Dvds-Vmy.js → CreatedOnDcom-de-de-C1S7f22L.js} +1 -1
  9. package/lib/src/{CreatedOnDcom-en-us-fd3C-8Tp.js → CreatedOnDcom-en-us-DgTlWaXE.js} +1 -1
  10. package/lib/src/{CreatedOnDcom-es-es-DTXZW35-.js → CreatedOnDcom-es-es-CxSQJe_V.js} +1 -1
  11. package/lib/src/{CreatedOnDcom-fr-ca-DRCYOKPs.js → CreatedOnDcom-fr-ca-Bbam2tt6.js} +1 -1
  12. package/lib/src/{CreatedOnDcom-fr-fr-DpIB8x2x.js → CreatedOnDcom-fr-fr-Cb0XDRPL.js} +1 -1
  13. package/lib/src/{CreatedOnDcom-pt-br-Esnxy8wX.js → CreatedOnDcom-pt-br-BjdX3-E3.js} +1 -1
  14. package/lib/src/{CreatedOnDcom-pt-pt-CaiVHFXR.js → CreatedOnDcom-pt-pt-BXpbfopB.js} +1 -1
  15. package/lib/src/brand-page/constants/misc.constants.d.ts +8 -0
  16. package/lib/src/brand-page/constants/misc.constants.js +7 -0
  17. package/lib/src/brand-page/constants/styles.constants.d.ts +2 -0
  18. package/lib/src/brand-page/constants/styles.constants.js +2 -0
  19. package/lib/src/brand-page/models/brand-page-context.model.d.ts +0 -2
  20. package/lib/src/brand-page/models/brand-page-context.model.js +0 -12
  21. package/lib/src/brand-page/models/content.model.d.ts +1 -0
  22. package/lib/src/brand-page/models/content.model.js +1 -0
  23. package/lib/src/brand-page/models/index.d.ts +19 -0
  24. package/lib/src/brand-page/utils/content-callback-util.d.ts +2 -1
  25. package/lib/src/brand-page/utils/content-callback-util.js +3 -0
  26. package/lib/src/brand-page/utils/model.util.d.ts +2 -1
  27. package/lib/src/brand-page/utils/model.util.js +3 -0
  28. package/lib/src/brand-page/utils/view-model.util.d.ts +2 -0
  29. package/lib/src/brand-page/utils/view-model.util.js +24 -0
  30. package/lib/src/brand-page/view-models.d.ts +9 -2
  31. package/lib/src/brand-page-maker/components/action-bar/ActionBar.mixin.d.ts +0 -1
  32. package/lib/src/brand-page-maker/components/action-bar/ActionBar.mixin.js +0 -9
  33. package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.d.ts +10 -42
  34. package/lib/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.d.ts +10 -42
  35. package/lib/src/brand-page-maker/components/preview/Preview.mixin.d.ts +0 -14
  36. package/lib/src/brand-page-maker/components/quick-link-bar/QuickLinkBar.mixin.d.ts +0 -42
  37. package/lib/src/brand-page-maker/components/seo-settings/SeoPageSettings.mixin.d.ts +0 -42
  38. package/lib/src/brand-page-maker/components/seo-settings/UrlSettings.mixin.d.ts +0 -42
  39. package/lib/src/brand-page-maker/components/seo-settings/UseExistingDomainModal.mixin.d.ts +0 -42
  40. package/lib/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.d.ts +0 -42
  41. package/lib/src/brand-page-maker/components/upgrade-to-premium-button/UpgradeToPremiumButton.mixin.d.ts +0 -42
  42. package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.d.ts +13 -0
  43. package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.js +64 -30
  44. package/lib/src/brand-page-maker/constants/config-settings-constant.d.ts +2 -1
  45. package/lib/src/brand-page-maker/constants/config-settings-constant.js +101 -2
  46. package/lib/src/brand-page-maker/convertors/faq-display-style.convertor.d.ts +3 -0
  47. package/lib/src/brand-page-maker/convertors/faq-display-style.convertor.js +47 -0
  48. package/lib/src/brand-page-maker/models/content-settings-configs.d.ts +22 -1
  49. package/lib/src/brand-page-maker/models/content-settings-configs.js +1 -0
  50. package/lib/src/brand-page-maker/models/content-settings.d.ts +4 -1
  51. package/lib/src/brand-page-maker/models/content-settings.js +45 -1
  52. package/lib/src/brand-page-maker/stores/brandpage-container/brand-page-container-store.d.ts +0 -42
  53. package/lib/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.js +1 -0
  54. package/lib/src/brand-page-maker/utils/apply-color-strategy.js +47 -17
  55. package/lib/src/brand-page-maker/utils/rich-text-editor.util.d.ts +6 -0
  56. package/lib/src/brand-page-maker/utils/rich-text-editor.util.js +24 -1
  57. package/lib/src/brand-page-maker/validators/field.validator.js +2 -0
  58. package/lib/src/bundled-translations-DY9YvuL-.js +179 -0
  59. package/lib/src/bundles/bundled-translations.json +20 -2
  60. package/lib/src/composables/useTranslate.d.ts +1 -1
  61. package/lib/src/composables/useTranslate.js +1 -1
  62. package/lib/src/css/library.brand-page-brandCrowd.min.css +143 -36
  63. package/lib/src/css/library.brand-page-brandPage.min.css +143 -36
  64. package/lib/src/css/library.brand-page-designCom.min.css +143 -36
  65. package/lib/src/css/library.brand-page-preview.min.css +143 -36
  66. package/lib/src/index.mjs +159 -158
  67. package/lib/src/{lib-V7aqfuiB.js → lib-DwpSAbLZ.js} +26889 -26178
  68. package/lib/src/metadata-builder/parsers/section-tool-arguments.d.ts +1 -1
  69. package/lib/src/shared/color-strategies/color-strategies.js +4 -0
  70. package/lib/src/shared/color-strategies/rte-field-pairs.d.ts +12 -0
  71. package/lib/src/shared/color-strategies/rte-field-pairs.js +10 -0
  72. package/lib/src/shared/content-settings/components/content-settings/ContentSettings.mixin.d.ts +10 -0
  73. package/lib/src/shared/content-settings/components/content-settings/ReplaceImageSettingsForContent.mixin.d.ts +11 -1
  74. package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItem.mixin.d.ts +10 -0
  75. package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.d.ts +10 -0
  76. package/lib/src/shared/content-settings/components/focused-content-item-settings/FocusedContentItemSettings.mixin.d.ts +10 -0
  77. package/lib/src/shared/content-settings/components/items-field/ItemsField.mixin.d.ts +14 -1
  78. package/lib/src/shared/content-settings/components/items-field/ItemsField.mixin.js +11 -1
  79. package/lib/src/shared/content-settings/components/payment-config-field/PaymentConfigField.mixin.d.ts +0 -42
  80. package/lib/src/shared/content-settings/components/textarea-field/TextareaField.mixin.d.ts +12 -0
  81. package/lib/src/shared/content-settings/components/textarea-field/TextareaField.mixin.js +5 -0
  82. package/lib/src/shared/content-settings/models.d.ts +6 -0
  83. package/lib/src/shared/content-settings/store/content-settings-store.d.ts +14 -1
  84. package/lib/src/shared/content-settings/store/content-settings-store.js +24 -2
  85. package/lib/src/shared/logo-settings/components/LogoSettings.mixin.d.ts +0 -42
  86. package/lib/src/shared/page-settings/components/page-settings/PageSettings.mixin.d.ts +10 -42
  87. package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.d.ts +10 -42
  88. package/lib/src/shared/publish-brand-page-with-state/PublishBrandPageModalWithState.mixin.d.ts +0 -56
  89. package/lib/src/shared/side-bar/components/SideBar.mixin.d.ts +0 -42
  90. package/lib/src/shared/style-settings/components/StyleSettings.mixin.d.ts +0 -42
  91. package/package.json +2 -2
  92. package/src/brand-page/components/faq-section/FaqSection.test.ts +292 -0
  93. package/src/brand-page/components/faq-section/FaqSection.vue +299 -0
  94. package/src/brand-page/components/page/PageComponent.vue +2 -0
  95. package/src/brand-page/components/section-heading/SectionHeading.vue +52 -0
  96. package/src/brand-page/components/text-wrapper/TextWrapper.test.ts +33 -0
  97. package/src/brand-page/constants/misc.constants.ts +12 -0
  98. package/src/brand-page/constants/styles.constants.ts +2 -0
  99. package/src/brand-page/models/brand-page-context.model.ts +0 -23
  100. package/src/brand-page/models/content.model.ts +1 -0
  101. package/src/brand-page/models/index.ts +21 -0
  102. package/src/brand-page/utils/content-callback-util.ts +5 -0
  103. package/src/brand-page/utils/model.util.ts +7 -0
  104. package/src/brand-page/utils/view-model.util.test.ts +96 -0
  105. package/src/brand-page/utils/view-model.util.ts +49 -0
  106. package/src/brand-page/view-models.ts +12 -1
  107. package/src/brand-page-maker/components/action-bar/ActionBar.mixin.ts +0 -10
  108. package/src/brand-page-maker/components/action-bar/ActionBarNavigation.test.ts +8 -49
  109. package/src/brand-page-maker/components/action-bar/ActionBarNavigation.vue +1 -1
  110. package/src/brand-page-maker/components/rich-text-editor/RichTextEditor.test.ts +165 -0
  111. package/src/brand-page-maker/components/rich-text-editor/RichTextEditor.vue +37 -49
  112. package/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.ts +87 -30
  113. package/src/brand-page-maker/constants/config-settings-constant.ts +108 -0
  114. package/src/brand-page-maker/constants/faq-section-config.test.ts +151 -0
  115. package/src/brand-page-maker/convertors/faq-display-style.convertor.ts +59 -0
  116. package/src/brand-page-maker/convertors/i18n/converter.json +4 -1
  117. package/src/brand-page-maker/models/content-settings-configs.ts +24 -0
  118. package/src/brand-page-maker/models/content-settings.ts +57 -0
  119. package/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.ts +2 -0
  120. package/src/brand-page-maker/utils/__snapshots__/apply-color-strategy.golden.test.ts.snap +4063 -895
  121. package/src/brand-page-maker/utils/apply-color-strategy.test.ts +104 -2
  122. package/src/brand-page-maker/utils/apply-color-strategy.ts +77 -19
  123. package/src/brand-page-maker/utils/rich-text-editor.util.test.ts +110 -0
  124. package/src/brand-page-maker/utils/rich-text-editor.util.ts +66 -0
  125. package/src/brand-page-maker/validators/field.validator.ts +2 -0
  126. package/src/bundles/bundled-translations.json +20 -2
  127. package/src/composables/useTranslate.ts +2 -1
  128. package/src/metadata-builder/builders/item-id-utils.test.ts +24 -0
  129. package/src/metadata-builder/parsers/section-tool-arguments.ts +2 -1
  130. package/src/metadata-builder/parsers/section-tool-map.test.ts +12 -0
  131. package/src/shared/color-strategies/color-strategies.ts +4 -0
  132. package/src/shared/color-strategies/rte-field-pairs.ts +23 -0
  133. package/src/shared/content-settings/components/content-settings/i18n/content-settings.json +17 -1
  134. package/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.vue +3 -0
  135. package/src/shared/content-settings/components/items-field/ItemsField.mixin.ts +11 -1
  136. package/src/shared/content-settings/components/items-field/ItemsField.vue +8 -2
  137. package/src/shared/content-settings/components/list-items-field/ListItemsField.test.ts +429 -0
  138. package/src/shared/content-settings/components/list-items-field/ListItemsField.vue +262 -0
  139. package/src/shared/content-settings/components/textarea-field/TextareaField.mixin.ts +6 -0
  140. package/src/shared/content-settings/components/textarea-field/TextareaField.vue +1 -0
  141. package/src/shared/content-settings/models.ts +7 -0
  142. package/src/shared/content-settings/store/content-settings-store.test.ts +70 -0
  143. package/src/shared/content-settings/store/content-settings-store.ts +37 -3
  144. package/src/shared/ecommerce-settings/util/refreshDuplicatedMetadata.test.ts +19 -0
  145. package/src/shared/page-settings/store/page-settings-store.test.ts +0 -71
  146. package/src/utils/minify-brand-page-metadata.test.ts +35 -0
  147. package/lib/src/bundled-translations-BYUfWBT3.js +0 -179
@@ -60,6 +60,7 @@
60
60
  :styles="content[styleName]"
61
61
  :label="field.label"
62
62
  :fields="field.fields"
63
+ :item-fields="field.itemFields"
63
64
  :options-convertor="field.optionsConvertor"
64
65
  :value-convertor="field.valueConvertor"
65
66
  :variant="variant"
@@ -114,6 +115,7 @@ import ShapeField from '../shape-field/ShapeField.vue';
114
115
  import ColorReplacementsField from '../color-replacements-field/ColorReplacementsField.vue';
115
116
  import TestimonialItemsField from '../testimonial-items-field/TestimonialItemsField.vue';
116
117
  import TextItemsField from '../text-items-field/TextItemsField.vue';
118
+ import ListItemsField from '../list-items-field/ListItemsField.vue';
117
119
  import SliderField from '../slider-field/SliderField.vue';
118
120
  import PageDropdownField from '../page-dropdown-field/PageDropdownField.vue';
119
121
  import PersonalisationOverrideField from '../personalisation-override-field/PersonalisationOverrideField.vue';
@@ -153,6 +155,7 @@ export default {
153
155
  'color-replacements-field': ColorReplacementsField,
154
156
  'testimonial-items-field': TestimonialItemsField,
155
157
  'text-items-field': TextItemsField,
158
+ 'list-items-field': ListItemsField,
156
159
  'product-config-option-field': ProductConfigOptionField,
157
160
  FieldSubHeader,
158
161
  Toggle,
@@ -39,8 +39,13 @@ export default defineComponent({
39
39
  required: false,
40
40
  default: true,
41
41
  },
42
+ deleteLabel: {
43
+ type: String,
44
+ required: false,
45
+ default: 'Delete payment option',
46
+ },
42
47
  },
43
- emits: ['items-change', 'items-delete', 'itemsEdit'],
48
+ emits: ['items-change', 'items-delete', 'itemsEdit', 'item-toggle'],
44
49
  data: () => ({
45
50
  itemStates: {} as Record<number, ItemFieldState>,
46
51
  }),
@@ -88,6 +93,11 @@ export default defineComponent({
88
93
  methods: {
89
94
  toggleCollapsed(index: number) {
90
95
  this.itemStates[index].isCollapsed = !this.itemStates[index].isCollapsed;
96
+ this.$emit('item-toggle', index, this.itemStates[index].isCollapsed);
97
+ },
98
+ expand(index: number): void {
99
+ this.itemStates[index] = { index, isCollapsed: false };
100
+ this.$emit('item-toggle', index, false);
91
101
  },
92
102
  onDelete(index: number): void {
93
103
  this.$emit('items-delete', index);
@@ -7,7 +7,10 @@
7
7
  :disabled="!isDraggable"
8
8
  >
9
9
  <template #item="{ element, index }">
10
- <li :key="`items-${element.item.id}-${index}`">
10
+ <li
11
+ :key="`items-${element.item.id}-${index}`"
12
+ data-test-items-field-row
13
+ >
11
14
  <div
12
15
  class="tw-mb-2 tw-bg-grayscale-200 tw-p-4 tw-rounded-md tw-border tw-border-grayscale-400 tw-border-solid"
13
16
  >
@@ -49,14 +52,17 @@
49
52
  </div>
50
53
  <button
51
54
  class="tw-bg-transparent tw-border-none tw-cursor-pointer tw-object-center tw-p-0 tw-text-grayscale-600 hover:tw-text-primary-500 tw-transition-colors tw-duration-300 tw-ease-out"
55
+ data-test-items-field-delete
52
56
  @click="onDelete(index)"
53
57
  >
54
- <span class="tw-sr-only">Delete payment option</span>
58
+ <span class="tw-sr-only">{{ deleteLabel }}</span>
55
59
  <Icon name="delete" size="sm" />
56
60
  </button>
57
61
  <div
58
62
  v-if="canCollapse"
59
63
  class="tw-bg-transparent tw-border-none tw-text-grayscale-600 tw-cursor-pointer tw-ml-2"
64
+ data-test-items-field-toggle
65
+ :data-test-collapsed="element.state.isCollapsed"
60
66
  @click="toggleCollapsed(index)"
61
67
  >
62
68
  <Icon
@@ -0,0 +1,429 @@
1
+ import { vi } from 'vitest';
2
+ import { defineComponent } from 'vue';
3
+ import { flushPromises, mount } from '@vue/test-utils';
4
+ import { createPinia, setActivePinia } from 'pinia';
5
+ import ListItemsField from './ListItemsField.vue';
6
+ import ItemsField from '../items-field/ItemsField.vue';
7
+ import { useUiContentSettingsStore } from '../../store/content-settings-store';
8
+ import { TextInput } from '@designcrowd/fe-shared-lib';
9
+ import type { ListItemFieldSchema } from '@brand-page-maker/models/content-settings-configs';
10
+
11
+ vi.mock('@src/composables/useTranslate', async (importOriginal) => ({
12
+ ...(await importOriginal<Record<string, unknown>>()),
13
+ contentSettingsTr: (key: string, values?: Record<string, unknown>) =>
14
+ values ? `${key}:${JSON.stringify(values)}` : key,
15
+ }));
16
+
17
+ const itemFields: ListItemFieldSchema[] = [
18
+ {
19
+ key: 'question',
20
+ kind: 'text',
21
+ title: 'Question',
22
+ defaultValue: () => 'New question?',
23
+ },
24
+ {
25
+ key: 'answer',
26
+ kind: 'textarea',
27
+ title: 'Answer',
28
+ defaultValue: () => 'Add the answer here.',
29
+ },
30
+ ];
31
+
32
+ const makeItems = (count: number) =>
33
+ Array.from({ length: count }, (_v, i) => ({
34
+ id: `q${i + 1}`,
35
+ question: `Question ${i + 1}`,
36
+ answer: `Answer ${i + 1}`,
37
+ }));
38
+
39
+ const mountField = (
40
+ count: number,
41
+ maxLength = 6,
42
+ extraProps: Record<string, unknown> = {},
43
+ ) =>
44
+ mount(ListItemsField, {
45
+ props: {
46
+ id: 'faq-1',
47
+ name: 'items',
48
+ value: makeItems(count),
49
+ content: { id: 'faq-1', type: 'faq-section' } as any,
50
+ itemFields,
51
+ maxLength,
52
+ ...extraProps,
53
+ },
54
+ global: {
55
+ stubs: { TextareaField: TextareaFieldStub, TextStyleField: true },
56
+ },
57
+ });
58
+
59
+ // Declares the editor's props so the test can read what the list binds.
60
+ const TextareaFieldStub = defineComponent({
61
+ name: 'TextareaField',
62
+ props: {
63
+ id: { type: String, default: '' },
64
+ name: { type: String, default: '' },
65
+ value: { type: [String, Object], default: '' },
66
+ styles: { type: Object, default: undefined },
67
+ itemStyleTarget: { type: Object, default: undefined },
68
+ maxLength: { type: Number, default: undefined },
69
+ showRichTextEditor: { type: Boolean, default: false },
70
+ },
71
+ emits: ['update:modelValue'],
72
+ template: '<div />',
73
+ });
74
+
75
+ const richItemFields: ListItemFieldSchema[] = itemFields.map((schema) => ({
76
+ ...schema,
77
+ styleKey: `${schema.key}Style`,
78
+ defaultStyle: () => ({ textStyleType: 'headings' as const }),
79
+ maxLength: schema.key === 'question' ? 200 : 1000,
80
+ }));
81
+
82
+ const styledItems = (count: number) =>
83
+ makeItems(count).map((item, i) => ({
84
+ ...item,
85
+ questionStyle: {
86
+ textStyleType: 'headings' as const,
87
+ fontSize: `${i + 1}em`,
88
+ },
89
+ answerStyle: { textStyleType: 'paragraphs' as const },
90
+ }));
91
+
92
+ const boldDoc = (text: string) => ({
93
+ type: 'doc',
94
+ content: [
95
+ {
96
+ type: 'paragraph',
97
+ content: [{ type: 'text', text, marks: [{ type: 'bold' }] }],
98
+ },
99
+ ],
100
+ });
101
+
102
+ const openFirstRow = async (wrapper: ReturnType<typeof mountField>) => {
103
+ (wrapper.findComponent(ItemsField).vm as any).expand(0);
104
+ await wrapper.vm.$nextTick();
105
+ };
106
+
107
+ const lastEmitted = (wrapper: ReturnType<typeof mountField>): any => {
108
+ const events = wrapper.emitted('update:modelValue') ?? [];
109
+
110
+ return events[events.length - 1]?.[0];
111
+ };
112
+
113
+ const addButton = (wrapper: ReturnType<typeof mountField>) =>
114
+ wrapper.find('[data-test-list-items-add]');
115
+
116
+ describe('ListItemsField', () => {
117
+ beforeEach(() => {
118
+ setActivePinia(createPinia());
119
+ });
120
+
121
+ test('adds a new item at the end with the schema defaults, and opens it', async () => {
122
+ const wrapper = mountField(5);
123
+
124
+ await addButton(wrapper).trigger('click');
125
+ await flushPromises();
126
+
127
+ const value = lastEmitted(wrapper).value;
128
+
129
+ expect(value).toHaveLength(6);
130
+ expect(value[5]).toMatchObject({
131
+ question: 'New question?',
132
+ answer: 'Add the answer here.',
133
+ });
134
+ expect(value[5].id).toEqual(expect.any(String));
135
+ expect(useUiContentSettingsStore().activeItemIdByContentId['faq-1']).toBe(
136
+ value[5].id,
137
+ );
138
+ });
139
+
140
+ test('at the cap the add button is disabled and says so', () => {
141
+ const wrapper = mountField(6);
142
+
143
+ expect(addButton(wrapper).attributes('disabled')).toBeDefined();
144
+ expect(addButton(wrapper).text()).toBe('listItemsMaxReached:{"max":6}');
145
+ });
146
+
147
+ test('below the cap the add button reads "add item"', () => {
148
+ expect(addButton(mountField(3)).text()).toBe('listItemsAddItem');
149
+ });
150
+
151
+ test('delete emits a new array without the item', async () => {
152
+ const wrapper = mountField(3);
153
+ const original = wrapper.props('value');
154
+
155
+ wrapper.findComponent(ItemsField).vm.$emit('items-delete', 1);
156
+ await flushPromises();
157
+
158
+ const value = lastEmitted(wrapper).value;
159
+
160
+ expect(value.map((item: any) => item.id)).toEqual(['q1', 'q3']);
161
+ expect(value).not.toBe(original);
162
+ });
163
+
164
+ test('reorder emits the new order as a new array', async () => {
165
+ const wrapper = mountField(3);
166
+ const reordered = [...makeItems(3)].reverse();
167
+
168
+ wrapper.findComponent(ItemsField).vm.$emit('items-change', reordered);
169
+ await flushPromises();
170
+
171
+ const value = lastEmitted(wrapper).value;
172
+
173
+ expect(value.map((item: any) => item.id)).toEqual(['q3', 'q2', 'q1']);
174
+ expect(value).not.toBe(reordered);
175
+ });
176
+
177
+ test('typing is debounced into one emit', async () => {
178
+ vi.useFakeTimers();
179
+ const wrapper = mountField(2);
180
+
181
+ (wrapper.findComponent(ItemsField).vm as any).expand(0);
182
+ await wrapper.vm.$nextTick();
183
+
184
+ const question = wrapper.findComponent(TextInput);
185
+
186
+ question.vm.$emit('update:modelValue', 'Wh');
187
+ question.vm.$emit('update:modelValue', 'What?');
188
+ expect(wrapper.emitted('update:modelValue')).toBeUndefined();
189
+
190
+ vi.advanceTimersByTime(300);
191
+ expect(wrapper.emitted('update:modelValue')).toHaveLength(1);
192
+ expect(lastEmitted(wrapper).value[0].question).toBe('What?');
193
+ vi.useRealTimers();
194
+ });
195
+
196
+ // Saved plain text is escaped, like every section text field; the renderer
197
+ // outputs it as HTML.
198
+ test('plain text is stored encoded, like a section text field', async () => {
199
+ vi.useFakeTimers();
200
+ const wrapper = mountField(1);
201
+
202
+ await openFirstRow(wrapper);
203
+ wrapper
204
+ .findComponent(TextInput)
205
+ .vm.$emit('update:modelValue', 'Press <Enter> & go');
206
+ vi.advanceTimersByTime(300);
207
+
208
+ expect(lastEmitted(wrapper).value[0].question).toBe(
209
+ 'Press &lt;Enter&gt; &amp; go',
210
+ );
211
+ vi.useRealTimers();
212
+ });
213
+
214
+ test('labels each row with its question', () => {
215
+ const labels = mountField(2)
216
+ .findAll('[data-test-list-item-label]')
217
+ .map((label) => label.text());
218
+
219
+ expect(labels).toEqual(['Question 1', 'Question 2']);
220
+ });
221
+
222
+ test('announces a generic delete label, not "payment option"', () => {
223
+ const html = mountField(1).html();
224
+
225
+ expect(html).toContain('listItemsDeleteLabel');
226
+ expect(html).not.toContain('Delete payment option');
227
+ });
228
+
229
+ test('opening a row marks it as the active item', async () => {
230
+ const wrapper = mountField(3);
231
+
232
+ wrapper.findComponent(ItemsField).vm.$emit('item-toggle', 2, false);
233
+ await flushPromises();
234
+
235
+ expect(useUiContentSettingsStore().activeItemIdByContentId['faq-1']).toBe(
236
+ 'q3',
237
+ );
238
+ });
239
+
240
+ describe('rich-text item fields', () => {
241
+ const mountRich = (showRichTextEditor: boolean, count = 2) =>
242
+ mountField(count, 6, {
243
+ itemFields: richItemFields,
244
+ showRichTextEditor,
245
+ value: styledItems(count),
246
+ });
247
+
248
+ test("use the section-title editor, bound to the item's own style", async () => {
249
+ const wrapper = mountRich(true);
250
+
251
+ (wrapper.findComponent(ItemsField).vm as any).expand(1);
252
+ await wrapper.vm.$nextTick();
253
+
254
+ const editors = wrapper.findAllComponents(TextareaFieldStub);
255
+
256
+ expect(
257
+ editors.map((e) => [
258
+ e.props('name'),
259
+ e.props('showRichTextEditor'),
260
+ e.props('maxLength'),
261
+ e.props('itemStyleTarget'),
262
+ ]),
263
+ ).toEqual([
264
+ [
265
+ 'question',
266
+ true,
267
+ 200,
268
+ { itemsField: 'items', index: 1, styleField: 'questionStyle' },
269
+ ],
270
+ [
271
+ 'answer',
272
+ true,
273
+ 1000,
274
+ { itemsField: 'items', index: 1, styleField: 'answerStyle' },
275
+ ],
276
+ ]);
277
+ expect(editors[0].props('styles')).toEqual(
278
+ styledItems(2)[1].questionStyle,
279
+ );
280
+ expect(wrapper.findComponent(TextInput).exists()).toBe(false);
281
+ });
282
+
283
+ test('without rich text: plain inputs plus a font-style control per styled text', async () => {
284
+ const wrapper = mountRich(false);
285
+
286
+ await openFirstRow(wrapper);
287
+
288
+ // The section fields' plain inputs: TextField for the question, a plain TextareaField for the answer.
289
+ expect(wrapper.findComponent(TextInput).exists()).toBe(true);
290
+ expect(
291
+ wrapper
292
+ .findAllComponents(TextareaFieldStub)
293
+ .map((e) => [e.props('name'), e.props('showRichTextEditor')]),
294
+ ).toEqual([['answer', false]]);
295
+ expect(
296
+ wrapper.findAllComponents({ name: 'TextStyleField' }),
297
+ ).toHaveLength(2);
298
+ });
299
+
300
+ test('an editor write replaces that field and emits at once', async () => {
301
+ const wrapper = mountRich(true);
302
+
303
+ await openFirstRow(wrapper);
304
+
305
+ wrapper
306
+ .findAllComponents(TextareaFieldStub)[1]
307
+ .vm.$emit('update:modelValue', {
308
+ id: 'faq-1',
309
+ name: 'answer',
310
+ value: boldDoc('Bold answer'),
311
+ });
312
+ await flushPromises();
313
+
314
+ const payload = lastEmitted(wrapper);
315
+
316
+ expect(payload.value[0].answer).toEqual(boldDoc('Bold answer'));
317
+ expect(payload.value[1]).toEqual(styledItems(2)[1]);
318
+ expect(payload.isHydrationWrite).toBeUndefined();
319
+ });
320
+
321
+ test("the editor's mount-time rewrite is passed on as a hydration write", async () => {
322
+ const wrapper = mountRich(true);
323
+
324
+ await openFirstRow(wrapper);
325
+
326
+ wrapper
327
+ .findAllComponents(TextareaFieldStub)[0]
328
+ .vm.$emit('update:modelValue', {
329
+ id: 'faq-1',
330
+ name: 'question',
331
+ value: boldDoc('Question 1'),
332
+ isHydrationWrite: true,
333
+ });
334
+ await flushPromises();
335
+
336
+ expect(lastEmitted(wrapper).isHydrationWrite).toBe(true);
337
+ });
338
+
339
+ test("a toolbar style write to the store reaches the item's editor", async () => {
340
+ const wrapper = mountRich(true);
341
+
342
+ await openFirstRow(wrapper);
343
+
344
+ const value = styledItems(2);
345
+
346
+ value[0].questionStyle = { ...value[0].questionStyle, fontSize: '3em' };
347
+ await wrapper.setProps({ value });
348
+
349
+ expect(
350
+ wrapper.findAllComponents(TextareaFieldStub)[0].props('styles'),
351
+ ).toMatchObject({ fontSize: '3em' });
352
+ });
353
+
354
+ test("a new item copies the last item's styles, as separate objects", async () => {
355
+ const wrapper = mountRich(false);
356
+
357
+ await addButton(wrapper).trigger('click');
358
+ await flushPromises();
359
+
360
+ const value = lastEmitted(wrapper).value;
361
+ const added = value[2];
362
+
363
+ expect(added.questionStyle).toEqual(value[1].questionStyle);
364
+ expect(added.questionStyle).not.toBe(value[1].questionStyle);
365
+ expect(added.answerStyle).toEqual(value[1].answerStyle);
366
+ expect(added.question).toBe('New question?');
367
+ });
368
+
369
+ test('the first item ever added takes the default style', async () => {
370
+ const wrapper = mountField(0, 6, {
371
+ itemFields: richItemFields,
372
+ showRichTextEditor: false,
373
+ value: [],
374
+ });
375
+
376
+ await addButton(wrapper).trigger('click');
377
+ await flushPromises();
378
+
379
+ expect(lastEmitted(wrapper).value[0].questionStyle).toEqual({
380
+ textStyleType: 'headings',
381
+ });
382
+ });
383
+
384
+ test('with rich text, a new item starts as a document styled by its linked role', async () => {
385
+ const wrapper = mountRich(true);
386
+
387
+ await addButton(wrapper).trigger('click');
388
+ await flushPromises();
389
+
390
+ const added = lastEmitted(wrapper).value[2];
391
+
392
+ expect(added.question.type).toBe('doc');
393
+ expect(JSON.stringify(added.question)).toContain('New question?');
394
+ });
395
+
396
+ // After a colour strategy the linked style carries its own colour; a new item
397
+ // must bake that colour, not the linked global one, or it stands out.
398
+ test("a new rich-text item bakes the copied style's own colour", async () => {
399
+ const value = styledItems(2).map((item) => ({
400
+ ...item,
401
+ questionStyle: { ...item.questionStyle, color: 'var(--palette-3)' },
402
+ }));
403
+ const wrapper = mountField(2, 6, {
404
+ itemFields: richItemFields,
405
+ showRichTextEditor: true,
406
+ value,
407
+ });
408
+
409
+ await addButton(wrapper).trigger('click');
410
+ await flushPromises();
411
+
412
+ expect(JSON.stringify(lastEmitted(wrapper).value[2].question)).toContain(
413
+ 'palette-3',
414
+ );
415
+ });
416
+
417
+ test('labels a row by the plain text of a rich-text question', () => {
418
+ const wrapper = mountField(1, 6, {
419
+ itemFields: richItemFields,
420
+ showRichTextEditor: true,
421
+ value: [{ ...styledItems(1)[0], question: boldDoc('Rich question') }],
422
+ });
423
+
424
+ expect(wrapper.find('[data-test-list-item-label]').text()).toBe(
425
+ 'Rich question',
426
+ );
427
+ });
428
+ });
429
+ });