@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.
- package/.babelrc +4 -0
- package/dist/index.cjs.js +7792 -0
- package/package.json +54 -0
- package/rollup.config.js +68 -0
- package/src/actions/featureBuilderStringsActions.js +88 -0
- package/src/actions/featureDefinitionsIndex.js +258 -0
- package/src/actions/formActions.js +311 -0
- package/src/actions/index.js +12 -0
- package/src/actions/listingActions.js +350 -0
- package/src/actions/wizardActions.js +240 -0
- package/src/components/ActivityCardExample.jsx +86 -0
- package/src/components/ActivityCardExample.module.css +130 -0
- package/src/components/BackgroundLoader.jsx +33 -0
- package/src/components/BackgroundLoader.module.css +46 -0
- package/src/components/BaseFieldConfig.jsx +305 -0
- package/src/components/BaseFieldConfig.module.css +42 -0
- package/src/components/CenteredContainer.jsx +29 -0
- package/src/components/CenteredContainer.module.css +171 -0
- package/src/components/DeleteConfirmationPopup.jsx +95 -0
- package/src/components/DeleteConfirmationPopup.module.css +12 -0
- package/src/components/ErrorBoundary.jsx +134 -0
- package/src/components/ErrorBoundary.module.css +77 -0
- package/src/components/ErrorMessage.jsx +85 -0
- package/src/components/ErrorMessage.module.css +116 -0
- package/src/components/ExampleDisplay.jsx +26 -0
- package/src/components/ExampleDisplay.module.css +3 -0
- package/src/components/FeatureBuilderSidebar.jsx +84 -0
- package/src/components/FeatureBuilderSuccessPopup.jsx +55 -0
- package/src/components/FeatureBuilderSuccessPopup.module.css +43 -0
- package/src/components/FeatureBuilderWelcomePopup.jsx +51 -0
- package/src/components/FeatureBuilderWelcomePopup.module.css +21 -0
- package/src/components/FeatureListingCard.jsx +104 -0
- package/src/components/FeatureListingCard.module.css +62 -0
- package/src/components/Fields.jsx +460 -0
- package/src/components/Fields.module.css +159 -0
- package/src/components/IconLoader.jsx +153 -0
- package/src/components/IconLoader.module.css +92 -0
- package/src/components/IconSelector.jsx +112 -0
- package/src/components/IconSelector.module.css +197 -0
- package/src/components/ListingEditor.jsx +406 -0
- package/src/components/ListingEditor.module.css +14 -0
- package/src/components/ListingSuccessPopup.jsx +52 -0
- package/src/components/LoadingScreen.jsx +54 -0
- package/src/components/LoadingScreen.module.css +103 -0
- package/src/components/LoadingState.jsx +40 -0
- package/src/components/LoadingState.module.css +18 -0
- package/src/components/PreviewFull.js +24 -0
- package/src/components/PreviewFull.module.css +11 -0
- package/src/components/PreviewGrid.js +14 -0
- package/src/components/PreviewWidget.js +27 -0
- package/src/components/PreviewWidget.module.css +15 -0
- package/src/components/SidebarLayout.jsx +292 -0
- package/src/components/SidebarLayout.module.css +145 -0
- package/src/components/SkeletonLoader.jsx +128 -0
- package/src/components/SkeletonLoader.module.css +295 -0
- package/src/components/SortButtonGroup.jsx +34 -0
- package/src/components/SortButtonGroup.module.css +51 -0
- package/src/components/ToastContainer.jsx +98 -0
- package/src/components/ToastContainer.module.css +156 -0
- package/src/components/ToggleSwitch.js +40 -0
- package/src/components/ToggleSwitch.module.css +48 -0
- package/src/components/TwoColumnInput.jsx +29 -0
- package/src/components/TwoColumnInput.module.css +32 -0
- package/src/components/ViewFull.js +139 -0
- package/src/components/ViewFull.module.css +71 -0
- package/src/components/ViewWidget.js +62 -0
- package/src/components/ViewWidget.module.css +28 -0
- package/src/components/iconCategories.js +135 -0
- package/src/components/iconImports.js +409 -0
- package/src/components/index.js +61 -0
- package/src/components/listing/FileListItem.jsx +86 -0
- package/src/components/listing/GalleryDisplay.jsx +331 -0
- package/src/components/listing/GalleryDisplay.module.css +309 -0
- package/src/components/listing/ListingCTAInput.jsx +82 -0
- package/src/components/listing/ListingDescriptionInput.jsx +73 -0
- package/src/components/listing/ListingField.jsx +101 -0
- package/src/components/listing/ListingField.module.css +106 -0
- package/src/components/listing/ListingFileInput.jsx +255 -0
- package/src/components/listing/ListingFileInput.module.css +192 -0
- package/src/components/listing/ListingForm.jsx +90 -0
- package/src/components/listing/ListingForm.module.css +38 -0
- package/src/components/listing/ListingGalleryInput.jsx +236 -0
- package/src/components/listing/ListingGalleryInput.module.css +131 -0
- package/src/components/listing/ListingImageInput.jsx +153 -0
- package/src/components/listing/ListingTextInput.jsx +72 -0
- package/src/feature.config.js +130 -0
- package/src/helper/index.js +135 -0
- package/src/hooks/useFeatureDefinitionLoader.js +62 -0
- package/src/images/full.png +0 -0
- package/src/images/fullNoTitle.png +0 -0
- package/src/images/previewWidget.png +0 -0
- package/src/images/widget.png +0 -0
- package/src/index.js +38 -0
- package/src/pages/CreateListingPage.jsx +49 -0
- package/src/pages/EditListingPage.jsx +58 -0
- package/src/reducers/featureBuilderReducer.js +744 -0
- package/src/screens/CreateListing.module.css +45 -0
- package/src/screens/Form.module.css +734 -0
- package/src/screens/FormFieldsStep.jsx +689 -0
- package/src/screens/FormLayoutStep.jsx +445 -0
- package/src/screens/FormOverviewStep.jsx +396 -0
- package/src/screens/ListingScreen.jsx +478 -0
- package/src/screens/ListingScreen.module.css +333 -0
- package/src/selectors/featureBuilderSelectors.js +529 -0
- package/src/types/index.js +91 -0
- package/src/utils/textUtils.js +89 -0
- package/src/validators/galleryValidators.js +345 -0
- package/src/values.config.a.js +49 -0
- package/src/values.config.b.js +49 -0
- package/src/values.config.c.js +49 -0
- package/src/values.config.d.js +49 -0
- package/src/values.config.js +49 -0
- package/src/webapi/featureDefinitionActions.js +0 -0
- package/src/webapi/featuresActions.js +90 -0
- package/src/webapi/helper.js +4 -0
- package/src/webapi/index.js +12 -0
- 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
|
+
};
|