@windstream/react-shared-components 0.2.54 → 0.2.56

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 (27) hide show
  1. package/dist/contentful/index.d.ts +200 -2
  2. package/dist/contentful/index.esm.js +5 -5
  3. package/dist/contentful/index.esm.js.map +1 -1
  4. package/dist/contentful/index.js +4 -4
  5. package/dist/contentful/index.js.map +1 -1
  6. package/dist/index.d.ts +2 -2
  7. package/dist/styles.css +1 -1
  8. package/package.json +1 -1
  9. package/src/contentful/blocks/dynamic-form/DynamicForm.stories.tsx +154 -0
  10. package/src/contentful/blocks/dynamic-form/conditional-fields.test.tsx +108 -0
  11. package/src/contentful/blocks/dynamic-form/conditional-fields.tsx +103 -0
  12. package/src/contentful/blocks/dynamic-form/constants.ts +28 -0
  13. package/src/contentful/blocks/dynamic-form/context.ts +35 -0
  14. package/src/contentful/blocks/dynamic-form/field-map.test.tsx +421 -0
  15. package/src/contentful/blocks/dynamic-form/field-map.tsx +486 -0
  16. package/src/contentful/blocks/dynamic-form/form-field.test.tsx +319 -0
  17. package/src/contentful/blocks/dynamic-form/form-field.tsx +224 -0
  18. package/src/contentful/blocks/dynamic-form/index.test.tsx +355 -0
  19. package/src/contentful/blocks/dynamic-form/index.tsx +552 -0
  20. package/src/contentful/blocks/dynamic-form/render.test.tsx +116 -0
  21. package/src/contentful/blocks/dynamic-form/render.tsx +172 -0
  22. package/src/contentful/blocks/dynamic-form/types.ts +211 -0
  23. package/src/contentful/blocks/dynamic-form/utils.test.ts +238 -0
  24. package/src/contentful/blocks/dynamic-form/utils.ts +255 -0
  25. package/src/contentful/blocks/primary-hero/index.test.tsx +28 -3
  26. package/src/contentful/blocks/primary-hero/index.tsx +5 -1
  27. package/src/contentful/index.ts +20 -0
