@plusscommunities/pluss-feature-builder-web-a 1.0.2-beta.4 → 1.0.2-beta.6

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.
@@ -9,145 +9,145 @@ const { Apis } = PlussCore;
9
9
  const { fileActions } = Apis;
10
10
 
11
11
  const IconLoader = ({
12
- value,
13
- defaultValue,
14
- onChange,
15
- onRemove,
16
- disableRemove = false,
17
- featureId,
12
+ value,
13
+ defaultValue,
14
+ onChange,
15
+ onRemove,
16
+ disableRemove = false,
17
+ featureId,
18
18
  }) => {
19
- const [isUploading, setIsUploading] = useState(false);
20
- const fileInputRef = useRef(null);
21
-
22
- // Determine which icon to show
23
- const hasCustomIcon =
24
- value && typeof value === "string" && value.startsWith("http");
25
- const iconToShow = defaultValue || "star";
26
-
27
- // Generate filename with feature ID
28
- const generateFilename = (file, featureId) => {
29
- const timestamp = Date.now();
30
- const ext = file.name.split(".").pop();
31
- return `build-your-feature-${featureId || "new"}-${timestamp}.${ext}`;
32
- };
33
-
34
- // Get FontAwesome icon component directly (no async needed)
35
- const iconComponent = !hasCustomIcon ? iconImports[iconToShow] : null;
36
-
37
- const handleFileSelect = async (event) => {
38
- const file = event.target.files[0];
39
- if (!file || isUploading) return;
40
-
41
- // Validate file type - only accept images
42
- if (!file.type.startsWith("image/")) {
43
- alert("Please upload an image file (JPEG, PNG, GIF, or WebP)");
44
- return;
45
- }
46
-
47
- setIsUploading(true);
48
-
49
- try {
50
- // Compress and upload the image
51
- const compressedFile = await fileActions.compressImage(
52
- file,
53
- 1400,
54
- 0.8,
55
- false,
56
- );
57
-
58
- const filename = generateFilename(compressedFile, featureId);
59
- const url = await fileActions.uploadMediaAsync(compressedFile, filename);
60
-
61
- if (onChange) {
62
- onChange(url);
63
- }
64
- } catch (error) {
65
- alert("Failed to upload image. Please try again.");
66
- } finally {
67
- setIsUploading(false);
68
- // Reset file input
69
- if (fileInputRef.current) {
70
- fileInputRef.current.value = "";
71
- }
72
- }
73
- };
74
-
75
- const handleUploadClick = () => {
76
- if (fileInputRef.current && !isUploading) {
77
- fileInputRef.current.click();
78
- }
79
- };
80
-
81
- const handleRemove = () => {
82
- if (onRemove && !disableRemove) {
83
- onRemove();
84
- }
85
- };
86
-
87
- // Container will use CSS modules classes
88
- // Custom styling through props is deprecated in favor of CSS consistency
89
-
90
- return (
91
- <div className={`${styles.iconLoader} ${styles.iconLoader__container}`}>
92
- {/* Show the icon/image */}
93
- {hasCustomIcon ? (
94
- <img
95
- src={value}
96
- alt="Custom icon"
97
- className={styles.iconLoader__image}
98
- />
99
- ) : (
100
- <div className={styles.iconLoader__iconContainer}>
101
- {iconComponent ? (
102
- <FontAwesomeIcon
103
- icon={iconComponent}
104
- className={`${styles.iconLoader__icon} ${styles.iconLoader__iconLarge}`}
105
- />
106
- ) : (
107
- <div className={styles.iconLoader__fallbackText}>{iconToShow}</div>
108
- )}
109
- </div>
110
- )}
111
-
112
- {/* Upload/Delete button overlay - only shows on hover */}
113
- <div className={styles.iconLoader__buttonOverlay}>
114
- {hasCustomIcon ? (
115
- <Button
116
- buttonType="secondary"
117
- isActive
118
- onClick={handleRemove}
119
- disabled={disableRemove || isUploading}
120
- size="small"
121
- leftIcon="times"
122
- aria-label="Delete icon"
123
- >
124
- Delete
125
- </Button>
126
- ) : (
127
- <Button
128
- buttonType="primary"
129
- onClick={() => { }}
130
- disabled={isUploading}
131
- size="small"
132
- leftIcon="upload"
133
- loading={isUploading}
134
- aria-label="Upload icon"
135
- >
136
- Upload
137
- </Button>
138
- )}
139
- </div>
140
-
141
- {/* Hidden file input */}
142
- <input
143
- ref={fileInputRef}
144
- type="file"
145
- accept="image/jpeg,image/png,image/gif,image/webp,image/svg"
146
- onChange={handleFileSelect}
147
- className={styles.iconLoader__hiddenInput}
148
- />
149
- </div>
150
- );
19
+ const [isUploading, setIsUploading] = useState(false);
20
+ const fileInputRef = useRef(null);
21
+
22
+ // Determine which icon to show
23
+ const hasCustomIcon =
24
+ value && typeof value === "string" && value.startsWith("http");
25
+ const iconToShow = defaultValue || "star";
26
+
27
+ // Generate filename with feature ID
28
+ const generateFilename = (file, featureId) => {
29
+ const timestamp = Date.now();
30
+ const ext = file.name.split(".").pop();
31
+ return `build-your-feature-${featureId || "new"}-${timestamp}.${ext}`;
32
+ };
33
+
34
+ // Get FontAwesome icon component directly (no async needed)
35
+ const iconComponent = !hasCustomIcon ? iconImports[iconToShow] : null;
36
+
37
+ const handleFileSelect = async (event) => {
38
+ const file = event.target.files[0];
39
+ if (!file || isUploading) return;
40
+
41
+ // Validate file type - only accept images
42
+ if (!file.type.startsWith("image/")) {
43
+ alert("Please upload an image file (JPEG, PNG, GIF, or WebP)");
44
+ return;
45
+ }
46
+
47
+ setIsUploading(true);
48
+
49
+ try {
50
+ // Compress and upload the image
51
+ const compressedFile = await fileActions.compressImage(
52
+ file,
53
+ 1400,
54
+ 0.8,
55
+ false,
56
+ );
57
+
58
+ const filename = generateFilename(compressedFile, featureId);
59
+ const url = await fileActions.uploadMediaAsync(compressedFile, filename);
60
+
61
+ if (onChange) {
62
+ onChange(url);
63
+ }
64
+ } catch (error) {
65
+ alert("Failed to upload image. Please try again.");
66
+ } finally {
67
+ setIsUploading(false);
68
+ // Reset file input
69
+ if (fileInputRef.current) {
70
+ fileInputRef.current.value = "";
71
+ }
72
+ }
73
+ };
74
+
75
+ const handleUploadClick = () => {
76
+ if (fileInputRef.current && !isUploading) {
77
+ fileInputRef.current.click();
78
+ }
79
+ };
80
+
81
+ const handleRemove = () => {
82
+ if (onRemove && !disableRemove) {
83
+ onRemove();
84
+ }
85
+ };
86
+
87
+ // Container will use CSS modules classes
88
+ // Custom styling through props is deprecated in favor of CSS consistency
89
+
90
+ return (
91
+ <div className={`${styles.iconLoader} ${styles.iconLoader__container}`}>
92
+ {/* Show the icon/image */}
93
+ {hasCustomIcon ? (
94
+ <img
95
+ src={value}
96
+ alt="Custom icon"
97
+ className={styles.iconLoader__image}
98
+ />
99
+ ) : (
100
+ <div className={styles.iconLoader__iconContainer}>
101
+ {iconComponent ? (
102
+ <FontAwesomeIcon
103
+ icon={iconComponent}
104
+ className={`${styles.iconLoader__icon} ${styles.iconLoader__iconLarge}`}
105
+ />
106
+ ) : (
107
+ <div className={styles.iconLoader__fallbackText}>{iconToShow}</div>
108
+ )}
109
+ </div>
110
+ )}
111
+
112
+ {/* Upload/Delete button overlay - only shows on hover */}
113
+ <div className={styles.iconLoader__buttonOverlay}>
114
+ {hasCustomIcon ? (
115
+ <Button
116
+ buttonType="secondary"
117
+ isActive
118
+ onClick={handleRemove}
119
+ disabled={disableRemove || isUploading}
120
+ size="small"
121
+ leftIcon="times"
122
+ aria-label="Delete icon"
123
+ >
124
+ Delete
125
+ </Button>
126
+ ) : (
127
+ <Button
128
+ buttonType="primary"
129
+ onClick={() => {}}
130
+ disabled={isUploading}
131
+ size="small"
132
+ leftIcon="upload"
133
+ loading={isUploading}
134
+ aria-label="Upload icon"
135
+ >
136
+ Upload
137
+ </Button>
138
+ )}
139
+ </div>
140
+
141
+ {/* Hidden file input */}
142
+ <input
143
+ ref={fileInputRef}
144
+ type="file"
145
+ accept="image/jpeg,image/png,image/gif,image/webp,image/svg"
146
+ onChange={handleFileSelect}
147
+ className={styles.iconLoader__hiddenInput}
148
+ />
149
+ </div>
150
+ );
151
151
  };
