@plusscommunities/pluss-feature-builder-web-b 1.0.2-beta.8 → 1.0.4-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.
@@ -4,439 +4,440 @@ import { values } from "../values.config.js";
4
4
  import { PlussCore } from "../feature.config";
5
5
  import styles from "./Form.module.css";
6
6
  import {
7
- Text,
8
- Button,
9
- LoadingState,
10
- IconLoader,
11
- ErrorBoundary,
12
- FeatureBuilderSuccessPopup,
7
+ Text,
8
+ Button,
9
+ LoadingState,
10
+ IconLoader,
11
+ ErrorBoundary,
12
+ FeatureBuilderSuccessPopup,
13
13
  } from "../components";
14
14
  import ToastContainer from "../components/ToastContainer.jsx";
15
15
  import { useDispatch, useSelector } from "react-redux";
16
16
  import {
17
- setLayoutType,
18
- setGridLayoutIcon,
19
- submitForm,
20
- clearFormSubmissionState,
21
- setInitialValues,
17
+ setLayoutType,
18
+ setGridLayoutIcon,
19
+ submitForm,
20
+ clearFormSubmissionState,
21
+ setInitialValues,
22
22
  } from "../actions/formActions";
23
23
  import {
24
- selectFormLayout,
25
- selectFormIcon,
26
- selectIsCreateMode,
27
- selectIsEditMode,
28
- selectIsStepValid,
29
- selectStepErrors,
30
- selectFormIsSubmitting,
31
- selectFormSubmitError,
32
- selectFormSubmitSuccess,
33
- selectFormDisplayName,
34
- selectFormTitle,
35
- selectDefinitionId,
36
- selectFormIsInitial,
24
+ selectFormLayout,
25
+ selectFormIcon,
26
+ selectIsCreateMode,
27
+ selectIsEditMode,
28
+ selectIsStepValid,
29
+ selectStepErrors,
30
+ selectFormIsSubmitting,
31
+ selectFormSubmitError,
32
+ selectFormSubmitSuccess,
33
+ selectFormDisplayName,
34
+ selectFormTitle,
35
+ selectDefinitionId,
36
+ selectFormIsInitial,
37
37
  } from "../selectors/featureBuilderSelectors";
38
38
  import { useFeatureDefinitionLoader } from "../hooks/useFeatureDefinitionLoader";
39
39
  import {
40
- validateAndUpdateStep,
41
- setCurrentStepAndSave,
40
+ validateAndUpdateStep,
41
+ setCurrentStepAndSave,
42
42
  } from "../actions/wizardActions";
43
43
  import { withRouter } from "react-router-dom";
44
44
 
