@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,478 @@
1
+ import React, { useEffect, useState } from "react";
2
+ import { useSelector, useDispatch } from "react-redux";
3
+ import { withRouter } from "react-router-dom";
4
+ import { Table } from "react-bootstrap";
5
+ import { PlussCore } from "../feature.config";
6
+ import {
7
+ fetchListing,
8
+ fetchListingSilent,
9
+ deleteListing,
10
+ undeleteListing,
11
+ setSortBy,
12
+ setShowDeleted,
13
+ } from "../actions/listingActions";
14
+ import { fetchFeatureDefinitions } from "../actions/featureDefinitionsIndex.js";
15
+ import { capitalizeTextWithFallback } from "../utils/textUtils";
16
+ import {
17
+ selectSortedListings,
18
+ selectActiveListings,
19
+ selectDeletedListings,
20
+ selectSortBy,
21
+ selectShowDeleted,
22
+ selectListingsIsLoading,
23
+ selectListingsError,
24
+ selectListingsIsInitiallyLoaded,
25
+ selectListingsIsRestoring,
26
+ selectDefinition,
27
+ selectDefinitionIsLoading,
28
+ selectDefinitionError,
29
+ selectHasDefinition,
30
+ } from "../selectors/featureBuilderSelectors";
31
+ import {
32
+ DeleteConfirmationPopup,
33
+ ErrorMessage,
34
+ LoadingState,
35
+ FeatureBuilderSidebar,
36
+ SortButtonGroup,
37
+ ToggleSwitch,
38
+ Header,
39
+ ErrorBoundary,
40
+ AddButton,
41
+ } from "../components";
42
+
43
+ import { values } from "../values.config.js";
44
+
45
+ import styles from "./ListingScreen.module.css";
46
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
47
+ import { faPen, faTrash } from "@fortawesome/free-solid-svg-icons";
48
+
49
+
50
+ const { Button, Text } = PlussCore.Components;
51
+
52
+ const ListingScreen = (props) => {
53
+ const { history } = props;
54
+ const dispatch = useDispatch();
55
+ const auth = useSelector((state) => state.auth);
56
+ const sortedListings = useSelector(selectSortedListings);
57
+ const activeListings = useSelector(selectActiveListings);
58
+ const deletedListings = useSelector(selectDeletedListings);
59
+ const sortBy = useSelector(selectSortBy);
60
+ const showDeleted = useSelector(selectShowDeleted);
61
+ const isLoading = useSelector(selectListingsIsLoading);
62
+ const isInitiallyLoaded = useSelector(selectListingsIsInitiallyLoaded);
63
+ const error = useSelector(selectListingsError);
64
+ const isRestoring = useSelector(selectListingsIsRestoring);
65
+ const definition = useSelector(selectDefinition);
66
+ const displayName = definition?.displayName || definition?.title || "Feature";
67
+ const isDefinitionLoading = useSelector(selectDefinitionIsLoading);
68
+ const definitionError = useSelector(selectDefinitionError);
69
+ const hasDefinition = useSelector(selectHasDefinition);
70
+
71
+ const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
72
+ const [listingToDelete, setListingToDelete] = useState(null);
73
+ const [deleteMessage, setDeleteMessage] = useState("");
74
+ const [isDeleting, setIsDeleting] = useState(false);
75
+
76
+ // Error handling state
77
+ const [actionError, setActionError] = useState("");
78
+ const [showActionError, setShowActionError] = useState(false);
79
+
80
+ useEffect(() => {
81
+ // Always fetch feature definitions
82
+ dispatch(fetchFeatureDefinitions());
83
+
84
+ // Check data freshness - if no listings exist or data is stale, fetch with loading
85
+ const dataNeedsRefresh =
86
+ !isInitiallyLoaded ||
87
+ sortedListings.length === 0 ||
88
+ (sortedListings.length > 0 && !sortedListings[0].id);
89
+
90
+ if (dataNeedsRefresh) {
91
+ // Data is empty or invalid - show loading and fetch
92
+ dispatch(fetchListing());
93
+ } else {
94
+ // Data exists - refresh in background silently
95
+ dispatch(fetchListingSilent());
96
+ }
97
+ }, [dispatch, isInitiallyLoaded, sortedListings.length]);
98
+
99
+ // Helper function to show action errors
100
+ const showActionErrorMessage = (message) => {
101
+ setActionError(message);
102
+ setShowActionError(true);
103
+ // Auto-hide after 5 seconds
104
+ setTimeout(() => {
105
+ setShowActionError(false);
106
+ }, 5000);
107
+ };
108
+
109
+ // Smart refresh with data freshness check
110
+ const handleSmartRefresh = () => {
111
+ // Force fresh data load regardless of current state
112
+ dispatch(fetchListing());
113
+ };
114
+
115
+ const handleDeleteClick = (listing) => {
116
+ setListingToDelete(listing);
117
+ setShowDeleteConfirm(true);
118
+ setDeleteMessage("");
119
+ };
120
+
121
+ const handleEditClick = (listing) => {
122
+ const id = listing.id || listing._id;
123
+ history.push(values.routeEditListing.replace(":id", id));
124
+ };
125
+
126
+ const handleCreateClick = () => {
127
+ history.push(values.routeCreateListing);
128
+ };
129
+
130
+ const handleConfirmDelete = () => {
131
+ if (listingToDelete) {
132
+ setIsDeleting(true);
133
+ dispatch(deleteListing(listingToDelete.id))
134
+ .then(() => {
135
+ const title =
136
+ (listingToDelete.fields &&
137
+ listingToDelete.fields["mandatory-title"]) ||
138
+ "Item";
139
+ setDeleteMessage(
140
+ `${capitalizeTextWithFallback(title, "Item")} deleted. You can restore it anytime.`,
141
+ );
142
+ setShowDeleteConfirm(false);
143
+ setListingToDelete(null);
144
+ setIsDeleting(false);
145
+ // Clear message after 3 seconds
146
+ setTimeout(() => setDeleteMessage(""), 3000);
147
+ })
148
+ .catch((error) => {
149
+ const errorMessage = error.message || "Network error";
150
+ showActionErrorMessage(
151
+ `Can't delete listing right now. Check your internet connection and try again. (${errorMessage})`,
152
+ );
153
+ setShowDeleteConfirm(false);
154
+ setListingToDelete(null);
155
+ setIsDeleting(false);
156
+ });
157
+ }
158
+ };
159
+
160
+ const handleCancelDelete = () => {
161
+ setShowDeleteConfirm(false);
162
+ setListingToDelete(null);
163
+ };
164
+
165
+ const handleRestoreClick = (listing) => {
166
+ dispatch(undeleteListing(listing.id))
167
+ .then(() => {
168
+ const title =
169
+ (listing.fields && listing.fields["mandatory-title"]) || "Item";
170
+ setDeleteMessage(
171
+ `${capitalizeTextWithFallback(title, "Item")} restored and ready to use.`,
172
+ );
173
+ // Clear message after 3 seconds
174
+ setTimeout(() => setDeleteMessage(""), 3000);
175
+ })
176
+ .catch((error) => {
177
+ const errorMessage = error.message || "Network error";
178
+ showActionErrorMessage(
179
+ `Can't restore listing right now. Check your internet connection and try again. (${errorMessage})`,
180
+ );
181
+ });
182
+ };
183
+
184
+ const handleSortChange = (sortType) => {
185
+ dispatch(setSortBy(sortType));
186
+ };
187
+
188
+ const handleToggleDeleted = (showDeleted) => {
189
+ dispatch(setShowDeleted(showDeleted));
190
+ };
191
+
192
+ const handleShowDeletedListings = () => {
193
+ dispatch(setShowDeleted(true));
194
+ };
195
+
196
+ const renderTableRows = () => {
197
+ const source = sortedListings;
198
+
199
+ return source.map((listing) => {
200
+ const id = listing.id || listing._id;
201
+ const title =
202
+ (listing.fields && listing.fields["mandatory-title"]) || "Untitled";
203
+ const modifiedAt =
204
+ listing.editedAt ||
205
+ listing.updatedAt ||
206
+ listing.createdAt ||
207
+ new Date();
208
+ const isDeleted = !!listing.deletedAt;
209
+
210
+ return (
211
+ <tr
212
+ key={id}
213
+ className={isDeleted ? styles["listingScreen__row--deleted"] : ""}
214
+ >
215
+ <td>
216
+ {new Date(modifiedAt).toLocaleDateString("en-AU", {
217
+ day: "numeric",
218
+ month: "short",
219
+ year: "2-digit",
220
+ })}
221
+ </td>
222
+ <td>
223
+ <div className={styles.titleContainer}>
224
+ <span
225
+ onClick={() => handleEditClick(listing)}
226
+ className={
227
+ isDeleted ? styles["title--deleted"] : styles["titleLink"]
228
+ }
229
+ >
230
+ {title}
231
+ </span>
232
+ {isDeleted && <span className={styles.deletedTag}>Deleted</span>}
233
+ </div>
234
+ </td>
235
+ <td>
236
+ <div className={styles.actions}>
237
+ {!isDeleted && (
238
+ <span
239
+ onClick={() => handleEditClick(listing)}
240
+ className={styles.actionIcon}
241
+ title="Edit"
242
+ >
243
+ <FontAwesomeIcon icon={faPen} />
244
+ </span>
245
+ )}
246
+
247
+ {!isDeleted && (
248
+ <span
249
+ onClick={() => handleDeleteClick(listing)}
250
+ className={styles.actionIcon}
251
+ title="Delete"
252
+ >
253
+ <FontAwesomeIcon icon={faTrash} />
254
+ </span>
255
+ )}
256
+ {isDeleted && (
257
+ <Button
258
+ onClick={() => handleRestoreClick(listing)}
259
+ buttonType="outlinedAction"
260
+ disabled={isRestoring}
261
+ narrow
262
+ leftIcon={isRestoring ? "sync" : "undo"}
263
+ loading={isRestoring}
264
+ >
265
+ {isRestoring ? "Restoring..." : "Restore"}
266
+ </Button>
267
+ )}
268
+ </div>
269
+ </td>
270
+ </tr>
271
+ );
272
+ });
273
+ };
274
+
275
+ // Check for content management permission
276
+ if (
277
+ !PlussCore.Session.validateAccess(
278
+ auth.site,
279
+ values.permissionFeatureBuilderContent,
280
+ auth,
281
+ )
282
+ ) {
283
+ return (
284
+ <div className="hub-wrapperContainer">
285
+ <FeatureBuilderSidebar />
286
+ <div className="hub-contentWrapper">
287
+ <div className={styles.welcomeContainer}>
288
+ <div className={styles.welcomeHeader}>
289
+ <Text type="h1" className={styles.welcomeTitle}>
290
+ Access Restricted
291
+ </Text>
292
+ <Text type="body" className={styles.welcomeSubtitle}>
293
+ You don't have permission to manage feature content. Please
294
+ contact your administrator if you need access.
295
+ </Text>
296
+ </div>
297
+ </div>
298
+ </div>
299
+ </div>
300
+ );
301
+ }
302
+
303
+ // Don't show full-screen loading, use skeleton states instead
304
+
305
+ return (
306
+ <ErrorBoundary
307
+ title="Unable to load listings"
308
+ message="If you continue to experience issues loading your listings, please try refreshing the page or contact support."
309
+ onRetry={handleSmartRefresh}
310
+ >
311
+ <div className="hub-wrapperContainer">
312
+ <FeatureBuilderSidebar />
313
+ <div className="hub-headerContentWrapper">
314
+ <Header />
315
+ <div className={styles.root}>
316
+ <div className={styles.header}>
317
+ <div className={styles.headerText}>
318
+ <Text type="h1">Manage Your {displayName} Listings</Text>
319
+ <Text type="bodyLarge">
320
+ View, edit, and manage all your {displayName.toLowerCase()}{" "}
321
+ listings
322
+ </Text>
323
+ </div>
324
+ {sortedListings.length > 0 && (
325
+ <div className={styles.headerActions}>
326
+ <div className={styles.sortSection}>
327
+ <Text type="help" className={styles.uppercase}>
328
+ SORT BY
329
+ </Text>
330
+ {SortButtonGroup && (
331
+ <SortButtonGroup
332
+ currentSort={sortBy}
333
+ onSortChange={handleSortChange}
334
+ />
335
+ )}
336
+ </div>
337
+ <div className={styles.toggleContainer}>
338
+ <Text type="body">Show deleted {displayName} listing</Text>
339
+ <ToggleSwitch
340
+ isActive={showDeleted}
341
+ onChange={handleToggleDeleted}
342
+ />
343
+ </div>
344
+ </div>
345
+ )}
346
+ </div>
347
+
348
+ {/* Action errors (delete, restore, toggle) */}
349
+ {showActionError && (
350
+ <ErrorMessage
351
+ message={actionError}
352
+ variant="error"
353
+ onClose={() => setShowActionError(false)}
354
+ />
355
+ )}
356
+
357
+ {/* Success messages */}
358
+ {deleteMessage && deleteMessage.includes("success") && (
359
+ <ErrorMessage
360
+ message={deleteMessage}
361
+ variant="success"
362
+ onClose={() => setDeleteMessage("")}
363
+ />
364
+ )}
365
+
366
+ {/* General listing errors */}
367
+ {error && (
368
+ <ErrorMessage
369
+ message={`Error loading listings: ${error}`}
370
+ variant="error"
371
+ />
372
+ )}
373
+
374
+ {!hasDefinition && !isDefinitionLoading && (
375
+ <div className={styles.empty}>
376
+ <div className="emptyState" />
377
+ <p>
378
+ No feature definition exists. Please create a feature
379
+ definition first before creating listings.
380
+ </p>
381
+ <Button
382
+ buttonType="primary"
383
+ leftIcon="plus"
384
+ onClick={handleCreateClick}
385
+ isActive={true}
386
+ >
387
+ Add {displayName}
388
+ </Button>
389
+ </div>
390
+ )}
391
+
392
+ {definitionError && (
393
+ <ErrorMessage
394
+ message={`Error loading feature definition: ${definitionError}`}
395
+ variant="error"
396
+ />
397
+ )}
398
+
399
+ {isLoading ? (
400
+ <div className={styles.loadingContainer}>
401
+ <LoadingState message="Loading data..." />
402
+ </div>
403
+ ) : sortedListings.length > 0 ? (
404
+ <Table className="plussTable" striped bordered condensed hover>
405
+ <thead>
406
+ <tr key="header">
407
+ <th className={styles.dateColumn}>Last modified at</th>
408
+ <th>Title</th>
409
+ <th className={styles.actionsColumn}>Actions</th>
410
+ </tr>
411
+ </thead>
412
+ <tbody>{renderTableRows()}</tbody>
413
+ </Table>
414
+ ) : hasDefinition ? (
415
+ // Smart empty state logic
416
+ activeListings.length === 0 &&
417
+ deletedListings.length > 0 &&
418
+ !showDeleted ? (
419
+ <div className={styles.empty}>
420
+ <div className="emptyState" />
421
+ <p>
422
+ You have {deletedListings.length} deleted{" "}
423
+ {displayName.toLowerCase()} listing
424
+ {deletedListings.length > 1 ? "s" : ""}.
425
+ </p>
426
+ <div className={styles.emptyActions}>
427
+ <Button
428
+ buttonType="primary"
429
+ leftIcon="plus"
430
+ onClick={handleCreateClick}
431
+ className={styles.emptySecondaryButton}
432
+ isActive
433
+ >
434
+ Add new {displayName}
435
+ </Button>
436
+ <Button
437
+ buttonType="secondary"
438
+ leftIcon="eye"
439
+ onClick={handleShowDeletedListings}
440
+ className={styles.emptyPrimaryButton}
441
+ >
442
+ Show deleted listing
443
+ {deletedListings.length > 1 ? "s" : ""}
444
+ </Button>
445
+ </div>
446
+ </div>
447
+ ) : (
448
+ // Regular empty state
449
+ <div className={styles.empty}>
450
+ <div className="emptyState" />
451
+ <p>No {displayName.toLowerCase()} listings found.</p>
452
+ <Button
453
+ buttonType="primary"
454
+ leftIcon="plus"
455
+ onClick={handleCreateClick}
456
+ isActive
457
+ >
458
+ Add {displayName}
459
+ </Button>
460
+ </div>
461
+ )
462
+ ) : null}
463
+
464
+ <DeleteConfirmationPopup
465
+ isOpen={showDeleteConfirm}
466
+ listing={listingToDelete}
467
+ onConfirm={handleConfirmDelete}
468
+ onCancel={handleCancelDelete}
469
+ isDeleting={isDeleting}
470
+ />
471
+ </div>
472
+ </div>
473
+ </div>
474
+ </ErrorBoundary>
475
+ );
476
+ };
477
+
478
+ export default withRouter(ListingScreen);