@plusscommunities/pluss-feature-builder-web-b 1.0.2-beta.0 → 1.0.2-beta.10

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();
@@ -169,8 +168,8 @@ const ListingEditor = ({ mode = "create", listingId, onSuccess, onCancel }) => {
169
168
 
170
169
  // formData always has structure { fields: {...} } after our fixes
171
170
  const submissionData = isEditMode
172
- ? { id: listingId, fields: formData.fields }
173
- : { fields: formData.fields };
171
+ ? { id: listingId, fields: formData.fields, site: auth.site }
172
+ : { fields: formData.fields, site: auth.site };
174
173
 
175
174
  const actionPromise = dispatch(
176
175
  isEditMode ? editListing(submissionData) : createListing(submissionData),
@@ -1,32 +1,23 @@
1
- import React, { useEffect, useState } from "react";
1
+ import React, { useEffect } from "react";
2
2
  import { useSelector } from "react-redux";
3
3
  import { withRouter } from "react-router";
4
- import { HubSidebar, DeleteConfirmationPopup } from "./index.js";
4
+ import { HubSidebar } from "./index.js";
5
5
  import {
6
6
  selectWizardMode,
7
7
  selectCurrentStep,
8
8
  selectIsStepComplete,
9
9
  selectIsStepAccessible,
10
- selectStepProgress,
11
10
  selectIsEditMode,
12
11
  selectIsCreateMode,
13
- selectDefinition,
14
- selectDefinitionId,
15
12
  } from "../selectors/featureBuilderSelectors";
16
13
  import { goToStep } from "../actions/wizardActions";
17
14
  import { useDispatch } from "react-redux";
18
- import { PlussCore } from "../feature.config";
19
15
  import { values } from "../values.config";
20
16
  import styles from "./SidebarLayout.module.css";
21
17
 
22
- const { Helper, Session } = PlussCore;
23
- const { getUrl } = Helper;
24
- const { authedFunction } = Session;
25
-
26
18
  /**
27
19
  * Sidebar Layout component for feature builder wizard
28
20
  * Provides navigation sidebar with step progression, completion tracking
29
- * Includes delete functionality for feature definitions
30
21
  * Manages step accessibility based on wizard mode and validation state
31
22
  *
32
23
  * @param {Object} props - Component props
@@ -48,14 +39,6 @@ const SideBarInner = (props) => {
48
39
  const isEditMode = useSelector(selectIsEditMode);
49
40
  const isCreateMode = useSelector(selectIsCreateMode);
50
41
  const currentStep = useSelector(selectCurrentStep);
51
- const stepProgress = useSelector(selectStepProgress);
52
- const definition = useSelector(selectDefinition);
53
- const definitionId = useSelector(selectDefinitionId);
54
- const auth = useSelector((state) => state.auth);
55
-
56
- // Delete functionality state
57
- const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
58
- const [isDeleting, setIsDeleting] = useState(false);
59
42
 
60
43
  const goTo = (url) => history.push(url);
61
44
 
@@ -65,8 +48,17 @@ const SideBarInner = (props) => {
65
48
 
66
49
  // Define step configuration with dynamic URL based on mode
67
50
  const getStepUrl = (stepKey) => {
68
- // Always use /definition/ in the URL since that's how routes are registered
69
- 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
+ }
70
62
  };
71
63
 
72
64
  const steps = [
@@ -92,6 +84,8 @@ const SideBarInner = (props) => {
92
84
 
93
85
  // Build sidebar items based on mode
94
86
  const buildSidebarItems = () => {
87
+ const isWizardMode = mode === "create";
88
+
95
89
  return steps.map((step, index) => {
96
90
  const isCompleted = selectIsStepComplete(step.key);
97
91
  const isAccessible = selectIsStepAccessible(step.key);
@@ -104,17 +98,19 @@ const SideBarInner = (props) => {
104
98
  text: stepText,
105
99
  icon: step.icon,
106
100
  selected: isSelected(step.url),
107
- onclick: isAccessible
108
- ? () => {
109
- goTo(step.url);
110
- dispatch(goToStep(step.key));
111
- }
112
- : null,
101
+ onclick: isWizardMode
102
+ ? null
103
+ : isAccessible
104
+ ? () => {
105
+ goTo(step.url);
106
+ dispatch(goToStep(step.key));
107
+ }
108
+ : null,
113
109
  isFontAwesome: true,
114
110
  // Enhanced completion indicator
115
111
  completed: isCompleted,
116
- // Allow navigation to completed steps even in create mode
117
- disabled: mode === "create" && !isAccessible,
112
+ // Disable all navigation in create mode, otherwise respect accessibility
113
+ disabled: mode === "create" || !isAccessible,
118
114
  };
119
115
 
120
116
  return itemProps;
@@ -139,36 +135,10 @@ const SideBarInner = (props) => {
139
135
  },
140
136
  ];
141
137
 
142
- // Delete functionality handlers
143
- const handleDeleteClick = () => {
144
- setShowDeleteConfirm(true);
145
- };
146
-
147
- const handleConfirmDelete = async () => {
148
- if (!definition) return;
149
-
150
- setIsDeleting(true);
151
- try {
152
- await authedFunction({
153
- method: "POST",
154
- url: getUrl("feature-builder", "definition/update/delete"),
155
- data: { id: definitionId, site: auth.site },
156
- });
157
-
158
- // Redirect to welcome screen after deletion
159
- history.push("/feature-builder/definition/overview");
160
- } finally {
161
- setIsDeleting(false);
162
- setShowDeleteConfirm(false);
163
- }
164
- };
165
-
166
- const handleCancelDelete = () => {
167
- setShowDeleteConfirm(false);
168
- };
169
-
170
138
  // Add effect to manually attach click handlers since HubSidebar might not use onclick properly
171
139
  useEffect(() => {
140
+ const isWizardMode = mode === "create";
141
+
172
142
  const attachClickHandlers = () => {
173
143
  const stepsWithUrls = [
174
144
  { key: "overview", url: getStepUrl("overview") },
@@ -176,19 +146,12 @@ const SideBarInner = (props) => {
176
146
  { key: "layout", url: getStepUrl("layout") },
177
147
  ];
178
148
 
179
- stepsWithUrls.forEach((step) => {
149
+ stepsWithUrls.forEach((step, index) => {
180
150
  const navItem = Array.from(
181
- document.querySelectorAll(".sideNav-item"),
151
+ document.querySelectorAll(".hub-wrapperContainer .sideNav-item"),
182
152
  ).find(
183
153
  (item) =>
184
- item.textContent &&
185
- item.textContent.includes(
186
- step.key === "overview"
187
- ? "Feature Overview"
188
- : step.key === "fields"
189
- ? "Configure Fields"
190
- : "Choose Layout",
191
- ),
154
+ item.textContent && item.textContent.includes(`${index + 1}.`),
192
155
  );
193
156
 
194
157
  if (navItem) {
@@ -207,8 +170,12 @@ const SideBarInner = (props) => {
207
170
  },
208
171
  });
209
172
 
210
- // Add our click handler only if accessible
211
- 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) {
212
179
  navItem.onclick = (event) => {
213
180
  event.preventDefault();
214
181
  event.stopPropagation();
@@ -218,9 +185,19 @@ const SideBarInner = (props) => {
218
185
  }
219
186
 
220
187
  // Make sure it's styled appropriately
221
- navItem.style.cursor = isAccessible ? "pointer" : "not-allowed";
222
- navItem.style.pointerEvents = "auto";
223
- 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
+ }
224
201
  }
225
202
  });
226
203
  };
@@ -263,28 +240,11 @@ const SideBarInner = (props) => {
263
240
  />
264
241
  )}
265
242
 
266
- {/* Dev Delete Button - Absolute positioned at bottom of sidebar */}
267
- {shouldShowSidebar && (
268
- <button onClick={handleDeleteClick} className={styles.devDeleteButton}>
269
- DELETE FEATURE DEF
270
- </button>
271
- )}
272
-
273
243
  <div className="hub-contentWrapper">
274
244
  <div className={styles.fullWidthContainer}>
275
245
  <div className={styles.contentContainer}>{children}</div>
276
246
  </div>
277
247
  </div>
278
-
279
- {/* Delete Confirmation Popup */}
280
- <DeleteConfirmationPopup
281
- isOpen={showDeleteConfirm}
282
- listing={definition}
283
- onConfirm={handleConfirmDelete}
284
- onCancel={handleCancelDelete}
285
- isDeleting={isDeleting}
286
- deleteType="featureDefinition"
287
- />
288
248
  </div>
289
249
  );