45
45
  const FormLayoutStepInner = (props) => {
46
- const { history } = props;
47
- const dispatch = useDispatch();
48
- const auth = useSelector((state) => state.auth);
49
- const layout = useSelector(selectFormLayout);
50
- const displayName = useSelector(selectFormDisplayName);
51
- const featureFormName = useSelector(selectFormTitle);
52
- const overviewIcon = useSelector(selectFormIcon);
53
- const definitionId = useSelector(selectDefinitionId);
54
- const layoutType = layout?.type || "round";
55
- // Get wizard state
56
- const isCreateMode = useSelector(selectIsCreateMode);
57
- const isEditMode = useSelector(selectIsEditMode);
58
-
59
- // Use custom hook to handle definition loading
60
- const { definition, definitionIsLoading } = useFeatureDefinitionLoader();
61
-
62
- // Get form initialization state
63
- const isFormInitial = useSelector(selectFormIsInitial);
64
-
65
- // Get validation state
66
- const isStepValid = useSelector(selectIsStepValid("layout"));
67
- const stepErrors = useSelector(selectStepErrors("layout"));
68
-
69
- // Get submission state
70
- const isSubmitting = useSelector(selectFormIsSubmitting);
71
- const submitError = useSelector(selectFormSubmitError);
72
- const submitSuccess = useSelector(selectFormSubmitSuccess);
73
-
74
- // Toast state
75
- const [toasts, setToasts] = React.useState([]);
76
-
77
- // Success popup state
78
- const [showSuccessPopup, setShowSuccessPopup] = React.useState(false);
79
-
80
- // Toast management functions
81
- const addToast = (type, message) => {
82
- const id = Date.now();
83
- setToasts((prev) => [...prev, { id, type, message, isVisible: true }]);
84
- };
85
-
86
- const removeToast = (id) => {
87
- setToasts((prev) => prev.filter((toast) => toast.id !== id));
88
- };
89
-
90
- // Handle success popup close
91
- const handleSuccessPopupClose = () => {
92
- setShowSuccessPopup(false);
93
- dispatch(clearFormSubmissionState());
94
- history.push(values.routeFormOverviewStep);
95
- };
96
-
97
- // Handle success popup button click
98
- const handleSuccessPopupButtonClick = () => {
99
- setShowSuccessPopup(false);
100
- dispatch(clearFormSubmissionState());
101
- history.push(values.routeFormOverviewStep);
102
- };
103
-
104
- // Handle successful submission with popup and redirect
105
- useEffect(() => {
106
- if (submitSuccess && !isSubmitting) {
107
- if (isEditMode) {
108
- addToast("success", "Changes saved");
109
- dispatch(clearFormSubmissionState());
110
- } else {
111
- // In create mode, show success popup
112
- setShowSuccessPopup(true);
113
- }
114
- }
115
- }, [submitSuccess, isEditMode, isSubmitting, dispatch]);
116
-
117
- // Handle submit error
118
- useEffect(() => {
119
- if (submitError) {
120
- addToast("error", "It didn't work. Please try again.");
121
- setTimeout(() => {
122
- window.location.reload();
123
- }, 1000);
124
- }
125
- }, [submitError]);
126
-
127
- // Error boundary handlers
128
- const handleRefresh = () => {
129
- // Refresh current step data
130
- dispatch(validateAndUpdateStep("layout"));
131
- };
132
-
133
- const handleBack = () => {
134
- // Go to overview step
135
- history.push(values.routeFormOverviewStep);
136
- };
137
-
138
- const layoutOptions = [
139
- {
140
- value: "round",
141
- title: "Round Images",
142
- description: "Round photos in a grid",
143
- image:
144
- "https://pluss-prd-uploads.s3.ap-southeast-2.amazonaws.com/uploads/users/ap-southeast-2:b5bebf26-ee4c-c29c-88c8-ec859245e17b/public/b8156f584c92a0edbe13a8e05d/fblayoutround.png",
145
- },
146
- {
147
- value: "condensed",
148
- title: "Compact List",
149
- description: "Small photos in a list",
150
- image:
151
- "https://pluss-prd-uploads.s3.ap-southeast-2.amazonaws.com/uploads/users/ap-southeast-2:b5bebf26-ee4c-c29c-88c8-ec859245e17b/public/dfec30d342249a4073e5ffc6b8/fblayoutcompact.png",
152
- },
153
- {
154
- value: "square",
155
- title: "Square Images",
156
- description: "Square photos in a grid",
157
- image:
158
- "https://pluss-prd-uploads.s3.ap-southeast-2.amazonaws.com/uploads/users/ap-southeast-2:b5bebf26-ee4c-c29c-88c8-ec859245e17b/public/771e4626462a93041746a746c8/fblayoutsquare.png",
159
- },
160
- {
161
- value: "feature",
162
- title: "Large Photos",
163
- description: "Big photos with details",
164
- image:
165
- "https://pluss-prd-uploads.s3.ap-southeast-2.amazonaws.com/uploads/users/ap-southeast-2:b5bebf26-ee4c-c29c-88c8-ec859245e17b/public/f48acc614508ba246186b12845/fblayoutcardslarge.png",
166
- },
167
- ];
168
-
169
- useEffect(() => {
170
- // Set current step when component mounts
171
- dispatch(setCurrentStepAndSave("layout"));
172
- }, [dispatch]);
173
-
174
- // ADD THIS EFFECT: Hydrate form data from definition on refresh
175
- useEffect(() => {
176
- if (definition && !definitionIsLoading && isFormInitial) {
177
- dispatch(setInitialValues(definition));
178
-
179
- // In edit mode, trigger validation after setting initial values
180
- if (isEditMode) {
181
- setTimeout(() => {
182
- dispatch(validateAndUpdateStep("layout"));
183
- }, 100);
184
- }
185
- }
186
- }, [definition, definitionIsLoading, isFormInitial, isEditMode, dispatch]);
187
-
188
- // Add effect to handle definition loading and validation in edit mode
189
- useEffect(() => {
190
- // In edit mode, trigger validation when definition is available
191
- // Note: The new effect above handles data population, this handles re-validation
192
- if (isEditMode && definition && !definitionIsLoading && !isFormInitial) {
193
- // Only validate if form is NOT initial (meaning it has data)
194
- setTimeout(() => {
195
- dispatch(validateAndUpdateStep("layout"));
196
- }, 100);
197
- }
198
- }, [definition, definitionIsLoading, isEditMode, isFormInitial, dispatch]);
199
-
200
- function handleSelectLayout(layoutType) {
201
- dispatch(setLayoutType(layoutType));
202
- }
203
-
204
- function handleGridIconChange(iconUrl) {
205
- dispatch(setGridLayoutIcon(iconUrl));
206
- }
207
-
208
- function handleGridIconRemove() {
209
- // When custom grid icon is removed, set it to undefined to trigger fallback to overview icon
210
- dispatch(setGridLayoutIcon(undefined));
211
- }
212
-
213
- function handlePrevious() {
214
- // Clear form submission state when changing steps
215
- dispatch(clearFormSubmissionState());
216
-
217
- if (isCreateMode) {
218
- history.push(values.routeFormFieldsStep);
219
- } else {
220
- // In edit mode, go back to fields screen
221
- history.push(values.routeFormFieldsStep);
222
- }
223
- }
224
-
225
- function handleNext() {
226
- // Validate before proceeding
227
- const validationResult = dispatch(validateAndUpdateStep("layout"));
228
-
229
- // If validation passes, proceed with submission/navigation
230
- if (validationResult?.isValid) {
231
- if (isCreateMode) {
232
- // In create mode, submit form - success popup will be shown by useEffect
233
- dispatch(submitForm());
234
- } else {
235
- // In edit mode, just save changes
236
- dispatch(submitForm());
237
- }
238
- }
239
- // If validation fails, validation errors will be displayed
240
- }
241
-
242
- function handleSaveStep() {
243
- // Validate before saving in edit mode
244
- const validationResult = dispatch(validateAndUpdateStep("layout"));
245
-
246
- // If validation passes, proceed with saving
247
- if (validationResult?.isValid) {
248
- dispatch(submitForm());
249
- }
250
- }
251
-
252
- // Check for definition management permission
253
- if (
254
- !PlussCore.Session.validateAccess(
255
- auth.site,
256
- values.permissionFeatureBuilderDefinition,
257
- auth,
258
- )
259
- ) {
260
- return (
261
- <div className="hub-wrapperContainer">
262
- <div className="hub-contentWrapper">
263
- <div className={styles.welcomeContainer}>
264
- <div className={styles.welcomeHeader}>
265
- <Text type="h1" className={styles.welcomeTitle}>
266
- Access Restricted
267
- </Text>
268
- <Text type="body" className={styles.welcomeSubtitle}>
269
- You don't have permission to manage feature definitions. Please
270
- contact your administrator if you need access.
271
- </Text>
272
- </div>
273
- </div>
274
- </div>
275
- </div>
276
- );
277
- }
278
-
279
- return (
280
- <ErrorBoundary
281
- title="Unable to load layout design"
282
- message="If you continue to experience issues with the layout design, please try refreshing the page or contact support."
283
- onRetry={handleRefresh}
284
- >
285
- <SidebarLayout>
286
- <div className={styles.formLayoutHeader}>
287
- <Text
288
- type="formTitleLarge"
289
- className={`${isEditMode ? styles.editMode : styles.createMode}`}
290
- >
291
- {isEditMode ? "In-App Design" : "In-App Design"}
292
- </Text>
293
- </div>
294
- <Text type="body" className="paddingBottom-16">
295
- {isCreateMode
296
- ? "Pick how your feature looks. Choose a layout and add a grid icon if you want."
297
- : "Change how your feature looks. You can update the layout and grid icon anytime."}
298
- </Text>
299
-
300
- {/* Grid Icon Section */}
301
- <div className={styles.gridIconSection}>
302
- <Text type="formTitleSmall" className="">
303
- Grid Icon (Optional)
304
- </Text>
305
- <Text type="body" color="#6c757d" className="paddingBottom-16">
306
- Upload a grid icon for your feature. If you don't upload one, we'll
307
- use your main icon.
308
- </Text>
309
-
310
- <IconLoader
311
- value={layout?.gridIcon}
312
- defaultValue={overviewIcon}
313
- onChange={() => { }} // Disabled
314
- onRemove={() => { }} // Disabled
315
- featureId={definitionId || "new"}
316
- />
317
-
318
- <Text type="help" color="#6c757d" className="marginTop-16">
319
- We're working on bringing the ability to use custom grid icons
320
- </Text>
321
- </div>
322
-
323
- {/* Layout Selection Section */}
324
- <div className={styles.layoutSection}>
325
- <Text type="formTitleSmall">In-App Layout</Text>
326
- <Text type="body" color="#6c757d" className="paddingBottom-16">
327
- Select how your feature content will be displayed in the app
328
- </Text>
329
- <div className={styles.grid__four}>
330
- {definitionIsLoading ? (
331
- <div className={styles.gridIconLoading}>
332
- <LoadingState message="Loading layout options..." />
333
- </div>
334
- ) : (
335
- layoutOptions.map((option) => {
336
- const hasError =
337
- !isStepValid && stepErrors && stepErrors.layoutType;
338
- const isSelected = layoutType === option.value;
339
-
340
- return (
341
- <div
342
- key={option.value}
343
- className={`${styles.layoutOption} ${hasError ? styles.hasError : ""
344
- } ${isSelected ? styles.selected : ""}`}
345
- onClick={() => handleSelectLayout(option.value)}
346
- >
347
- <div className={styles.layoutOptionImage}>
348
- <img
349
- src={option.image}
350
- alt={option.title}
351
- className={styles.layoutOptionImg}
352
- />
353
- </div>
354
- <div className={styles.layoutOptionContent}>
355
- <div className={styles.layoutOptionTitle}>
356
- {option.title}
357
- </div>
358
- <div className={styles.layoutOptionDescription}>
359
- {option.description}
360
- </div>
361
- </div>
362
- {hasError && (
363
- <div className={styles.fieldError}>
364
- <span className={styles.errorIcon}>!</span>
365
- Layout selection is required
366
- </div>
367
- )}
368
- </div>
369
- );
370
- })
371
- )}
372
- </div>
373
- </div>
374
-
375
- {/* Top-level validation message - positioned above action buttons */}
376
- {!isStepValid && Object.keys(stepErrors).length > 0 && (
377
- <div
378
- className={styles.validationErrorMessage}
379
- role="alert"
380
- aria-live="polite"
381
- >
382
- {Object.keys(stepErrors).length}{" "}
383
- {Object.keys(stepErrors).length === 1
384
- ? "field needs"
385
- : "fields need"}{" "}
386
- attention before proceeding
387
- </div>
388
- )}
389
-
390
- {/* Mode-aware navigation buttons */}
391
- <div className={styles.navigation}>
392
- {isCreateMode ? (
393
- <>
394
- <Button
395
- buttonType="secondary"
396
- isActive
397
- onClick={handlePrevious}
398
- leftIcon="arrow-left"
399
- >
400
- Previous step: Choose Layout
401
- </Button>
402
- <Button
403
- buttonType="primary"
404
- isActive
405
- onClick={handleNext}
406
- leftIcon="check"
407
- disabled={isSubmitting}
408
- loading={isSubmitting}
409
- >
410
- {isSubmitting ? "Creating..." : "Complete Feature"}
411
- </Button>
412
- </>
413
- ) : (
414
- <Button
415
- buttonType="primary"
416
- isActive
417
- onClick={handleSaveStep}
418
- disabled={!isStepValid || isSubmitting}
419
- leftIcon={isSubmitting ? "sync" : "save"}
420
- loading={isSubmitting}
421
- >
422
- {isSubmitting ? "Saving..." : "Save"}
423
- </Button>
424
- )}
425
- </div>
426
- </SidebarLayout>
427
-
428
- {/* Success Popup */}
429
- <FeatureBuilderSuccessPopup
430
- isOpen={showSuccessPopup}
431
- onClose={handleSuccessPopupClose}
432
- featureName={featureFormName}
433
- displayName={displayName}
434
- />
435
-
436
- {/* Toast Container for Notifications */}
437
- <ToastContainer toasts={toasts} onDismiss={removeToast} />
438
- </ErrorBoundary>
439
- );
46
+ const { history } = props;
47
+ const dispatch = useDispatch();
48
+ const auth = useSelector((state) => state.auth);
49
+ const layout = useSelector(selectFormLayout);
50
+ const displayName = useSelector(selectFormDisplayName);
51
+ const featureFormName = useSelector(selectFormTitle);
52
+ const overviewIcon = useSelector(selectFormIcon);
53
+ const definitionId = useSelector(selectDefinitionId);
54
+ const layoutType = layout?.type || "round";
55
+ // Get wizard state
56
+ const isCreateMode = useSelector(selectIsCreateMode);
57
+ const isEditMode = useSelector(selectIsEditMode);
58
+
59
+ // Use custom hook to handle definition loading
60
+ const { definition, definitionIsLoading } = useFeatureDefinitionLoader();
61
+
62
+ // Get form initialization state
63
+ const isFormInitial = useSelector(selectFormIsInitial);
64
+
65
+ // Get validation state
66
+ const isStepValid = useSelector(selectIsStepValid("layout"));
67
+ const stepErrors = useSelector(selectStepErrors("layout"));
68
+
69
+ // Get submission state
70
+ const isSubmitting = useSelector(selectFormIsSubmitting);
71
+ const submitError = useSelector(selectFormSubmitError);
72
+ const submitSuccess = useSelector(selectFormSubmitSuccess);
73
+
74
+ // Toast state
75
+ const [toasts, setToasts] = React.useState([]);
76
+
77
+ // Success popup state
78
+ const [showSuccessPopup, setShowSuccessPopup] = React.useState(false);
79
+
80
+ // Toast management functions
81
+ const addToast = (type, message) => {
82
+ const id = Date.now();
83
+ setToasts((prev) => [...prev, { id, type, message, isVisible: true }]);
84
+ };
85
+
86
+ const removeToast = (id) => {
87
+ setToasts((prev) => prev.filter((toast) => toast.id !== id));
88
+ };
89
+
90
+ // Handle success popup close
91
+ const handleSuccessPopupClose = () => {
92
+ setShowSuccessPopup(false);
93
+ dispatch(clearFormSubmissionState());
94
+ history.push(values.routeFormOverviewStep);
95
+ };
96
+
97
+ // Handle success popup button click
98
+ const handleSuccessPopupButtonClick = () => {
99
+ setShowSuccessPopup(false);
100
+ dispatch(clearFormSubmissionState());
101
+ history.push(values.routeFormOverviewStep);
102
+ };
103
+
104
+ // Handle successful submission with popup and redirect
105
+ useEffect(() => {
106
+ if (submitSuccess && !isSubmitting) {
107
+ if (isEditMode) {
108
+ addToast("success", "Changes saved");
109
+ dispatch(clearFormSubmissionState());
110
+ } else {
111
+ // In create mode, show success popup
112
+ setShowSuccessPopup(true);
113
+ }
114
+ }
115
+ }, [submitSuccess, isEditMode, isSubmitting, dispatch]);
116
+
117
+ // Handle submit error
118
+ useEffect(() => {
119
+ if (submitError) {
120
+ addToast("error", "It didn't work. Please try again.");
121
+ setTimeout(() => {
122
+ window.location.reload();
123
+ }, 1000);
124
+ }
125
+ }, [submitError]);
126
+
127
+ // Error boundary handlers
128
+ const handleRefresh = () => {
129
+ // Refresh current step data
130
+ dispatch(validateAndUpdateStep("layout"));
131
+ };
132
+
133
+ const handleBack = () => {
134
+ // Go to overview step
135
+ history.push(values.routeFormOverviewStep);
136
+ };
137
+
138
+ const layoutOptions = [
139
+ {
140
+ value: "round",
141
+ title: "Round Images",
142
+ description: "Round photos in a grid",
143
+ image:
144
+ "https://pluss-prd-uploads.s3.ap-southeast-2.amazonaws.com/uploads/users/ap-southeast-2:b5bebf26-ee4c-c29c-88c8-ec859245e17b/public/b8156f584c92a0edbe13a8e05d/fblayoutround.png",
145
+ },
146
+ {
147
+ value: "condensed",
148
+ title: "Compact List",
149
+ description: "Small photos in a list",
150
+ image:
151
+ "https://pluss-prd-uploads.s3.ap-southeast-2.amazonaws.com/uploads/users/ap-southeast-2:b5bebf26-ee4c-c29c-88c8-ec859245e17b/public/dfec30d342249a4073e5ffc6b8/fblayoutcompact.png",
152
+ },
153
+ {
154
+ value: "square",
155
+ title: "Square Images",
156
+ description: "Square photos in a grid",
157
+ image:
158
+ "https://pluss-prd-uploads.s3.ap-southeast-2.amazonaws.com/uploads/users/ap-southeast-2:b5bebf26-ee4c-c29c-88c8-ec859245e17b/public/771e4626462a93041746a746c8/fblayoutsquare.png",
159
+ },
160
+ {
161
+ value: "feature",
162
+ title: "Large Photos",
163
+ description: "Big photos with details",
164
+ image:
165
+ "https://pluss-prd-uploads.s3.ap-southeast-2.amazonaws.com/uploads/users/ap-southeast-2:b5bebf26-ee4c-c29c-88c8-ec859245e17b/public/f48acc614508ba246186b12845/fblayoutcardslarge.png",
166
+ },
167
+ ];
168
+
169
+ useEffect(() => {
170
+ // Set current step when component mounts
171
+ dispatch(setCurrentStepAndSave("layout"));
172
+ }, [dispatch]);
173
+
174
+ // ADD THIS EFFECT: Hydrate form data from definition on refresh
175
+ useEffect(() => {
176
+ if (definition && !definitionIsLoading && isFormInitial) {
177
+ dispatch(setInitialValues(definition));
178
+
179
+ // In edit mode, trigger validation after setting initial values
180
+ if (isEditMode) {
181
+ setTimeout(() => {
182
+ dispatch(validateAndUpdateStep("layout"));
183
+ }, 100);
184
+ }
185
+ }
186
+ }, [definition, definitionIsLoading, isFormInitial, isEditMode, dispatch]);
187
+
188
+ // Add effect to handle definition loading and validation in edit mode
189
+ useEffect(() => {
190
+ // In edit mode, trigger validation when definition is available
191
+ // Note: The new effect above handles data population, this handles re-validation
192
+ if (isEditMode && definition && !definitionIsLoading && !isFormInitial) {
193
+ // Only validate if form is NOT initial (meaning it has data)
194
+ setTimeout(() => {
195
+ dispatch(validateAndUpdateStep("layout"));
196
+ }, 100);
197
+ }
198
+ }, [definition, definitionIsLoading, isEditMode, isFormInitial, dispatch]);
199
+
200
+ function handleSelectLayout(layoutType) {
201
+ dispatch(setLayoutType(layoutType));
202
+ }
203
+
204
+ function handleGridIconChange(iconUrl) {
205
+ dispatch(setGridLayoutIcon(iconUrl));
206
+ }
207
+
208
+ function handleGridIconRemove() {
209
+ // When custom grid icon is removed, set it to undefined to trigger fallback to overview icon
210
+ dispatch(setGridLayoutIcon(undefined));
211
+ }
212
+
213
+ function handlePrevious() {
214
+ // Clear form submission state when changing steps
215
+ dispatch(clearFormSubmissionState());
216
+
217
+ if (isCreateMode) {
218
+ history.push(values.routeFormFieldsStep);
219
+ } else {
220
+ // In edit mode, go back to fields screen
221
+ history.push(values.routeFormFieldsStep);
222
+ }
223
+ }
224
+
225
+ function handleNext() {
226
+ // Validate before proceeding
227
+ const validationResult = dispatch(validateAndUpdateStep("layout"));
228
+
229
+ // If validation passes, proceed with submission/navigation
230
+ if (validationResult?.isValid) {
231
+ if (isCreateMode) {
232
+ // In create mode, submit form - success popup will be shown by useEffect
233
+ dispatch(submitForm());
234
+ } else {
235
+ // In edit mode, just save changes
236
+ dispatch(submitForm());
237
+ }
238
+ }
239
+ // If validation fails, validation errors will be displayed
240
+ }
241
+
242
+ function handleSaveStep() {
243
+ // Validate before saving in edit mode
244
+ const validationResult = dispatch(validateAndUpdateStep("layout"));
245
+
246
+ // If validation passes, proceed with saving
247
+ if (validationResult?.isValid) {
248
+ dispatch(submitForm());
249
+ }
250
+ }
251
+
252
+ // Check for definition management permission
253
+ if (
254
+ !PlussCore.Session.validateAccess(
255
+ auth.site,
256
+ values.permissionFeatureBuilderDefinition,
257
+ auth,
258
+ )
259
+ ) {
260
+ return (
261
+ <div className="hub-wrapperContainer">
262
+ <div className="hub-contentWrapper">
263
+ <div className={styles.welcomeContainer}>
264
+ <div className={styles.welcomeHeader}>
265
+ <Text type="h1" className={styles.welcomeTitle}>
266
+ Access Restricted
267
+ </Text>
268
+ <Text type="body" className={styles.welcomeSubtitle}>
269
+ You don't have permission to manage feature definitions. Please
270
+ contact your administrator if you need access.
271
+ </Text>
272
+ </div>
273
+ </div>
274
+ </div>
275
+ </div>
276
+ );
277
+ }
278
+
279
+ return (
280
+ <ErrorBoundary
281
+ title="Unable to load layout design"
282
+ message="If you continue to experience issues with the layout design, please try refreshing the page or contact support."
283
+ onRetry={handleRefresh}
284
+ >
285
+ <SidebarLayout>
286
+ <div className={styles.formLayoutHeader}>
287
+ <Text
288
+ type="formTitleLarge"
289
+ className={`${isEditMode ? styles.editMode : styles.createMode}`}
290
+ >
291
+ {isEditMode ? "In-App Design" : "In-App Design"}
292
+ </Text>
293
+ </div>
294
+ <Text type="body" className="paddingBottom-16">
295
+ {isCreateMode
296
+ ? "Pick how your feature looks. Choose a layout and add a grid icon if you want."
297
+ : "Change how your feature looks. You can update the layout and grid icon anytime."}
298
+ </Text>
299
+
300
+ {/* Grid Icon Section */}
301
+ <div className={styles.gridIconSection}>
302
+ <Text type="formTitleSmall" className="">
303
+ Grid Icon (Optional)
304
+ </Text>
305
+ <Text type="body" color="#6c757d" className="paddingBottom-16">
306
+ Upload a grid icon for your feature. If you don't upload one, we'll
307
+ use your main icon.
308
+ </Text>
309
+
310
+ <IconLoader
311
+ value={layout?.gridIcon}
312
+ defaultValue={overviewIcon}
313
+ onChange={() => {}} // Disabled
314
+ onRemove={() => {}} // Disabled
315
+ featureId={definitionId || "new"}
316
+ />
317
+
318
+ <Text type="help" color="#6c757d" className="marginTop-16">
319
+ We're working on bringing the ability to use custom grid icons
320
+ </Text>
321
+ </div>
322
+
323
+ {/* Layout Selection Section */}
324
+ <div className={styles.layoutSection}>
325
+ <Text type="formTitleSmall">In-App Layout</Text>
326
+ <Text type="body" color="#6c757d" className="paddingBottom-16">
327
+ Select how your feature content will be displayed in the app
328
+ </Text>
329
+ <div className={styles.grid__four}>
330
+ {definitionIsLoading ? (
331
+ <div className={styles.gridIconLoading}>
332
+ <LoadingState message="Loading layout options..." />
333
+ </div>
334
+ ) : (
335
+ layoutOptions.map((option) => {
336
+ const hasError =
337
+ !isStepValid && stepErrors && stepErrors.layoutType;
338
+ const isSelected = layoutType === option.value;
339
+
340
+ return (
341
+ <div
342
+ key={option.value}
343
+ className={`${styles.layoutOption} ${
344
+ hasError ? styles.hasError : ""
345
+ } ${isSelected ? styles.selected : ""}`}
346
+ onClick={() => handleSelectLayout(option.value)}
347
+ >
348
+ <div className={styles.layoutOptionImage}>
349
+ <img
350
+ src={option.image}
351
+ alt={option.title}
352
+ className={styles.layoutOptionImg}
353
+ />
354
+ </div>
355
+ <div className={styles.layoutOptionContent}>
356
+ <div className={styles.layoutOptionTitle}>
357
+ {option.title}
358
+ </div>
359
+ <div className={styles.layoutOptionDescription}>
360
+ {option.description}
361
+ </div>
362
+ </div>
363
+ {hasError && (
364
+ <div className={styles.fieldError}>
365
+ <span className={styles.errorIcon}>!</span>
366
+ Layout selection is required
367
+ </div>
368
+ )}
369
+ </div>
370
+ );
371
+ })
372
+ )}
373
+ </div>
374
+ </div>
375
+
376
+ {/* Top-level validation message - positioned above action buttons */}
377
+ {!isStepValid && Object.keys(stepErrors).length > 0 && (
378
+ <div
379
+ className={styles.validationErrorMessage}
380
+ role="alert"
381
+ aria-live="polite"
382
+ >
383
+ {Object.keys(stepErrors).length}{" "}
384
+ {Object.keys(stepErrors).length === 1
385
+ ? "field needs"
386
+ : "fields need"}{" "}
387
+ attention before proceeding
388
+ </div>
389
+ )}
390
+
391
+ {/* Mode-aware navigation buttons */}
392
+ <div className={styles.navigation}>
393
+ {isCreateMode ? (
394
+ <>
395
+ <Button
396
+ buttonType="secondary"
397
+ isActive
398
+ onClick={handlePrevious}
399
+ leftIcon="arrow-left"
400
+ >
401
+ Previous step: Choose Layout
402
+ </Button>
403
+ <Button
404
+ buttonType="primary"
405
+ isActive
406
+ onClick={handleNext}
407
+ leftIcon="check"
408
+ disabled={isSubmitting}
409
+ loading={isSubmitting}
410
+ >
411
+ {isSubmitting ? "Creating..." : "Complete Feature"}
412
+ </Button>
413
+ </>
414
+ ) : (
415
+ <Button
416
+ buttonType="primary"
417
+ isActive
418
+ onClick={handleSaveStep}
419
+ disabled={!isStepValid || isSubmitting}
420
+ leftIcon={isSubmitting ? "sync" : "save"}
421
+ loading={isSubmitting}
422
+ >
423
+ {isSubmitting ? "Saving..." : "Save"}
424
+ </Button>
425
+ )}
426
+ </div>
427
+ </SidebarLayout>
428
+
429
+ {/* Success Popup */}
430
+ <FeatureBuilderSuccessPopup
431
+ isOpen={showSuccessPopup}
432
+ onClose={handleSuccessPopupClose}
433
+ featureName={featureFormName}
434
+ displayName={displayName}
435
+ />
436
+
437
+ {/* Toast Container for Notifications */}
438
+ <ToastContainer toasts={toasts} onDismiss={removeToast} />
439
+ </ErrorBoundary>
440
+ );
440
441
  };
441
442
 
442
443
  export const FormLayoutStep = withRouter(FormLayoutStepInner);