152
152
 
153
153
  export default IconLoader;
@@ -6,7 +6,6 @@ import { allIcons, defaultIcon } from "./iconCategories";
6
6
  import { iconImports } from "./iconImports";
7
7
  import styles from "./IconSelector.module.css";
8
8
 
9
-
10
9
  const IconSelector = ({
11
10
  selectedIcon,
12
11
  onIconSelect,
@@ -32,7 +32,6 @@ import {
32
32
  import { fetchFeatureDefinitions } from "../actions/featureDefinitionsIndex.js";
33
33
  import { values } from "../values.config.js";
34
34
 
35
-
36
35
  const ListingEditor = ({ mode = "create", listingId, onSuccess, onCancel }) => {
37
36
  const isEditMode = mode === "edit";
38
37
  const dispatch = useDispatch();
@@ -48,8 +48,17 @@ const SideBarInner = (props) => {
48
48
 
49
49
  // Define step configuration with dynamic URL based on mode
50
50
  const getStepUrl = (stepKey) => {
51
- // Always use /definition/ in the URL since that's how routes are registered
52
- return `/feature-builder/definition/${stepKey}`;
51
+ // Use routes from values.config to support different variants (-a, -b, -c, -d)
52
+ switch (stepKey) {
53
+ case "overview":
54
+ return values.routeFormOverviewStep;
55
+ case "fields":
56
+ return values.routeFormFieldsStep;
57
+ case "layout":
58
+ return values.routeFormLayoutStep;
59
+ default:
60
+ return "";
61
+ }
53
62
  };
54
63
 
55
64
  const steps = [
@@ -75,6 +84,8 @@ const SideBarInner = (props) => {
75
84
 
76
85
  // Build sidebar items based on mode
77
86
  const buildSidebarItems = () => {
87
+ const isWizardMode = mode === "create" || mode === "edit";
88
+
78
89
  return steps.map((step, index) => {
79
90
  const isCompleted = selectIsStepComplete(step.key);
80
91
  const isAccessible = selectIsStepAccessible(step.key);
@@ -87,17 +98,19 @@ const SideBarInner = (props) => {
87
98
  text: stepText,
88
99
  icon: step.icon,
89
100
  selected: isSelected(step.url),
90
- onclick: isAccessible
91
- ? () => {
92
- goTo(step.url);
93
- dispatch(goToStep(step.key));
94
- }
95
- : null,
101
+ onclick: isWizardMode
102
+ ? null
103
+ : isAccessible
104
+ ? () => {
105
+ goTo(step.url);
106
+ dispatch(goToStep(step.key));
107
+ }
108
+ : null,
96
109
  isFontAwesome: true,
97
110
  // Enhanced completion indicator
98
111
  completed: isCompleted,
99
- // Allow navigation to completed steps even in create mode
100
- disabled: mode === "create" && !isAccessible,
112
+ // Disable all navigation in wizard mode
113
+ disabled: isWizardMode || (mode === "create" && !isAccessible),
101
114
  };
102
115
 
103
116
  return itemProps;
@@ -124,6 +137,8 @@ const SideBarInner = (props) => {
124
137
 
125
138
  // Add effect to manually attach click handlers since HubSidebar might not use onclick properly
126
139
  useEffect(() => {
140
+ const isWizardMode = mode === "create" || mode === "edit";
141
+
127
142
  const attachClickHandlers = () => {
128
143
  const stepsWithUrls = [
129
144
  { key: "overview", url: getStepUrl("overview") },
@@ -131,19 +146,12 @@ const SideBarInner = (props) => {
131
146
  { key: "layout", url: getStepUrl("layout") },
132
147
  ];
133
148
 
134
- stepsWithUrls.forEach((step) => {
149
+ stepsWithUrls.forEach((step, index) => {
135
150
  const navItem = Array.from(
136
- document.querySelectorAll(".sideNav-item"),
151
+ document.querySelectorAll(".hub-wrapperContainer .sideNav-item"),
137
152
  ).find(
138
153
  (item) =>
139
- item.textContent &&
140
- item.textContent.includes(
141
- step.key === "overview"
142
- ? "Feature Overview"
143
- : step.key === "fields"
144
- ? "Configure Fields"
145
- : "Choose Layout",
146
- ),
154
+ item.textContent && item.textContent.includes(`${index + 1}.`),
147
155
  );
148
156
 
149
157
  if (navItem) {
@@ -162,8 +170,12 @@ const SideBarInner = (props) => {
162
170
  },
163
171
  });
164
172
 
165
- // Add our click handler only if accessible
166
- if (isAccessible) {
173
+ // Check if this is the current step (selected)
174
+ const isCurrentStep = isSelected(step.url);
175
+
176
+ // In wizard mode, don't attach any click handlers
177
+ // Only attach click handler if not in wizard mode AND accessible
178
+ if (!isWizardMode && isAccessible) {
167
179
  navItem.onclick = (event) => {
168
180
  event.preventDefault();
169
181
  event.stopPropagation();
@@ -173,9 +185,19 @@ const SideBarInner = (props) => {
173
185
  }
174
186
 
175
187
  // Make sure it's styled appropriately
176
- navItem.style.cursor = isAccessible ? "pointer" : "not-allowed";
177
- navItem.style.pointerEvents = "auto";
178
- navItem.style.opacity = isAccessible ? "1" : "0.5";
188
+ // In wizard mode: block clicks, but keep current step visible
189
+ navItem.style.pointerEvents = isWizardMode ? "none" : "auto";
190
+
191
+ if (isWizardMode) {
192
+ // Current step: full opacity, not-allowed cursor
193
+ // Other steps: reduced opacity
194
+ navItem.style.opacity = isCurrentStep ? "1" : "0.4";
195
+ navItem.style.cursor = "not-allowed";
196
+ } else {
197
+ // Not in wizard mode: normal styling based on accessibility
198
+ navItem.style.cursor = isAccessible ? "pointer" : "not-allowed";
199
+ navItem.style.opacity = isAccessible ? "1" : "0.5";
200
+ }
179
201
  }
180
202
  });
181
203
  };
@@ -58,40 +58,6 @@
58
58
  }
59
59
 
60
60
  /* Enhanced sidebar navigation for progress indication */
61
- :global(.hub-sideBar) {
62
- /* Make sidebar more prominent during creation mode */
63
- }
64
-
65
- :global(.sideNav-item.isCurrent) {
66
- background: var(--colour-purple, #6e79c5) !important;
67
- color: var(--colour-white, #ffffff) !important;
68
- font-weight: 600 !important;
69
- border-left: 4px solid var(--colour-branding-dark, #364196) !important;
70
- }
71
-
72
- :global(.sideNav-item.isCurrent) i {
73
- color: var(--colour-white, #ffffff) !important;
74
- }
75
-
76
- :global(.sideNav-item.isCompleted) {
77
- color: var(--colour-branding-dark, #364196) !important;
78
- position: relative;
79
- }
80
-
81
- :global(.sideNav-item.isCompleted::after) {
82
- content: "✓";
83
- position: absolute;
84
- right: 1rem;
85
- top: 50%;
86
- transform: translateY(-50%);
87
- color: var(--colour-branding-dark, #364196);
88
- font-weight: bold;
89
- }
90
-
91
- :global(.sideNav-item.isDisabled) {
92
- opacity: 0.4;
93
- cursor: not-allowed;
94
- }
95
61
 
96
62
  /* Enhanced progress section */
97
63
  :global(.hub-sideBar-section) {
@@ -57,5 +57,3 @@ export { default as ToastContainer } from "./ToastContainer.jsx";
57
57
  // Gallery components
58
58
  export { default as ListingGalleryInput } from "./listing/ListingGalleryInput.jsx";
59
59
  export { default as GalleryDisplay } from "./listing/GalleryDisplay.jsx";
60
-
61
-
@@ -4,7 +4,6 @@ import { Text } from "../../components";
4
4
  import { iconImports } from "../../components/iconImports.js";
5
5
  import styles from "./GalleryDisplay.module.css";
6
6
 
7
-
8
7
  export const GalleryDisplay = (props) => {
9
8
  const {
10
9
  images = [],
@@ -186,7 +186,10 @@ const ListingGalleryInput = ({
186
186
  >
187
187
  {images.map((image, index) => {
188
188
  return (
189
- <div key={`image-${index}`} className={galleryStyles.imageItem}>
189
+ <div
190
+ key={`image-${index}`}
191
+ className={galleryStyles.imageItem}
192
+ >
190
193
  <img
191
194
  src={image}
192
195
  alt={`Gallery item ${index + 1}`}
@@ -3,8 +3,9 @@ import { useDispatch, useSelector } from "react-redux";
3
3
  import { fetchFeatureDefinitions } from "../actions/featureDefinitionsIndex";
4
4
  import {
5
5
  selectDefinition,
6
- selectDefinitionIsLoading,
7
6
  selectDefinitionError,
7
+ selectDefinitionIsLoading,
8
+ selectDefinitionMode,
8
9
  } from "../selectors/featureBuilderSelectors";
9
10
 
10
11
  /**
@@ -35,9 +36,12 @@ export const useFeatureDefinitionLoader = (options = {}) => {
35
36
  const definition = useSelector(selectDefinition);
36
37
  const isLoading = useSelector(selectDefinitionIsLoading);
37
38
  const error = useSelector(selectDefinitionError);
39
+ const mode = useSelector(selectDefinitionMode);
38
40
 
39
41
  // Determine if we need to load the definition
40
- const shouldLoadDefinition = !definition || forceReload;
42
+ // Only load if: definition is missing AND mode is not yet set (meaning we haven't tried to fetch yet)
43
+ // OR forceReload is true
44
+ const shouldLoadDefinition = (!definition && !mode) || forceReload;
41
45
 
42
46
  // Function to manually reload definition
43
47
  const reloadDefinition = () => {
package/src/index.js CHANGED
@@ -18,12 +18,12 @@ export const Reducers = (() => {
18
18
  export const Screens = (() => {
19
19
  const screens = {};
20
20
 
21
- screens[values.screenFormOverviewStep] = FormOverviewStep;
22
- screens[values.screenFormFieldsStep] = FormFieldsStep;
23
- screens[values.screenFormLayoutStep] = FormLayoutStep;
24
- screens[values.screenListingScreen] = ListingScreen;
25
- screens[values.pageCreateListing] = CreateListingPage;
26
- screens[values.pageEditListing] = EditListingPage;
21
+ screens["FormOverviewStep"] = FormOverviewStep;
22
+ screens["FormFieldsStep"] = FormFieldsStep;
23
+ screens["FormLayoutStep"] = FormLayoutStep;
24
+ screens["ListingScreen"] = ListingScreen;
25
+ screens["CreateListingPage"] = CreateListingPage;
26
+ screens["EditListingPage"] = EditListingPage;
27
27
  return screens;
28
28
  })();
29
29
 
@@ -277,28 +277,21 @@ const formReducer = (state = INITIAL_FORM_STATE, action) => {
277
277
  switch (type) {
278
278
  case formActionTypes.SET_INITIAL_VALUES: {
279
279
  // The actual definition data is in payload.featureDefinition.definition
280
- const definitionWrapper =
281
- payload && payload.featureDefinition
282
- ? payload.featureDefinition
283
- : payload;
284
- const definition =
285
- definitionWrapper && definitionWrapper.definition
286
- ? definitionWrapper.definition
287
- : definitionWrapper;
280
+ const definitionWrapper = payload?.featureDefinition ?? payload;
281
+ const definition = definitionWrapper?.definition ?? definitionWrapper;
288
282
 
289
283
  // Validate and map definition data to form state structure
290
284
  const mappedValues = {
291
- title: (definition && definition.title) || "",
292
- icon: (definition && definition.icon) || "star",
293
- displayName: (definition && definition.displayName) || "",
294
- layout: (definition && definition.layout) || {
285
+ title: definition?.title || "",
286
+ icon: definition?.icon || "star",
287
+ displayName: definition?.displayName || "",
288
+ layout: definition?.layout || {
295
289
  gridIcon: undefined,
296
290
  type: "round",
297
291
  },
298
- fields:
299
- definition && Array.isArray(definition.fields)
300
- ? definition.fields
301
- : state.fields,
292
+ fields: Array.isArray(definition?.fields)
293
+ ? definition.fields
294
+ : state.fields,
302
295
  };
303
296
 
304
297
  const newState = {
@@ -518,17 +511,13 @@ const definitionReducer = (state = INITIAL_STATE.definition, action) => {
518
511
  // Extract from API response for edit mode
519
512
  // Handle nested structure: data.featureDefinition.definition
520
513
  const featureDefinitionWrapper =
521
- data && data.featureDefinition ? data.featureDefinition : data;
514
+ data?.featureDefinition ?? data;
522
515
  definition =
523
- featureDefinitionWrapper && featureDefinitionWrapper.definition
524
- ? featureDefinitionWrapper.definition
525
- : featureDefinitionWrapper;
526
- definitionId =
527
- (featureDefinitionWrapper && featureDefinitionWrapper.id) ||
528
- values.featureId;
516
+ featureDefinitionWrapper?.definition ?? featureDefinitionWrapper;
517
+ definitionId = featureDefinitionWrapper?.id ?? values.featureId;
529
518
 
530
519
  // Ensure fields array exists and preserves order property
531
- if (definition && definition.fields) {
520
+ if (definition?.fields) {
532
521
  // Create a new array to ensure we preserve all field properties including order
533
522
  definition.fields = definition.fields.map((field) => ({
534
523
  ...field,
@@ -566,7 +555,7 @@ const definitionReducer = (state = INITIAL_STATE.definition, action) => {
566
555
  definition: payload, // Optimistically update with new definition
567
556
  mode: "edit", // Switch to edit mode immediately
568
557
  },
569
- id: payload && payload.id,
558
+ id: payload?.id,
570
559
  error: null,
571
560
  };
572
561
 
@@ -426,6 +426,11 @@
426
426
  cursor: pointer;
427
427
  transition: all 0.2s ease;
428
428
  background-color: transparent;
429
+ opacity: 0.6;
430
+ }
431
+
432
+ .layoutOption:hover {
433
+ opacity: 1;
429
434
  }
430
435
 
431
436
  .layoutOptionImage {
@@ -439,10 +444,14 @@
439
444
  }
440
445
 
441
446
  .layoutOption.selected .layoutOptionImage {
442
- border-color: var(--border-line-grey, #dbddf1);
447
+ border-color: var(--colour-purple, #6e79c5);
443
448
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
444
449
  }
445
450
 
451
+ .layoutOption.selected {
452
+ opacity: 1;
453
+ }
454
+
446
455
  .layoutOptionImg {
447
456
  width: 100%;
448
457
  height: 100%;
@@ -451,6 +460,7 @@
451
460
 
452
461
  .layoutOptionContent {
453
462
  max-width: 250px;
463
+ margin-bottom: 0.5rem;
454
464
  }
455
465
 
456
466
  .layoutOptionTitle {