@@ -0,0 +1,255 @@
1
+ import { constants } from "./constants";
2
+ import { DynamicFormFieldConfig, DynamicFormItem, FormAction } from "./types";
3
+
4
+ export const generateSubmissionId = (): string => {
5
+ if (
6
+ typeof crypto !== "undefined" &&
7
+ typeof crypto.randomUUID === "function"
8
+ ) {
9
+ return crypto.randomUUID();
10
+ }
11
+ // RFC4122-ish fallback
12
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, c => {
13
+ const r = (Math.random() * 16) | 0;
14
+ const v = c === "x" ? r : (r & 0x3) | 0x8;
15
+ return v.toString(16);
16
+ });
17
+ };
18
+
19
+ const isFormField = (
20
+ item: DynamicFormItem
21
+ ): item is DynamicFormItem & { itemType: "formField" } =>
22
+ item.itemType === "formField";
23
+
24
+ const isConditional = (
25
+ item: DynamicFormItem
26
+ ): item is DynamicFormItem & { itemType: "conditional" } =>
27
+ item.itemType === "conditional";
28
+
29
+ /**
30
+ * Expand conditional field groups: when the conditional trigger value is "yes"
31
+ * its nested fields participate in validation / payload building.
32
+ */
33
+ export const processFields = (
34
+ items: DynamicFormItem[],
35
+ formData: Record<string, any>
36
+ ): DynamicFormItem[] => {
37
+ const additional = items.filter(isConditional).flatMap(entry => {
38
+ const value = formData[entry.fieldId];
39
+ if (typeof value === "string" && value.toLowerCase() === "yes") {
40
+ return entry.formFieldsToShow;
41
+ }
42
+ return [];
43
+ });
44
+ return [...items, ...additional];
45
+ };
46
+
47
+ export const arrayForFieldValidations = (
48
+ items: DynamicFormItem[],
49
+ sftp = false,
50
+ formData: Record<string, any> = {}
51
+ ): any[] => {
52
+ const processed = processFields(items, formData);
53
+ return processed.filter(isFormField).map(field => {
54
+ if (sftp) {
55
+ return { [field.fieldId]: field.sftpFieldId };
56
+ }
57
+ const { fieldId, requiredField, fieldType, formSection, dataSource } =
58
+ field;
59
+ return { fieldId, requiredField, fieldType, formSection, dataSource };
60
+ });
61
+ };
62
+
63
+ export const buildSftpMap = (
64
+ items: DynamicFormItem[],
65
+ formData: Record<string, any> = {}
66
+ ): Record<string, string> => {
67
+ return arrayForFieldValidations(items, true, formData).reduce(
68
+ (acc, curr) => ({ ...acc, ...curr }),
69
+ {} as Record<string, string>
70
+ );
71
+ };
72
+
73
+ export const groupFormDataBySection = (
74
+ extractedArray: Array<any>,
75
+ formObject: Record<string, any>
76
+ ): Record<string, Array<Record<string, any>>> => {
77
+ const groupedData: Record<string, Array<Record<string, any>>> = {};
78
+ extractedArray.forEach(item => {
79
+ const { fieldId, formSection } = item;
80
+ if (!formSection || typeof formSection !== "string") return;
81
+ if (!groupedData[formSection]) {
82
+ groupedData[formSection] = [];
83
+ }
84
+ if (fieldId in formObject) {
85
+ groupedData[formSection].push({ [fieldId]: formObject[fieldId] });
86
+ }
87
+ });
88
+ return groupedData;
89
+ };
90
+
91
+ export const formPayloadCreation = (
92
+ items: DynamicFormItem[],
93
+ formData: Record<string, any>
94
+ ): Record<string, any> => {
95
+ const strippedFormData = Object.fromEntries(
96
+ Object.entries(formData).filter(
97
+ ([, value]) => value !== "" && value !== undefined
98
+ )
99
+ );
100
+ const extractedArray = arrayForFieldValidations(
101
+ items,
102
+ false,
103
+ strippedFormData
104
+ );
105
+
106
+ const hasMentionedFormSection = extractedArray.every(item => {
107
+ const formSection = item.formSection;
108
+ return typeof formSection === "string" && formSection !== "";
109
+ });
110
+
111
+ if (hasMentionedFormSection && extractedArray.length > 0) {
112
+ return { ...groupFormDataBySection(extractedArray, strippedFormData) };
113
+ }
114
+ return { form1: { ...strippedFormData } };
115
+ };
116
+
117
+ export const getInvalidFields = (
118
+ elementsArray: Array<any>,
119
+ formObject: Record<string, any>
120
+ ): string[] => {
121
+ return elementsArray
122
+ .filter(element => {
123
+ if (!element.requiredField) return false;
124
+ const value = formObject[element.fieldId];
125
+ if (element.fieldType === "Checkbox" && !element.dataSource) {
126
+ return value !== true;
127
+ }
128
+ return value === "" || value === null || value === undefined;
129
+ })
130
+ .map(element => element.fieldId);
131
+ };
132
+
133
+ /**
134
+ * Build the section-aware, width-aware layout groups used for rendering.
135
+ * Mirrors the original grouping logic but operates on normalized items.
136
+ */
137
+ export const groupFieldsForLayout = (items: DynamicFormItem[]): any[] => {
138
+ const groups: any[] = [];
139
+ let i = 0;
140
+
141
+ while (i < items.length) {
142
+ const current = items[i];
143
+
144
+ if (!isFormField(current)) {
145
+ groups.push({ type: "single", fields: [current] });
146
+ i++;
147
+ continue;
148
+ }
149
+
150
+ const currentValidationGroupId = current.validationGroupId;
151
+
152
+ if (currentValidationGroupId) {
153
+ const groupFields: DynamicFormItem[] = [];
154
+ let j = i;
155
+ while (j < items.length) {
156
+ const field = items[j];
157
+ if (
158
+ isFormField(field) &&
159
+ field.validationGroupId === currentValidationGroupId
160
+ ) {
161
+ groupFields.push(field);
162
+ j++;
163
+ } else {
164
+ break;
165
+ }
166
+ }
167
+
168
+ const validationGroup: any = {
169
+ type: "validationGroup",
170
+ validationGroupId: currentValidationGroupId,
171
+ validationNote: current.validationNote,
172
+ validationType: current.validationType,
173
+ subGroups: [] as any[],
174
+ };
175
+
176
+ let k = 0;
177
+ while (k < groupFields.length) {
178
+ const field = groupFields[k] as DynamicFormFieldConfig & {
179
+ itemType: string;
180
+ };
181
+ const fieldWidth = field.fieldWidth;
182
+
183
+ if (fieldWidth === false) {
184
+ const nextField = groupFields[k + 1] as
185
+ | (DynamicFormFieldConfig & { itemType: string })
186
+ | undefined;
187
+ if (nextField && nextField.fieldWidth === false) {
188
+ validationGroup.subGroups.push({
189
+ type: "double",
190
+ fields: [field, nextField],
191
+ });
192
+ k += 2;
193
+ } else {
194
+ validationGroup.subGroups.push({ type: "single", fields: [field] });
195
+ k++;
196
+ }
197
+ } else {
198
+ validationGroup.subGroups.push({ type: "single", fields: [field] });
199
+ k++;
200
+ }
201
+ }
202
+
203
+ groups.push(validationGroup);
204
+ i = j;
205
+ } else {
206
+ const currentFieldWidth = current.fieldWidth;
207
+ if (currentFieldWidth === false) {
208
+ const nextField = items[i + 1];
209
+ if (
210
+ nextField &&
211
+ isFormField(nextField) &&
212
+ nextField.fieldWidth === false &&
213
+ !nextField.validationGroupId
214
+ ) {
215
+ groups.push({ type: "double", fields: [current, nextField] });
216
+ i += 2;
217
+ } else {
218
+ groups.push({ type: "single", fields: [current] });
219
+ i++;
220
+ }
221
+ } else {
222
+ groups.push({ type: "single", fields: [current] });
223
+ i++;
224
+ }
225
+ }
226
+ }
227
+
228
+ return groups;
229
+ };
230
+
231
+ export const shouldShowPopup = (
232
+ items: DynamicFormItem[],
233
+ formData: Record<string, any>
234
+ ): boolean => {
235
+ return items.some(item => {
236
+ if (!isFormField(item)) return false;
237
+ const ds = item.dataSource;
238
+ if (!Array.isArray(ds)) return false;
239
+ const selectedValue = formData[item.fieldId];
240
+ const selectedOption = ds.find((opt: any) => opt.value === selectedValue);
241
+ return selectedOption?.popup === true;
242
+ });
243
+ };
244
+
245
+ export const filterActionsForEmailTemplate = (
246
+ actions: FormAction[],
247
+ emailTemplate?: string
248
+ ): FormAction[] => {
249
+ if (!emailTemplate) {
250
+ return actions.filter(a => a !== "email");
251
+ }
252
+ return actions;
253
+ };
254
+
255
+ export { constants, isFormField, isConditional };
@@ -1,7 +1,7 @@
1
1
  import React from "react";