290
250
  };
@@ -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) {
@@ -103,43 +69,3 @@
103
69
  :global(.hub-sideBar-section:last-child) {
104
70
  border-bottom: none;
105
71
  }
106
-
107
- /* Delete button styling - bold red for development */
108
- .deleteButton {
109
- color: #dc3545 !important;
110
- font-weight: bold !important;
111
- }
112
-
113
- :global(.sideNav-item.deleteButton) {
114
- color: #dc3545 !important;
115
- font-weight: bold !important;
116
- }
117
-
118
- :global(.sideNav-item.deleteButton:hover) {
119
- background-color: rgba(220, 53, 69, 0.1) !important;
120
- }
121
-
122
- :global(.sideNav-item.deleteButton i) {
123
- color: #dc3545 !important;
124
- }
125
-
126
- /* Development delete button */
127
- .devDeleteButton {
128
- position: absolute;
129
- bottom: 20px;
130
- left: 20px;
131
- background-color: #ff0000;
132
- color: #ffffff;
133
- border: none;
134
- padding: 8px 12px;
135
- border-radius: 4px;
136
- cursor: pointer;
137
- font-size: 12px;
138
- font-weight: bold;
139
- z-index: 1000;
140
- box-shadow: 0 2px 4px rgba(0,0,0,0.3);
141
- }
142
-
143
- .devDeleteButton:hover {
144
- background-color: #cc0000;
145
- }
@@ -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 = [],