@plusscommunities/pluss-feature-builder-web-d 1.0.2-beta.0

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 (117) hide show
  1. package/.babelrc +4 -0
  2. package/dist/index.cjs.js +7792 -0
  3. package/package.json +54 -0
  4. package/rollup.config.js +68 -0
  5. package/src/actions/featureBuilderStringsActions.js +88 -0
  6. package/src/actions/featureDefinitionsIndex.js +258 -0
  7. package/src/actions/formActions.js +311 -0
  8. package/src/actions/index.js +12 -0
  9. package/src/actions/listingActions.js +350 -0
  10. package/src/actions/wizardActions.js +240 -0
  11. package/src/components/ActivityCardExample.jsx +86 -0
  12. package/src/components/ActivityCardExample.module.css +130 -0
  13. package/src/components/BackgroundLoader.jsx +33 -0
  14. package/src/components/BackgroundLoader.module.css +46 -0
  15. package/src/components/BaseFieldConfig.jsx +305 -0
  16. package/src/components/BaseFieldConfig.module.css +42 -0
  17. package/src/components/CenteredContainer.jsx +29 -0
  18. package/src/components/CenteredContainer.module.css +171 -0
  19. package/src/components/DeleteConfirmationPopup.jsx +95 -0
  20. package/src/components/DeleteConfirmationPopup.module.css +12 -0
  21. package/src/components/ErrorBoundary.jsx +134 -0
  22. package/src/components/ErrorBoundary.module.css +77 -0
  23. package/src/components/ErrorMessage.jsx +85 -0
  24. package/src/components/ErrorMessage.module.css +116 -0
  25. package/src/components/ExampleDisplay.jsx +26 -0
  26. package/src/components/ExampleDisplay.module.css +3 -0
  27. package/src/components/FeatureBuilderSidebar.jsx +84 -0
  28. package/src/components/FeatureBuilderSuccessPopup.jsx +55 -0
  29. package/src/components/FeatureBuilderSuccessPopup.module.css +43 -0
  30. package/src/components/FeatureBuilderWelcomePopup.jsx +51 -0
  31. package/src/components/FeatureBuilderWelcomePopup.module.css +21 -0
  32. package/src/components/FeatureListingCard.jsx +104 -0
  33. package/src/components/FeatureListingCard.module.css +62 -0
  34. package/src/components/Fields.jsx +460 -0
  35. package/src/components/Fields.module.css +159 -0
  36. package/src/components/IconLoader.jsx +153 -0
  37. package/src/components/IconLoader.module.css +92 -0
  38. package/src/components/IconSelector.jsx +112 -0
  39. package/src/components/IconSelector.module.css +197 -0
  40. package/src/components/ListingEditor.jsx +406 -0
  41. package/src/components/ListingEditor.module.css +14 -0
  42. package/src/components/ListingSuccessPopup.jsx +52 -0
  43. package/src/components/LoadingScreen.jsx +54 -0
  44. package/src/components/LoadingScreen.module.css +103 -0
  45. package/src/components/LoadingState.jsx +40 -0
  46. package/src/components/LoadingState.module.css +18 -0
  47. package/src/components/PreviewFull.js +24 -0
  48. package/src/components/PreviewFull.module.css +11 -0
  49. package/src/components/PreviewGrid.js +14 -0
  50. package/src/components/PreviewWidget.js +27 -0
  51. package/src/components/PreviewWidget.module.css +15 -0
  52. package/src/components/SidebarLayout.jsx +292 -0
  53. package/src/components/SidebarLayout.module.css +145 -0
  54. package/src/components/SkeletonLoader.jsx +128 -0
  55. package/src/components/SkeletonLoader.module.css +295 -0
  56. package/src/components/SortButtonGroup.jsx +34 -0
  57. package/src/components/SortButtonGroup.module.css +51 -0
  58. package/src/components/ToastContainer.jsx +98 -0
  59. package/src/components/ToastContainer.module.css +156 -0
  60. package/src/components/ToggleSwitch.js +40 -0
  61. package/src/components/ToggleSwitch.module.css +48 -0
  62. package/src/components/TwoColumnInput.jsx +29 -0
  63. package/src/components/TwoColumnInput.module.css +32 -0
  64. package/src/components/ViewFull.js +139 -0
  65. package/src/components/ViewFull.module.css +71 -0
  66. package/src/components/ViewWidget.js +62 -0
  67. package/src/components/ViewWidget.module.css +28 -0
  68. package/src/components/iconCategories.js +135 -0
  69. package/src/components/iconImports.js +409 -0
  70. package/src/components/index.js +61 -0
  71. package/src/components/listing/FileListItem.jsx +86 -0
  72. package/src/components/listing/GalleryDisplay.jsx +331 -0
  73. package/src/components/listing/GalleryDisplay.module.css +309 -0
  74. package/src/components/listing/ListingCTAInput.jsx +82 -0
  75. package/src/components/listing/ListingDescriptionInput.jsx +73 -0
  76. package/src/components/listing/ListingField.jsx +101 -0
  77. package/src/components/listing/ListingField.module.css +106 -0
  78. package/src/components/listing/ListingFileInput.jsx +255 -0
  79. package/src/components/listing/ListingFileInput.module.css +192 -0
  80. package/src/components/listing/ListingForm.jsx +90 -0
  81. package/src/components/listing/ListingForm.module.css +38 -0
  82. package/src/components/listing/ListingGalleryInput.jsx +236 -0
  83. package/src/components/listing/ListingGalleryInput.module.css +131 -0
  84. package/src/components/listing/ListingImageInput.jsx +153 -0
  85. package/src/components/listing/ListingTextInput.jsx +72 -0
  86. package/src/feature.config.js +130 -0
  87. package/src/helper/index.js +135 -0
  88. package/src/hooks/useFeatureDefinitionLoader.js +62 -0
  89. package/src/images/full.png +0 -0
  90. package/src/images/fullNoTitle.png +0 -0
  91. package/src/images/previewWidget.png +0 -0
  92. package/src/images/widget.png +0 -0
  93. package/src/index.js +38 -0
  94. package/src/pages/CreateListingPage.jsx +49 -0
  95. package/src/pages/EditListingPage.jsx +58 -0
  96. package/src/reducers/featureBuilderReducer.js +744 -0
  97. package/src/screens/CreateListing.module.css +45 -0
  98. package/src/screens/Form.module.css +734 -0
  99. package/src/screens/FormFieldsStep.jsx +689 -0
  100. package/src/screens/FormLayoutStep.jsx +445 -0
  101. package/src/screens/FormOverviewStep.jsx +396 -0
  102. package/src/screens/ListingScreen.jsx +478 -0
  103. package/src/screens/ListingScreen.module.css +333 -0
  104. package/src/selectors/featureBuilderSelectors.js +529 -0
  105. package/src/types/index.js +91 -0
  106. package/src/utils/textUtils.js +89 -0
  107. package/src/validators/galleryValidators.js +345 -0
  108. package/src/values.config.a.js +49 -0
  109. package/src/values.config.b.js +49 -0
  110. package/src/values.config.c.js +49 -0
  111. package/src/values.config.d.js +49 -0
  112. package/src/values.config.js +49 -0
  113. package/src/webapi/featureDefinitionActions.js +0 -0
  114. package/src/webapi/featuresActions.js +90 -0
  115. package/src/webapi/helper.js +4 -0
  116. package/src/webapi/index.js +12 -0
  117. package/src/webapi/listingActions.js +176 -0