2
2
  import { PrimaryHero } from "./index";
3
3
 
4
- import { render, screen } from "@testing-library/react";
4
+ import { fireEvent, render, screen } from "@testing-library/react";
5
5
 
6
6
  jest.mock("@shared/components/checklist", () => ({
7
7
  Checklist: ({ items }: any) => (
@@ -14,8 +14,14 @@ jest.mock("@shared/components/checklist", () => ({
14
14
  }));
15
15
 
16
16
  jest.mock("@shared/components/next-image", () => ({
17
- NextImage: ({ src, alt, className }: any) => (
18
- <img data-testid="next-image" src={src} alt={alt} className={className} />
17
+ NextImage: ({ src, alt, className, onClick }: any) => (
18
+ <img
19
+ data-testid="next-image"
20
+ src={src}
21
+ alt={alt}
22
+ className={className}
23
+ onClick={onClick}
24
+ />
19
25
  ),
20
26
  }));
21
27
 
@@ -347,6 +353,25 @@ describe("PrimaryHero", () => {
347
353
  expect(badges).toHaveLength(2);
348
354
  });
349
355
 
356
+ it("adds cursor class and triggers onImageClick for badge image", () => {
357
+ const onImageClick = jest.fn();
358
+
359
+ render(
360
+ <PrimaryHero
361
+ {...baseProps}
362
+ badgeImage="https://img.test/badge.png"
363
+ onImageClick={onImageClick}
364
+ />
365
+ );
366
+
367
+ const badges = screen.getAllByAltText("Badge");
368
+ expect(badges).toHaveLength(2);
369
+ badges.forEach((badge) => expect(badge).toHaveClass("cursor-pointer"));
370
+
371
+ fireEvent.click(badges[0]);
372
+ expect(onImageClick).toHaveBeenCalledTimes(1);
373
+ });
374
+
350
375
  it("does not render badge image when not provided", () => {
351
376
  render(<PrimaryHero {...baseProps} />);
352
377
  expect(screen.queryByAltText("Badge")).not.toBeInTheDocument();
@@ -310,7 +310,11 @@ export const PrimaryHero: React.FC<PrimaryHeroProps> = props => {
310
310
  <NextImage
311
311
  src={badgeImageUrl || ""}
312
312
  alt={"Badge"}
313
- className="absolute left-5 top-5 aspect-square w-26 object-cover object-center"
313
+ className={cx(
314
+ "absolute left-5 top-5 aspect-square w-26 object-cover object-center",
315
+ onImageClick && "cursor-pointer"
316
+ )}
317
+ onClick={onImageClick}
314
318
  width={104}
315
319
  height={104}
316
320
  quality={90}
@@ -102,6 +102,26 @@ export type { TypeCartRetentionBannerFields as CartRetentionBannerProps } from "
102
102
  export { IframeEmbed } from "./blocks/iframe-embed";
103
103
  export type { IframeEmbedProps } from "./blocks/iframe-embed/types";
104
104
 
105
+ export { DynamicForm } from "./blocks/dynamic-form";
106
+ export type {
107
+ DynamicFormProps,
108
+ DynamicFormItem,
109
+ DynamicFormFieldConfig,
110
+ DynamicFormFieldOption,
111
+ DynamicFormFieldType,
112
+ ConditionalFieldConfig,
113
+ DynamicFormPayload,
114
+ SubmitHandler,
115
+ SubmitContext,
116
+ FormAction,
117
+ FieldRenderProps,
118
+ ExistingDataSourceResolver,
119
+ } from "./blocks/dynamic-form/types";
120
+ export {
121
+ constants as dynamicFormConstants,
122
+ Regex as dynamicFormRegex,
123
+ } from "./blocks/dynamic-form/constants";
124
+
105
125
  // Contentful Rich Text Hooks & Utils
106
126
  export { toDocument } from "../utils/contentful/to-document";
107
127