@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.
- 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,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
|
+
};
|