@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,529 @@
1
+ import { values } from "../values.config";
2
+
3
+ /**
4
+ * Retrieves the feature builder state from the Redux store
5
+ *
6
+ * @param {Object} state - The Redux store state
7
+ * @returns {Object} The feature builder state or empty object if not found
8
+ */
9
+ export const getFeatureBuilderState = (state) => state[values.reducerKey] || {};
10
+
11
+ // ============ FORM SELECTORS ============
12
+
13
+ /**
14
+ * Retrieves the form state from the feature builder state
15
+ *
16
+ * @param {Object} state - The Redux store state
17
+ * @returns {Object} The form state object or undefined
18
+ */
19
+ export const selectFormState = (state) => getFeatureBuilderState(state).form;
20
+
21
+ /**
22
+ * Retrieves the form title from the form state
23
+ *
24
+ * @param {Object} state - The Redux store state
25
+ * @returns {string|undefined} The form title or undefined if not set
26
+ */
27
+ export const selectFormTitle = (state) => {
28
+ const formState = selectFormState(state);
29
+ return formState && formState.title;
30
+ };
31
+
32
+ /**
33
+ * Retrieves the form icon from the form state
34
+ *
35
+ * @param {Object} state - The Redux store state
36
+ * @returns {string|undefined} The form icon identifier or undefined if not set
37
+ */
38
+ export const selectFormIcon = (state) => {
39
+ const formState = selectFormState(state);
40
+ return formState && formState.icon;
41
+ };
42
+
43
+ /**
44
+ * Retrieves the form display name from the form state
45
+ *
46
+ * @param {Object} state - The Redux store state
47
+ * @returns {string|undefined} The form display name or undefined if not set
48
+ */
49
+ export const selectFormDisplayName = (state) => {
50
+ const formState = selectFormState(state);
51
+ return formState && formState.displayName;
52
+ };
53
+
54
+ /**
55
+ * Retrieves the form layout from the form state
56
+ *
57
+ * @param {Object} state - The Redux store state
58
+ * @returns {Object|undefined} The form layout object or undefined if not set
59
+ */
60
+ export const selectFormLayout = (state) => {
61
+ const formState = selectFormState(state);
62
+ return formState && formState.layout;
63
+ };
64
+
65
+ /**
66
+ * Retrieves the form fields from the form state
67
+ *
68
+ * @param {Object} state - The Redux store state
69
+ * @returns {Array} Array of field definitions or empty array if not set
70
+ */
71
+ export const selectFormFields = (state) => {
72
+ const formState = selectFormState(state);
73
+ return (formState && formState.fields) || [];
74
+ };
75
+
76
+ export const selectFormField = (fieldId) => (state) =>
77
+ selectFormFields(state).find((field) => field.id === fieldId);
78
+
79
+ export const selectFormIsInitial = (state) => {
80
+ const formState = selectFormState(state);
81
+ return formState && formState._isInitial;
82
+ };
83
+ export const selectFormIsSubmitting = (state) => {
84
+ const formState = selectFormState(state);
85
+ return formState && formState._isSubmitting;
86
+ };
87
+
88
+ export const selectFormSubmitError = (state) => {
89
+ const formState = selectFormState(state);
90
+ return formState && formState._submitError;
91
+ };
92
+
93
+ export const selectFormSubmitSuccess = (state) => {
94
+ const formState = selectFormState(state);
95
+ return formState && formState._submitSuccess;
96
+ };
97
+
98
+ // ============ DEFINITION SELECTORS ============
99
+
100
+ export const selectDefinitionState = (state) =>
101
+ getFeatureBuilderState(state).definition;
102
+
103
+ export const selectDefinition = (state) => {
104
+ const definitionState = selectDefinitionState(state);
105
+ return definitionState && definitionState.definition;
106
+ };
107
+ export const selectDefinitionId = (state) => {
108
+ const definitionState = selectDefinitionState(state);
109
+ return definitionState && definitionState.id;
110
+ };
111
+ export const selectDefinitionIsLoading = (state) => {
112
+ const definitionState = selectDefinitionState(state);
113
+ return definitionState && definitionState.isLoading;
114
+ };
115
+ export const selectDefinitionError = (state) => {
116
+ const definitionState = selectDefinitionState(state);
117
+ return definitionState && definitionState.error;
118
+ };
119
+ export const selectDefinitionIsCreating = (state) => {
120
+ const definitionState = selectDefinitionState(state);
121
+ return definitionState && definitionState.isCreating;
122
+ };
123
+ export const selectDefinitionIsEditing = (state) => {
124
+ const definitionState = selectDefinitionState(state);
125
+ return definitionState && definitionState.isEditing;
126
+ };
127
+
128
+ // Check if we have a definition loaded
129
+ export const selectHasDefinition = (state) => !!selectDefinition(state);
130
+
131
+ // ============ LISTINGS SELECTORS ============
132
+
133
+ export const selectListingsState = (state) =>
134
+ getFeatureBuilderState(state).listings;
135
+
136
+ export const selectListings = (state) => {
137
+ const listingsState = selectListingsState(state);
138
+ return (listingsState && listingsState.listings) || [];
139
+ };
140
+ export const selectListingsIsLoading = (state) => {
141
+ const listingsState = selectListingsState(state);
142
+ return listingsState && listingsState.isLoading;
143
+ };
144
+ export const selectListingsError = (state) => {
145
+ const listingsState = selectListingsState(state);
146
+ return listingsState && listingsState.error;
147
+ };
148
+ export const selectListingsIsCreating = (state) => {
149
+ const listingsState = selectListingsState(state);
150
+ return listingsState && listingsState.isCreating;
151
+ };
152
+ export const selectListingsIsEditing = (state) => {
153
+ const listingsState = selectListingsState(state);
154
+ return listingsState && listingsState.isEditing;
155
+ };
156
+ export const selectListingsIsDeleting = (state) => {
157
+ const listingsState = selectListingsState(state);
158
+ return listingsState && listingsState.isDeleting;
159
+ };
160
+ export const selectListingsIsRestoring = (state) => {
161
+ const listingsState = selectListingsState(state);
162
+ return listingsState && listingsState.isRestoring;
163
+ };
164
+ export const selectListingsIsInitiallyLoaded = (state) => {
165
+ const listingsState = selectListingsState(state);
166
+ return listingsState && listingsState.isInitiallyLoaded;
167
+ };
168
+
169
+ // Get a specific listing by ID
170
+ export const selectListingById = (listingId) => (state) => {
171
+ const listings = selectListings(state);
172
+
173
+ if (!listingId) {
174
+ return null;
175
+ }
176
+
177
+ const found = listings.find((listing) => {
178
+ const id = listing.id || listing._id;
179
+ return id === listingId;
180
+ });
181
+
182
+ return found;
183
+ };
184
+
185
+ // ============ SOFT DELETE SELECTORS ============
186
+
187
+ // Get active (non-deleted) listings
188
+ export const selectActiveListings = (state) => {
189
+ return selectListings(state).filter((listing) => !listing.deletedAt);
190
+ };
191
+
192
+ // Get deleted listings
193
+ export const selectDeletedListings = (state) => {
194
+ return selectListings(state).filter((listing) => listing.deletedAt);
195
+ };
196
+
197
+ // Check if there are any deleted listings
198
+ export const selectHasDeletedListings = (state) => {
199
+ return selectDeletedListings(state).length > 0;
200
+ };
201
+
202
+ // Check if there are any active listings
203
+ export const selectHasActiveListings = (state) => {
204
+ return selectActiveListings(state).length > 0;
205
+ };
206
+
207
+ // Get count of deleted listings
208
+ export const selectDeletedListingsCount = (state) => {
209
+ return selectDeletedListings(state).length;
210
+ };
211
+
212
+ // ============ COMBINED SELECTORS ============
213
+
214
+ // Get the current feature definition ID (either from definition or fallback to config)
215
+ export const selectCurrentFeatureDefinitionId = (state) =>
216
+ selectDefinitionId(state) || values.featureId;
217
+
218
+ // Check if there are any unsaved changes in the form
219
+ export const selectHasUnsavedChanges = (state) => {
220
+ const definition = selectDefinition(state);
221
+ const formTitle = selectFormTitle(state);
222
+ const formIcon = selectFormIcon(state);
223
+ const formDisplayName = selectFormDisplayName(state);
224
+ const formLayout = selectFormLayout(state);
225
+
226
+ if (!definition) return false;
227
+
228
+ return (
229
+ definition.title !== formTitle ||
230
+ definition.icon !== formIcon ||
231
+ definition.displayName !== formDisplayName ||
232
+ JSON.stringify(definition.layout) !== JSON.stringify(formLayout)
233
+ );
234
+ };
235
+
236
+ // Get validation state for the form
237
+ export const selectFormValidation = (state) => {
238
+ const formFields = selectFormFields(state);
239
+ const title = selectFormTitle(state);
240
+ const displayName = selectFormDisplayName(state);
241
+ const icon = selectFormIcon(state);
242
+
243
+ const hasTitle = title && title.trim().length > 0;
244
+ const hasDisplayName = displayName && displayName.trim().length > 0;
245
+ const hasIcon = icon && icon.length > 0;
246
+ const hasValidFields = formFields.every((field) => {
247
+ if (
248
+ field.isMandatory &&
249
+ (field.type === "text" ||
250
+ field.type === "description" ||
251
+ field.type === "title" ||
252
+ field.type === "image" ||
253
+ field.type === "feature-image" ||
254
+ field.type === "file" ||
255
+ field.type === "cta")
256
+ ) {
257
+ return field.values.label && field.values.label.trim().length > 0;
258
+ }
259
+ return true;
260
+ });
261
+
262
+ // Check summary field validation
263
+ const descriptionFields = formFields.filter(
264
+ (field) => field.type === "description",
265
+ );
266
+ const summaryFields = descriptionFields.filter(
267
+ (field) => field.values && field.values.useAsSummary === true,
268
+ );
269
+ const hasValidSummaryFields = summaryFields.length <= 1; // At most one summary field allowed
270
+
271
+ return {
272
+ isValid:
273
+ hasTitle &&
274
+ hasDisplayName &&
275
+ hasIcon &&
276
+ hasValidFields &&
277
+ hasValidSummaryFields,
278
+ hasTitle,
279
+ hasDisplayName,
280
+ hasIcon,
281
+ hasValidFields,
282
+ hasValidSummaryFields,
283
+ errors: {
284
+ title: !hasTitle ? "Title is required" : null,
285
+ displayName: !hasDisplayName ? "Display name is required" : null,
286
+ icon: !hasIcon ? "Icon is required" : null,
287
+ fields: !hasValidFields ? "Some required fields are missing" : null,
288
+ summary: !hasValidSummaryFields
289
+ ? "Only one description field can be used as preview text"
290
+ : null,
291
+ },
292
+ };
293
+ };
294
+
295
+ // ============ WIZARD SELECTORS ============
296
+
297
+ export const selectWizardState = (state) =>
298
+ getFeatureBuilderState(state).wizard;
299
+
300
+ export const selectWizardMode = (state) => {
301
+ const wizardState = selectWizardState(state);
302
+ return wizardState && wizardState.mode;
303
+ };
304
+
305
+ // Mode detection selectors - wizard mode is the SINGLE SOURCE OF TRUTH for create/edit mode
306
+ // Wizard mode is automatically synchronized with definition state through dispatched actions
307
+ export const selectIsCreateMode = (state) =>
308
+ selectWizardMode(state) === "create";
309
+
310
+ export const selectIsEditMode = (state) => selectWizardMode(state) === "edit";
311
+
312
+ export const selectNavigationState = (state) => {
313
+ const wizardState = selectWizardState(state);
314
+ return wizardState && wizardState.navigation;
315
+ };
316
+
317
+ export const selectCurrentStep = (state) => {
318
+ const navigationState = selectNavigationState(state);
319
+ return navigationState && navigationState.currentStep;
320
+ };
321
+
322
+ export const selectCanGoBack = (state) => {
323
+ const navigationState = selectNavigationState(state);
324
+ return navigationState && navigationState.canGoBack;
325
+ };
326
+
327
+ export const selectPreviousStep = (state) => {
328
+ const navigationState = selectNavigationState(state);
329
+ return navigationState && navigationState.previousStep;
330
+ };
331
+
332
+ export const selectCanGoForward = (state) => {
333
+ const navigationState = selectNavigationState(state);
334
+ return navigationState && navigationState.canGoForward;
335
+ };
336
+
337
+ export const selectStepValidation = (state) => {
338
+ const wizardState = selectWizardState(state);
339
+ return wizardState && wizardState.stepValidation;
340
+ };
341
+
342
+ export const selectStepValidationState = (step) => (state) => {
343
+ const stepValidation = selectStepValidation(state);
344
+ return stepValidation && stepValidation[step];
345
+ };
346
+
347
+ export const selectIsStepValid = (step) => (state) => {
348
+ const stepValidationState = selectStepValidationState(step)(state);
349
+ return stepValidationState && stepValidationState.isValid;
350
+ };
351
+
352
+ export const selectStepErrors = (step) => (state) => {
353
+ const stepValidationState = selectStepValidationState(step)(state);
354
+ return stepValidationState ? stepValidationState.errors : {};
355
+ };
356
+
357
+ export const selectStepCompletion = (state) => {
358
+ const wizardState = selectWizardState(state);
359
+ return wizardState && wizardState.stepCompletion;
360
+ };
361
+
362
+ export const selectIsStepComplete = (step) => (state) => {
363
+ const stepCompletion = selectStepCompletion(state);
364
+ return stepCompletion && stepCompletion[step];
365
+ };
366
+
367
+ export const selectAllStepsComplete = (state) => {
368
+ const stepCompletion = selectStepCompletion(state);
369
+ if (!stepCompletion) return false;
370
+ return (
371
+ stepCompletion.overview && stepCompletion.fields && stepCompletion.layout
372
+ );
373
+ };
374
+
375
+ export const selectStepProgress = (state) => {
376
+ const stepCompletion = selectStepCompletion(state);
377
+ if (!stepCompletion) return { completed: 0, total: 3, percentage: 0 };
378
+
379
+ const completed = Object.values(stepCompletion).filter(Boolean).length;
380
+ const total = Object.keys(stepCompletion).length;
381
+ const percentage = Math.round((completed / total) * 100);
382
+
383
+ return { completed, total, percentage };
384
+ };
385
+
386
+ // Determine initial wizard mode based on definition existence
387
+ export const selectInitialWizardMode = (state) => {
388
+ const hasDefinition = selectHasDefinition(state);
389
+ return hasDefinition ? "edit" : "create";
390
+ };
391
+
392
+ // Check if current step should be accessible in current mode
393
+ export const selectIsStepAccessible = (step) => (state) => {
394
+ const mode = selectWizardMode(state);
395
+ const currentStep = selectCurrentStep(state);
396
+
397
+ if (mode === "edit") {
398
+ // In edit mode, all steps are accessible
399
+ return true;
400
+ }
401
+
402
+ // In create mode, check if we can access this step
403
+ switch (step) {
404
+ case "welcome":
405
+ return !selectHasDefinition(state);
406
+ case "overview":
407
+ case "fields":
408
+ case "layout":
409
+ // In create mode, can only access if we're not on welcome screen
410
+ return currentStep !== "welcome";
411
+ default:
412
+ return false;
413
+ }
414
+ };
415
+
416
+ // ============ LISTING UI SELECTORS ============
417
+
418
+ // Get current sort method
419
+ export const selectSortBy = (state) => {
420
+ const listingsState = selectListingsState(state);
421
+ return listingsState && listingsState.sortBy
422
+ ? listingsState.sortBy
423
+ : "newest";
424
+ };
425
+
426
+ // Get show deleted toggle state
427
+ export const selectShowDeleted = (state) => {
428
+ const listingsState = selectListingsState(state);
429
+ return listingsState && listingsState.showDeleted
430
+ ? listingsState.showDeleted
431
+ : false;
432
+ };
433
+
434
+ // Get sorted and filtered listings
435
+ export const selectSortedListings = (state) => {
436
+ const showDeleted = selectShowDeleted(state);
437
+ const sortBy = selectSortBy(state);
438
+ const activeListings = selectActiveListings(state);
439
+ const deletedListings = selectDeletedListings(state);
440
+
441
+ // Determine which listings to include
442
+ let listingsToShow = activeListings;
443
+ if (showDeleted) {
444
+ listingsToShow = [...activeListings, ...deletedListings];
445
+ }
446
+
447
+ // Sort the listings
448
+ switch (sortBy) {
449
+ case "newest":
450
+ return [...listingsToShow].sort((a, b) => {
451
+ const dateA = a.createdAt || a.order;
452
+ const dateB = b.createdAt || b.order;
453
+ return new Date(dateB) - new Date(dateA);
454
+ });
455
+ case "oldest":
456
+ return [...listingsToShow].sort((a, b) => {
457
+ const dateA = a.createdAt || a.order;
458
+ const dateB = b.createdAt || b.order;
459
+ return new Date(dateA) - new Date(dateB);
460
+ });
461
+ case "az":
462
+ return [...listingsToShow].sort((a, b) => {
463
+ const titleA = (a.fields && a.fields["mandatory-title"]) || "";
464
+ const titleB = (b.fields && b.fields["mandatory-title"]) || "";
465
+ return titleA.localeCompare(titleB);
466
+ });
467
+ case "za":
468
+ return [...listingsToShow].sort((a, b) => {
469
+ const titleA = (a.fields && a.fields["mandatory-title"]) || "";
470
+ const titleB = (b.fields && b.fields["mandatory-title"]) || "";
471
+ return titleB.localeCompare(titleA);
472
+ });
473
+ default:
474
+ return listingsToShow;
475
+ }
476
+ };
477
+
478
+ // ============ SUMMARY FIELD SELECTORS ============
479
+
480
+ /**
481
+ * Get the description field that is marked as summary field
482
+ *
483
+ * @param {Object} state - The Redux store state
484
+ * @returns {Object|null} The field object marked as summary, or null if none found
485
+ */
486
+ export const selectSummaryDescriptionField = (state) => {
487
+ const fields = selectFormFields(state);
488
+ return (
489
+ fields.find(
490
+ (field) =>
491
+ field.type === "description" &&
492
+ field.values &&
493
+ field.values.useAsSummary === true,
494
+ ) || null
495
+ );
496
+ };
497
+
498
+ /**
499
+ * Get the field ID of the description field marked as summary
500
+ *
501
+ * @param {Object} state - The Redux store state
502
+ * @returns {string|null} The field ID marked as summary, or null if none found
503
+ */
504
+ export const selectSummaryDescriptionFieldId = (state) => {
505
+ const summaryField = selectSummaryDescriptionField(state);
506
+ return summaryField ? summaryField.id : null;
507
+ };
508
+
509
+ /**
510
+ * Extract summary content from listing data based on the summary field configuration
511
+ *
512
+ * @param {Object} state - The Redux store state
513
+ * @param {Object} listing - The listing object containing field values
514
+ * @returns {string} The summary content or empty string if not found
515
+ */
516
+ export const getSummaryDescriptionFromListing = (state, listing) => {
517
+ if (!listing || !listing.fields) {
518
+ return "";
519
+ }
520
+
521
+ const summaryField = selectSummaryDescriptionField(state);
522
+ if (!summaryField) {
523
+ // If no summary field is configured, return empty string
524
+ return "";
525
+ }
526
+
527
+ // Get the value from the listing's fields using the summary field's ID
528
+ return listing.fields[summaryField.id] || "";
529
+ };
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Core type definitions for the Feature Builder
3
+ * This file contains JSDoc type definitions for all major entities
4
+ */
5
+
6
+ /**
7
+ * @typedef {Object} Field
8
+ * @property {string} id - Unique identifier for the field
9
+ * @property {string} type - Field type (text, title, description, image, file, cta, feature-image, gallery)
10
+ * @property {string} label - Display label for the field
11
+ * @property {boolean} required - Whether the field is required
12
+ * @property {string} [placeholder] - Placeholder text for input fields
13
+ * @property {string} [helpText] - Help text displayed below the field
14
+ * @property {Object} [validation] - Validation rules for the field
15
+ * @property {Object} [options] - Additional options specific to field type
16
+ * @property {number} [order] - Display order of the field
17
+ */
18
+
19
+ /**
20
+ * @typedef {Object} FormState
21
+ * @property {string} id - Feature definition ID
22
+ * @property {string} displayName - Display name for the feature
23
+ * @property {string} [description] - Description of the feature
24
+ * @property {string} icon - Icon identifier for the feature
25
+ * @property {Field[]} fields - Array of field definitions
26
+ * @property {Object} layout - Layout configuration
27
+ * @property {boolean} isInitial - Whether this is the initial state
28
+ * @property {boolean} isDirty - Whether the form has unsaved changes
29
+ * @property {Object} validation - Form validation state
30
+ */
31
+
32
+ /**
33
+ * @typedef {Object} Listing
34
+ * @property {string} id - Unique identifier for the listing
35
+ * @property {string} featureDefinitionId - ID of the feature definition this belongs to
36
+ * @property {string} siteId - Site ID where this listing belongs
37
+ * @property {string} [title] - Listing title
38
+ * @property {string} [description] - Listing description
39
+ * @property {Object} [fieldValues] - Key-value pairs of field data
40
+ * @property {boolean} isActive - Whether the listing is active
41
+ * @property {boolean} isDeleted - Whether the listing is soft-deleted
42
+ * @property {string} [createdBy] - User ID who created this listing
43
+ * @property {string} [updatedBy] - User ID who last updated this listing
44
+ * @property {string} createdAt - Creation timestamp
45
+ * @property {string} updatedAt - Last update timestamp
46
+ */
47
+
48
+ /**
49
+ * @typedef {Object} FeatureDefinition
50
+ * @property {string} id - Unique identifier for the feature definition
51
+ * @property {string} displayName - Human-readable name for the feature
52
+ * @property {string} [description] - Description of what the feature does
53
+ * @property {string} icon - Icon identifier
54
+ * @property {Field[]} fields - Array of field definitions
55
+ * @property {Object} layout - Layout configuration
56
+ * @property {boolean} isActive - Whether the feature definition is active
57
+ * @property {string} [createdBy] - User ID who created this feature definition
58
+ * @property {string} createdAt - Creation timestamp
59
+ * @property {string} updatedAt - Last update timestamp
60
+ */
61
+
62
+ /**
63
+ * @typedef {Object} WizardState
64
+ * @property {string} mode - Current wizard mode ('create' | 'edit')
65
+ * @property {number} currentStep - Current step index
66
+ * @property {Object} validation - Validation state for each step
67
+ * @property {boolean} canProceed - Whether user can proceed to next step
68
+ * @property {boolean} isComplete - Whether wizard is complete
69
+ */
70
+
71
+ /**
72
+ * @typedef {Object} ApiResponse
73
+ * @property {boolean} success - Whether the API call was successful
74
+ * @property {*} data - Response data payload
75
+ * @property {string} [message] - Optional message from the server
76
+ * @property {string} [error] - Error message if the call failed
77
+ */
78
+
79
+ /**
80
+ * @typedef {Object} SortConfig
81
+ * @property {string} field - Field to sort by
82
+ * @property {string} direction - Sort direction ('asc' | 'desc')
83
+ */
84
+
85
+ /**
86
+ * @typedef {Object} FilterConfig
87
+ * @property {string} [search] - Search term
88
+ * @property {boolean} [showDeleted] - Whether to include deleted items
89
+ * @property {Object} [fieldFilters] - Additional field-based filters
90
+ */
91
+
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Text utility functions for consistent formatting throughout the application
3
+ */
4
+
5
+ /**
6
+ * Converts a string to Title Case (first letter of each word capitalized)
7
+ * Handles null, undefined, and empty string cases safely
8
+ *
9
+ * @param {string} text - The text to convert to Title Case
10
+ * @param {string} [fallback=""] - Fallback text if input is null/undefined
11
+ * @returns {string} - Title Cased text or fallback
12
+ *
13
+ * @example
14
+ * toTitleCase("hello world") // returns "Hello World"
15
+ * toTitleCase("feature builder") // returns "Feature Builder"
16
+ * toTitleCase("") // returns ""
17
+ * toTitleCase(null) // returns ""
18
+ * toTitleCase(undefined, "Default") // returns "Default"
19
+ */
20
+ export const toTitleCase = (text, fallback = "") => {
21
+ if (text === null || text === undefined) {
22
+ return fallback;
23
+ }
24
+
25
+ if (typeof text !== "string") {
26
+ return fallback;
27
+ }
28
+
29
+ if (text.length === 0) {
30
+ return fallback;
31
+ }
32
+
33
+ return text
34
+ .toLowerCase()
35
+ .split(" ")
36
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
37
+ .join(" ");
38
+ };
39
+
40
+ /**
41
+ * Capitalizes the first character of a string while preserving the rest
42
+ * Handles null, undefined, and empty string cases safely
43
+ *
44
+ * @param {string} text - The text to capitalize
45
+ * @param {string} [fallback=""] - Fallback text if input is null/undefined
46
+ * @returns {string} - Capitalized text or fallback
47
+ *
48
+ * @example
49
+ * capitalizeText("hello") // returns "Hello"
50
+ * capitalizeText("WORLD") // returns "WORLD"
51
+ * capitalizeText("") // returns ""
52
+ * capitalizeText(null) // returns ""
53
+ * capitalizeText(undefined, "item") // returns "item"
54
+ */
55
+ export const capitalizeText = (text, fallback = "") => {
56
+ if (text === null || text === undefined) {
57
+ return fallback;
58
+ }
59
+
60
+ if (typeof text !== "string") {
61
+ return fallback;
62
+ }
63
+
64
+ if (text.length === 0) {
65
+ return fallback;
66
+ }
67
+
68
+ return text.charAt(0).toUpperCase() + text.slice(1);
69
+ };
70
+
71
+ /**
72
+ * Capitalizes text and ensures it's not empty by providing a fallback
73
+ *
74
+ * @param {string} text - The text to capitalize
75
+ * @param {string} fallback - Fallback text if input is empty/null/undefined
76
+ * @returns {string} - Capitalized text or fallback
77
+ *
78
+ * @example
79
+ * capitalizeTextWithFallback("hello", "Item") // returns "Hello"
80
+ * capitalizeTextWithFallback("", "Item") // returns "Item"
81
+ * capitalizeTextWithFallback(null, "Item") // returns "Item"
82
+ */
83
+ export const capitalizeTextWithFallback = (text, fallback) => {
84
+ if (!text || (typeof text === "string" && text.trim().length === 0)) {
85
+ return capitalizeText(fallback, "");
86
+ }
87
+
88
+ return capitalizeText(text, fallback);
89
+ };