@@ -0,0 +1,689 @@
1
+ import React, { useEffect, useState } from "react";
2
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
3
+ import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
4
+ import { SidebarLayout } from "../components/SidebarLayout.jsx";
5
+ import { values } from "../values.config.js";
6
+ import { PlussCore } from "../feature.config";
7
+ import styles from "./Form.module.css";
8
+ import fieldStyles from "../components/Fields.module.css";
9
+ import { Field } from "../components/Fields.jsx";
10
+ import { iconImports } from "../components/iconImports";
11
+ import {
12
+ Text,
13
+ LoadingState,
14
+ SkeletonLoader,
15
+ DropdownInput,
16
+ Button,
17
+ ErrorBoundary,
18
+ Popup,
19
+ CenteredContainer,
20
+ } from "../components";
21
+
22
+ import ToastContainer from "../components/ToastContainer.jsx";
23
+
24
+ import { withRouter } from "react-router-dom";
25
+ import { useDispatch, useSelector } from "react-redux";
26
+ import {
27
+ selectFormField,
28
+ selectFormFields,
29
+ selectFormDisplayName,
30
+ selectIsCreateMode,
31
+ selectIsEditMode,
32
+ selectIsStepValid,
33
+ selectStepErrors,
34
+ selectCurrentStep,
35
+ selectFormIsSubmitting,
36
+ selectFormSubmitError,
37
+ selectFormSubmitSuccess,
38
+ selectFormIsInitial,
39
+ } from "../selectors/featureBuilderSelectors";
40
+ import { useFeatureDefinitionLoader } from "../hooks/useFeatureDefinitionLoader";
41
+ import {
42
+ addField,
43
+ deleteField,
44
+ updateFieldById as updateFieldValuesById,
45
+ setSummaryField,
46
+ submitForm,
47
+ clearFormSubmissionState,
48
+ setInitialValues,
49
+ } from "../actions/formActions";
50
+ import {
51
+ validateAndUpdateStep,
52
+ setCurrentStepAndSave,
53
+ } from "../actions/wizardActions";
54
+
55
+ /**
56
+ * Form Fields Step component for feature builder wizard
57
+ * Provides field management interface with add, edit, delete functionality
58
+ * Supports multiple field types (text, image, file, CTA, feature-image, description)
59
+ * Includes validation, error handling, and step navigation
60
+ *
61
+ * @param {Object} props - Component props
62
+ * @param {Object} props.history - React Router history object for navigation
63
+ * @returns {React.ReactElement} Form fields configuration interface
64
+ *
65
+ * @example
66
+ * <FormFieldsStep history={historyObject} />
67
+ */
68
+ const FormFieldsStepInner = (props) => {
69
+ const { history } = props;
70
+ const dispatch = useDispatch();
71
+ const auth = useSelector((state) => state.auth);
72
+ const fields = useSelector(selectFormFields);
73
+
74
+ // Get form initialization state
75
+ const isFormInitial = useSelector(selectFormIsInitial);
76
+
77
+ // Field selection popup state
78
+ const [showFieldSelector, setShowFieldSelector] = useState(false);
79
+ const [replacingFieldIndex, setReplacingFieldIndex] = useState(null);
80
+
81
+ // Toast state
82
+ const [toasts, setToasts] = React.useState([]);
83
+
84
+ // Available field types for card selection
85
+ // Note: We exclude "title" type since it's already included as base field
86
+ // UX-optimized order: text-first, media-second pattern for natural content creation workflow
87
+ const fieldTypes = [
88
+ {
89
+ Key: "description",
90
+ Title: "Description",
91
+ Description: "Add detailed text content",
92
+ UseCase: "Provide information, details, or descriptions",
93
+ Icon: "paragraph",
94
+ },
95
+ {
96
+ Key: "image",
97
+ Title: "Image",
98
+ Description: "Add photos or visual content",
99
+ UseCase: "Show facility photos, event pictures, product images",
100
+ Icon: "image",
101
+ },
102
+ {
103
+ Key: "gallery",
104
+ Title: "Gallery",
105
+ Description: "Add multiple photos in a gallery layout",
106
+ UseCase: "Create photo albums, event galleries, showcase multiple images",
107
+ Icon: "th",
108
+ },
109
+ {
110
+ Key: "file",
111
+ Title: "Files",
112
+ Description: "Share downloadable documents",
113
+ UseCase: "Upload menus, brochures, PDFs, or resources",
114
+ Icon: "paperclip",
115
+ },
116
+ {
117
+ Key: "cta",
118
+ Title: "Action Button",
119
+ Description: "Add clickable action button",
120
+ UseCase: "Link to reservations, bookings, or external sites",
121
+ Icon: "arrow-circle-right",
122
+ },
123
+ ];
124
+
125
+ // Use custom hook to handle definition loading
126
+ const { definition, definitionIsLoading, reloadDefinition } =
127
+ useFeatureDefinitionLoader();
128
+
129
+ // Get form state for display name
130
+ const formDisplayName = useSelector(selectFormDisplayName);
131
+
132
+ // Get wizard state
133
+ const isCreateMode = useSelector(selectIsCreateMode);
134
+ const isEditMode = useSelector(selectIsEditMode);
135
+
136
+ // Get validation state
137
+ const isStepValid = useSelector(selectIsStepValid("fields"));
138
+ const stepErrors = useSelector(selectStepErrors("fields"));
139
+ const showWarnings = !isStepValid && Object.keys(stepErrors).length > 0;
140
+
141
+ // Get submission state
142
+ const isSubmitting = useSelector(selectFormIsSubmitting);
143
+ const submitError = useSelector(selectFormSubmitError);
144
+ const submitSuccess = useSelector(selectFormSubmitSuccess);
145
+
146
+ // Toast management functions
147
+ const addToast = (type, message) => {
148
+ const id = Date.now();
149
+ setToasts((prev) => [...prev, { id, type, message, isVisible: true }]);
150
+ };
151
+
152
+ const removeToast = (id) => {
153
+ setToasts((prev) => prev.filter((toast) => toast.id !== id));
154
+ };
155
+
156
+ // Note: Removed automatic redirect after successful save in edit mode
157
+ // User should stay on the current step until they manually navigate
158
+
159
+ // Handle successful submission with optimistic update and redirect
160
+ React.useEffect(() => {
161
+ if (submitSuccess && !isSubmitting) {
162
+ if (isEditMode) {
163
+ addToast("success", "Changes saved");
164
+ dispatch(clearFormSubmissionState());
165
+ } else {
166
+ // In create mode, show success toast and redirect immediately
167
+ addToast("success", "Feature created successfully");
168
+
169
+ // Clear submission state and redirect to overview step
170
+ dispatch(clearFormSubmissionState());
171
+ setTimeout(() => {
172
+ history.push(values.routeFormOverviewStep);
173
+ }, 1000); // Brief delay to show toast
174
+ }
175
+ }
176
+ }, [submitSuccess, isEditMode, isSubmitting, dispatch, history]);
177
+
178
+ // Handle submit error
179
+ useEffect(() => {
180
+ if (submitError) {
181
+ addToast("error", "It didn't work. Please try again.");
182
+ setTimeout(() => {
183
+ window.location.reload();
184
+ }, 1000);
185
+ }
186
+ }, [submitError]);
187
+
188
+ // Scroll to top when validation errors appear
189
+ useEffect(() => {
190
+ if (showWarnings) {
191
+ // Scroll to top of form to show validation errors
192
+ window.scrollTo({ top: 0, behavior: "smooth" });
193
+ }
194
+ }, [showWarnings]);
195
+
196
+ // Error boundary handlers
197
+ const handleRefresh = () => {
198
+ // Refresh current step data
199
+ dispatch(validateAndUpdateStep("fields"));
200
+ };
201
+
202
+ const handleBack = () => {
203
+ // Go to overview step
204
+ history.push(values.routeFormOverviewStep);
205
+ };
206
+
207
+ useEffect(() => {
208
+ // Set current step when component mounts
209
+ dispatch(setCurrentStepAndSave("fields"));
210
+ }, [dispatch]);
211
+
212
+ // Add effect to handle definition loading and validation in edit mode
213
+ // ADD THIS EFFECT: Hydrate form data from definition on refresh
214
+ useEffect(() => {
215
+ if (definition && !definitionIsLoading && isFormInitial) {
216
+ dispatch(setInitialValues(definition));
217
+
218
+ // In edit mode, trigger validation after setting initial values
219
+ if (isEditMode) {
220
+ setTimeout(() => {
221
+ dispatch(validateAndUpdateStep("fields"));
222
+ }, 100);
223
+ }
224
+ }
225
+ }, [definition, definitionIsLoading, isFormInitial, isEditMode, dispatch]);
226
+
227
+ useEffect(() => {
228
+ // In edit mode, trigger validation when definition is available
229
+ // Note: The new effect above handles the data population, this handles re-validation
230
+ if (isEditMode && definition && !definitionIsLoading && !isFormInitial) {
231
+ // Only validate if form is NOT initial (meaning it has data)
232
+ setTimeout(() => {
233
+ dispatch(validateAndUpdateStep("fields"));
234
+ }, 100);
235
+ }
236
+ }, [definition, definitionIsLoading, isEditMode, isFormInitial, dispatch]);
237
+
238
+ function handleAddField(fieldType) {
239
+ dispatch(addField(fieldType));
240
+ setShowFieldSelector(false); // Close popup after adding field
241
+ }
242
+
243
+ function handleOpenFieldSelector() {
244
+ setReplacingFieldIndex(null); // Reset to add mode
245
+ setShowFieldSelector(true);
246
+ }
247
+
248
+ function handleCloseFieldSelector() {
249
+ setReplacingFieldIndex(null); // Reset replacement state
250
+ setShowFieldSelector(false);
251
+ }
252
+
253
+ function handleDeleteField(fieldId) {
254
+ dispatch(deleteField(fieldId));
255
+ }
256
+
257
+ function handleReplaceField(fieldIndex) {
258
+ // Store the field index to replace and open the field selector
259
+ setReplacingFieldIndex(fieldIndex);
260
+ setShowFieldSelector(true);
261
+ }
262
+
263
+ function handleAddReplacementField(fieldType) {
264
+ const fieldIndex = replacingFieldIndex;
265
+ if (fieldIndex !== null) {
266
+ // Delete the current field at this index
267
+ const currentField = allFields[fieldIndex];
268
+ if (currentField && !currentField.isMandatory) {
269
+ dispatch(deleteField(currentField.id));
270
+ }
271
+
272
+ // Add the new field
273
+ dispatch(addField(fieldType));
274
+
275
+ // Reset replacement state
276
+ setReplacingFieldIndex(null);
277
+ setShowFieldSelector(false);
278
+ }
279
+ }
280
+
281
+ function handleNext() {
282
+ // Validate before proceeding
283
+ const validationResult = dispatch(validateAndUpdateStep("fields"));
284
+
285
+ // If validation passes, navigate to next step
286
+ if (validationResult?.isValid) {
287
+ // Clear form submission state when changing steps
288
+ dispatch(clearFormSubmissionState());
289
+
290
+ if (isCreateMode) {
291
+ history.push(values.routeFormLayoutStep);
292
+ } else {
293
+ // In edit mode, navigate directly
294
+ history.push(values.routeFormLayoutStep);
295
+ }
296
+ }
297
+ // If validation fails, scroll to top to show error summary
298
+ else {
299
+ // Scroll to top of form to show validation errors
300
+ window.scrollTo({ top: 0, behavior: "smooth" });
301
+ }
302
+ }
303
+
304
+ function handlePrevious() {
305
+ // Clear form submission state when changing steps
306
+ dispatch(clearFormSubmissionState());
307
+
308
+ if (isCreateMode) {
309
+ history.push(values.routeFormOverviewStep);
310
+ } else {
311
+ // In edit mode, go back to overview screen
312
+ history.push(values.routeFormOverviewStep);
313
+ }
314
+ }
315
+
316
+ function handleSaveStep() {
317
+ // Validate before saving in edit mode
318
+ const validationResult = dispatch(validateAndUpdateStep("fields"));
319
+
320
+ // If validation passes, save the entire form
321
+ if (validationResult?.isValid) {
322
+ dispatch(submitForm());
323
+ }
324
+ // If validation fails, scroll to top to show error summary
325
+ else {
326
+ // Scroll to top of form to show validation errors
327
+ window.scrollTo({ top: 0, behavior: "smooth" });
328
+ }
329
+ }
330
+
331
+ // Get all fields in unified list (base + custom)
332
+ const allFields = fields || [];
333
+
334
+ // Create sorted copy for rendering
335
+ const sortedFields = allFields.slice().sort((a, b) => a.order - b.order);
336
+
337
+ // Filter description fields for summary demo
338
+ const descriptionFields = allFields.filter(
339
+ (field) => field.type === "description",
340
+ );
341
+
342
+ // Check for definition management permission
343
+ if (
344
+ !PlussCore.Session.validateAccess(
345
+ auth.site,
346
+ values.permissionFeatureBuilderDefinition,
347
+ auth,
348
+ )
349
+ ) {
350
+ return (
351
+ <div className="hub-wrapperContainer">
352
+ <div className="hub-contentWrapper">
353
+ <div className={styles.welcomeContainer}>
354
+ <div className={styles.welcomeHeader}>
355
+ <Text type="h1" className={styles.welcomeTitle}>
356
+ Access Restricted
357
+ </Text>
358
+ <Text type="body" className={styles.welcomeSubtitle}>
359
+ You don't have permission to manage feature definitions. Please
360
+ contact your administrator if you need access.
361
+ </Text>
362
+ </div>
363
+ </div>
364
+ </div>
365
+ </div>
366
+ );
367
+ }
368
+
369
+ return (
370
+ <ErrorBoundary
371
+ title="Unable to load fields configuration"
372
+ message="If you continue to experience issues with the fields configuration, please try refreshing the page or contact support."
373
+ onRetry={handleRefresh}
374
+ >
375
+ <SidebarLayout>
376
+ <div className={styles.formHeader}>
377
+ <Text
378
+ type="formTitleLarge"
379
+ className={` ${isEditMode ? styles.editMode : styles.createMode}`}
380
+ >
381
+ Configure fields
382
+ </Text>
383
+ </div>
384
+ <Text type="body" className="marginBottom-16">
385
+ Add fields to define the shape of your feature's listings.
386
+ </Text>
387
+
388
+ {/* Unified Fields Section */}
389
+ <div className={styles.section}>
390
+ {/* Show loading spinner while loading */}
391
+ {definitionIsLoading ? (
392
+ <div className={styles.fieldsLoadingContainer}>
393
+ <LoadingState message="Loading fields..." />
394
+ </div>
395
+ ) : (
396
+ sortedFields.map((field, fieldIndex) => (
397
+ <FormField
398
+ key={field.id}
399
+ id={field.id}
400
+ fieldIndex={fieldIndex}
401
+ handleDelete={
402
+ field.isMandatory ? null : () => handleDeleteField(field.id)
403
+ }
404
+ onReplaceField={
405
+ field.isMandatory
406
+ ? null
407
+ : () => handleReplaceField(fieldIndex)
408
+ }
409
+ showWarnings={showWarnings}
410
+ />
411
+ ))
412
+ )}
413
+
414
+ {/* Add Field Button at bottom */}
415
+ <div className={styles.addFieldContainer}>
416
+ <div className={styles.fieldNumberContainer}>
417
+ {/* Empty spacer to align with field numbers */}
418
+ </div>
419
+ <div className={styles.addFieldSection}>
420
+ <Button
421
+ buttonType="primary"
422
+ isActive
423
+ onClick={handleOpenFieldSelector}
424
+ leftIcon="plus"
425
+ aria-label="Add a new content field"
426
+ >
427
+ Add Content Field
428
+ </Button>
429
+ </div>
430
+ </div>
431
+ </div>
432
+
433
+ {/* Top-level validation message - positioned above action buttons */}
434
+ {!isStepValid && Object.keys(stepErrors).length > 0 && (
435
+ <div
436
+ className={styles.validationErrorMessage}
437
+ role="alert"
438
+ aria-live="polite"
439
+ >
440
+ {stepErrors.summary ? (
441
+ <>
442
+ {stepErrors.summary}
443
+ <br />
444
+ <span className={styles.helpNote}>
445
+ Only one description field can be marked as "Show as preview
446
+ text".
447
+ </span>
448
+ </>
449
+ ) : (
450
+ "The form has error, please review the fields above."
451
+ )}
452
+ </div>
453
+ )}
454
+
455
+ {/* Mode-aware navigation buttons */}
456
+ <div className={styles.navigation}>
457
+ {isCreateMode ? (
458
+ <>
459
+ <Button
460
+ buttonType="secondary"
461
+ isActive
462
+ onClick={handlePrevious}
463
+ leftIcon="arrow-left"
464
+ >
465
+ Previous step: Configure Fields
466
+ </Button>
467
+ <Button
468
+ buttonType="primary"
469
+ isActive
470
+ onClick={handleNext}
471
+ leftIcon="arrow-right"
472
+ >
473
+ Next step: Choose Layout
474
+ </Button>
475
+ </>
476
+ ) : (
477
+ <Button
478
+ buttonType="primary"
479
+ isActive
480
+ onClick={handleSaveStep}
481
+ disabled={isSubmitting}
482
+ leftIcon={isSubmitting ? "sync" : "save"}
483
+ loading={isSubmitting}
484
+ >
485
+ {isSubmitting ? "Saving..." : "Save"}
486
+ </Button>
487
+ )}
488
+ </div>
489
+ </SidebarLayout>
490
+
491
+ {/* Field Selector Popup */}
492
+ {showFieldSelector && (
493
+ <Popup
494
+ title={
495
+ replacingFieldIndex !== null ? "Replace Field" : "Add Content Field"
496
+ }
497
+ onClose={handleCloseFieldSelector}
498
+ buttons={[
499
+ {
500
+ text: "Cancel",
501
+ type: "secondary",
502
+ onClick: handleCloseFieldSelector,
503
+ },
504
+ ]}
505
+ hasPadding
506
+ width="1200px"
507
+ >
508
+ {/* Help Section - Visible for all users */}
509
+ <div className={styles.helpSection}>
510
+ <Text type="formTitleSmall" className={styles.helpText}>
511
+ <FontAwesomeIcon icon={faInfoCircle} /> Add fields to the
512
+ template.
513
+ <br />
514
+ These fields determine layout for every{" "}
515
+ {formDisplayName || "Feature"} visible to residents.
516
+ </Text>
517
+ </div>
518
+
519
+ <div className={styles.fieldTypeCards}>
520
+ {fieldTypes.map((fieldType) => (
521
+ <div
522
+ key={fieldType.Key}
523
+ className={styles.fieldTypeCard}
524
+ onClick={() =>
525
+ replacingFieldIndex !== null
526
+ ? handleAddReplacementField(fieldType.Key)
527
+ : handleAddField(fieldType.Key)
528
+ }
529
+ role="button"
530
+ tabIndex={0}
531
+ onKeyDown={(e) => {
532
+ if (e.key === "Enter" || e.key === " ") {
533
+ e.preventDefault();
534
+ if (replacingFieldIndex !== null) {
535
+ handleAddReplacementField(fieldType.Key);
536
+ } else {
537
+ handleAddField(fieldType.Key);
538
+ }
539
+ }
540
+ }}
541
+ >
542
+ <div className={styles.fieldTypeCardIcon}>
543
+ <FieldTypeIcon iconName={fieldType.Icon} />
544
+ </div>
545
+ <div className={styles.fieldTypeCardContent}>
546
+ <Text type="h5" className={styles.fieldTypeCardTitle}>
547
+ {fieldType.Title}
548
+ </Text>
549
+ <Text type="body" className={styles.fieldTypeCardDescription}>
550
+ {fieldType.Description}
551
+ </Text>
552
+ <Text type="help" className={styles.fieldTypeCardUseCase}>
553
+ <strong>Use case:</strong> {fieldType.UseCase}
554
+ </Text>
555
+ </div>
556
+ </div>
557
+ ))}
558
+ </div>
559
+ </Popup>
560
+ )}
561
+
562
+ {/* Toast Container for Notifications */}
563
+ <ToastContainer toasts={toasts} onDismiss={removeToast} />
564
+ </ErrorBoundary>
565
+ );
566
+ };
567
+
568
+ export const FormFieldsStep = withRouter(FormFieldsStepInner);
569
+
570
+ // FieldTypeIcon component for popup cards
571
+ const FieldTypeIcon = ({ iconName }) => {
572
+ const icon = iconImports[iconName];
573
+
574
+ return (
575
+ <span className={styles.fieldTypeCardIcon}>
576
+ <FontAwesomeIcon icon={icon} />
577
+ </span>
578
+ );
579
+ };
580
+
581
+ const FormField = (props) => {
582
+ const { id, handleDelete, showWarnings, fieldIndex, onReplaceField } = props;
583
+ const field = useField(id);
584
+ const stepErrors = useSelector(selectStepErrors("fields"));
585
+ const hasError = showWarnings && stepErrors && stepErrors[id];
586
+
587
+ // The Field component now handles its own styling, so we just render it directly
588
+ // Error states are passed through via props
589
+ return (
590
+ <Field
591
+ {...field}
592
+ fieldIndex={fieldIndex}
593
+ handleDelete={handleDelete}
594
+ onReplaceField={onReplaceField}
595
+ stepErrors={stepErrors}
596
+ showWarnings={showWarnings}
597
+ />
598
+ );
599
+ };
600
+
601
+ /**
602
+ * Custom hook for managing individual field state and operations
603
+ * Provides convenient methods to update field properties with Redux dispatch
604
+ *
605
+ * @param {string} id - Unique identifier of field to manage
606
+ * @returns {Object} Field object with setter methods for field properties
607
+ * @returns {Object} returns.field - Current field state from Redux store
608
+ * @returns {Function} returns.setLabel - Update field label
609
+ * @returns {Function} returns.toggleIsRequired - Toggle field required status
610
+ * @returns {Function} returns.setPlaceholder - Update field placeholder text
611
+ * @returns {Function} returns.setUrl - Update field URL
612
+ * @returns {Function} returns.setHelpText - Update field help text
613
+ * @returns {Function} returns.setAllowCaption - Update caption allowance setting
614
+ * @returns {Function} returns.toggleAllowCaption - Toggle caption allowance setting
615
+ * @returns {Function} returns.setUseAsSummary - Update use as summary setting for description fields
616
+ *
617
+ * @example
618
+ * const field = useField('field-123');
619
+ * field.setLabel('New Label');
620
+ * field.toggleIsRequired();
621
+ */
622
+ const useField = (id) => {
623
+ const field = useSelector(selectFormField(id));
624
+ const dispatch = useDispatch();
625
+ const { values } = field;
626
+
627
+ function setPlaceholder(value) {
628
+ const updatedField = { values: { ...values, placeholder: value } };
629
+ dispatch(updateFieldValuesById(id, updatedField));
630
+ }
631
+
632
+ function setLabel(value) {
633
+ const updatedField = { values: { ...values, label: value } };
634
+ dispatch(updateFieldValuesById(id, updatedField));
635
+ }
636
+
637
+ function toggleIsRequired() {
638
+ const updatedField = {
639
+ values: { ...values, isRequired: !values.isRequired },
640
+ };
641
+ dispatch(updateFieldValuesById(id, updatedField));
642
+ }
643
+
644
+ function setUrl(value) {
645
+ const updatedField = { values: { ...values, url: value } };
646
+ dispatch(updateFieldValuesById(id, updatedField));
647
+ }
648
+
649
+ function setHelpText(value) {
650
+ const updatedField = { values: { ...values, helpText: value } };
651
+ dispatch(updateFieldValuesById(id, updatedField));
652
+ }
653
+
654
+ function setAllowCaption(value) {
655
+ const updatedField = { values: { ...values, allowCaption: value } };
656
+
657
+ dispatch(updateFieldValuesById(id, updatedField));
658
+ }
659
+
660
+ function toggleAllowCaption() {
661
+ const updatedField = {
662
+ values: { ...values, allowCaption: !values.allowCaption },
663
+ };
664
+ dispatch(updateFieldValuesById(id, updatedField));
665
+ }
666
+
667
+ function setUseAsSummary(value) {
668
+ // When setting a field as summary, use the new action to ensure only one field is selected
669
+ if (value) {
670
+ dispatch(setSummaryField(id));
671
+ } else {
672
+ // When unsetting, use regular update to unset this specific field
673
+ const updatedField = { values: { ...values, useAsSummary: false } };
674
+ dispatch(updateFieldValuesById(id, updatedField));
675
+ }
676
+ }
677
+
678
+ return {
679
+ ...field,
680
+ setLabel,
681
+ toggleIsRequired,
682
+ setPlaceholder,
683
+ setUrl,
684
+ setHelpText,
685
+ setAllowCaption,
686
+ toggleAllowCaption,
687
+ setUseAsSummary,
688
+ };
689
+ };