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