@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.
@@ -14,27 +14,26 @@ const ListingFileInput = ({
14
14
  errorMessage,
15
15
  disabled = false,
16
16
  }) => {
17
- const fileInputRef = useRef(null);
18
- const [internalFiles, setInternalFiles] = useState([]);
17
+ const [inputs, setInputs] = useState([{ id: 0, fileUrl: null }]);
18
+ const nextIdRef = useRef(1);
19
19
 
20
- // Normalize file data to enhanced structure - always handle as multiple files
20
+ // Initialize inputs from value prop - map each existing file to an input
21
21
  useEffect(() => {
22
22
  if (value) {
23
+ let fileData = [];
24
+
23
25
  // Handle multiple files - could be array of URLs or array of file objects
24
26
  if (Array.isArray(value)) {
25
- const normalizedFiles = value
27
+ fileData = value
26
28
  .map((file) => {
27
29
  if (typeof file === "string") {
28
- // Backward compatibility: array of URLs
29
30
  const fileName = file.split("/").pop() || "Unknown File";
30
31
  return {
31
32
  url: file,
32
- name: fileName.replace(/\.[^/.]+$/, ""), // Remove extension
33
+ name: fileName.replace(/\.[^/.]+$/, ""),
33
34
  originalName: fileName,
34
- uploading: false,
35
35
  };
36
36
  } else if (file && typeof file === "object" && file.url) {
37
- // Enhanced structure
38
37
  return {
39
38
  url: file.url,
40
39
  name: file.name || file.originalName || "Unknown File",
@@ -42,27 +41,22 @@ const ListingFileInput = ({
42
41
  file.originalName ||
43
42
  file.url.split("/").pop() ||
44
43
  "Unknown File",
45
- uploading: false,
46
44
  };
47
45
  }
48
46
  return null;
49
47
  })
50
48
  .filter(Boolean);
51
- setInternalFiles(normalizedFiles);
52
49
  } else {
53
- // Handle single file values - normalize to array
50
+ // Handle single file values
54
51
  let singleFile = null;
55
52
  if (typeof value === "string") {
56
- // Backward compatibility: single URL
57
53
  const fileName = value.split("/").pop() || "Unknown File";
58
54
  singleFile = {
59
55
  url: value,
60
56
  name: fileName.replace(/\.[^/.]+$/, ""),
61
57
  originalName: fileName,
62
- uploading: false,
63
58
  };
64
59
  } else if (value && typeof value === "object" && value.url) {
65
- // Enhanced structure - preserve empty name if explicitly set
66
60
  singleFile = {
67
61
  url: value.url,
68
62
  name:
@@ -73,60 +67,104 @@ const ListingFileInput = ({
73
67
  value.originalName ||
74
68
  value.url.split("/").pop() ||
75
69
  "Unknown File",
76
- uploading: false,
77
70
  };
78
71
  }
79
- setInternalFiles(singleFile ? [singleFile] : []);
72
+ fileData = singleFile ? [singleFile] : [];
80
73
  }
74
+
75
+ // Create inputs for each existing file
76
+ const existingInputs = fileData.map((file, index) => ({
77
+ id: index,
78
+ fileUrl: file.url,
79
+ name: file.name,
80
+ originalName: file.originalName,
81
+ }));
82
+ // Add one empty input at the end
83
+ nextIdRef.current = fileData.length;
84
+ setInputs([...existingInputs, { id: nextIdRef.current, fileUrl: null }]);
85
+ nextIdRef.current++;
81
86
  } else {
82
- setInternalFiles([]);
87
+ setInputs([{ id: 0, fileUrl: null }]);
83
88
  }
84
89
  }, [value]);
85
90
 
86
91
  // Handle new file upload from FileInput component
87
- const handleFileUpload = (uploadedUrl) => {
88
- // Handle case where uploadedUrl might be undefined
92
+ const handleFileUpload = (inputId, uploadedUrl) => {
89
93
  if (!uploadedUrl) {
90
94
  return;
91
95
  }
92
96
 
93
97
  const fileName = uploadedUrl.split("/").pop() || "Unknown File";
94
- const newFile = {
95
- url: uploadedUrl,
96
- name: "", // Start with empty name as requested
97
- originalName: fileName,
98
- uploading: false,
99
- };
100
-
101
- // Always add new file to existing array for multiple file support
102
- const updatedFiles = [...internalFiles, newFile];
103
- setInternalFiles(updatedFiles);
104
- notifyParent(updatedFiles);
98
+
99
+ // Find the input that triggered this upload and update it
100
+ setInputs((prevInputs) => {
101
+ const updatedInputs = prevInputs.map((input) => {
102
+ if (input.id === inputId) {
103
+ return {
104
+ ...input,
105
+ fileUrl: uploadedUrl,
106
+ name: "",
107
+ originalName: fileName,
108
+ };
109
+ }
110
+ return input;
111
+ });
112
+
113
+ // Add a new empty input for the next upload
114
+ updatedInputs.push({ id: nextIdRef.current, fileUrl: null });
115
+ nextIdRef.current++;
116
+
117
+ return updatedInputs;
118
+ });
105
119
  };
106
120
 
107
121
  // Handle file name change
108
122
  const handleNameChange = (url, newName) => {
109
- const updatedFiles = internalFiles.map((file) =>
110
- file.url === url ? { ...file, name: newName } : file,
123
+ setInputs((prevInputs) =>
124
+ prevInputs.map((input) =>
125
+ input.fileUrl === url ? { ...input, name: newName } : input,
126
+ ),
111
127
  );
112
- setInternalFiles(updatedFiles);
113
- notifyParent(updatedFiles);
114
128
  };
115
129
 
116
130
  // Handle file removal
117
131
  const handleFileRemove = (url) => {
118
- const updatedFiles = internalFiles.filter((file) => file.url !== url);
119
- setInternalFiles(updatedFiles);
120
- notifyParent(updatedFiles);
132
+ setInputs((prevInputs) => {
133
+ return prevInputs.map((input) => {
134
+ if (input.fileUrl === url) {
135
+ return { ...input, fileUrl: null };
136
+ }
137
+ return input;
138
+ });
139
+ });
121
140
  };
122
141
 
123
- // Notify parent component of changes
124
- const notifyParent = (files) => {
125
- if (onChange) {
126
- // Always return array of files for multiple file support
142
+ // Get all uploaded files from inputs
143
+ const getUploadedFiles = () => {
144
+ return inputs
145
+ .filter((input) => input.fileUrl !== null)
146
+ .map((input) => ({
147
+ url: input.fileUrl,
148
+ name: input.name,
149
+ originalName: input.originalName,
150
+ }));
151
+ };
152
+
153
+ // Track previous uploaded files to prevent infinite loops
154
+ const prevFilesRef = useRef([]);
155
+
156
+ // Sync with parent when uploaded files actually change
157
+ useEffect(() => {
158
+ const files = getUploadedFiles();
159
+ const currentUrls = files.map((f) => f.url).join(",");
160
+ const prevUrls = prevFilesRef.current.map((f) => f.url).join(",");
161
+
162
+ // Only call onChange if the file list actually changed
163
+ if (currentUrls !== prevUrls && onChange) {
164
+ prevFilesRef.current = files;
127
165
  onChange(field.id, files);
128
166
  }
129
- };
167
+ }, [inputs]);
130
168
 
131
169
  // Define acceptable file types for documents and common formats
132
170
  const acceptTypes = {
@@ -145,7 +183,11 @@ const ListingFileInput = ({
145
183
  "image/png": [".png"],
146
184
  };
147
185
 
148
- // Always show drop zone regardless of existing files
186
+ // Get the last empty input (the one to show)
187
+ const emptyInput = inputs.find((input) => input.fileUrl === null);
188
+
189
+ // Get uploaded files for display
190
+ const uploadedFiles = getUploadedFiles();
149
191
 
150
192
  return (
151
193
  <div className={styles.listingFileInput}>
@@ -169,25 +211,26 @@ const ListingFileInput = ({
169
211
  )}
170
212
  </Text>
171
213
 
172
- {/* Drop zone */}
214
+ {/* Drop zone - always show exactly one empty input */}
173
215
  <div
174
216
  className={`${styles.listingFileInput__dropZone} ${showError ? styles["listingFileInput__dropZone--error"] : ""}`}
175
217
  >
176
- <FileInput
177
- ref={fileInputRef}
178
- className="imageInputOuter-single"
179
- refreshCallback={handleFileUpload}
180
- hasDefault={null}
181
- accept={acceptTypes}
182
- multiple={false} // Always handle one file at a time in the upload callback
183
- disabled={disabled}
184
- />
218
+ {emptyInput && (
219
+ <FileInput
220
+ key={emptyInput.id}
221
+ refreshCallback={(url) => handleFileUpload(emptyInput.id, url)}
222
+ hasDefault={null}
223
+ accept={acceptTypes}
224
+ multiple={false}
225
+ disabled={disabled}
226
+ />
227
+ )}
185
228
  </div>
186
229
 
187
230
  {/* File list */}
188
- {internalFiles.length > 0 && (
231
+ {uploadedFiles.length > 0 && (
189
232
  <div className={styles.listingFileInput__fileList}>
190
- {internalFiles.map((file) => (
233
+ {uploadedFiles.map((file) => (
191
234
  <FileListItem
192
235
  key={file.url}
193
236
  file={file}
@@ -199,31 +242,6 @@ const ListingFileInput = ({
199
242
  </div>
200
243
  )}
201
244
 
202
- {/* Add more files button - always show when files exist */}
203
- {internalFiles.length > 0 && (
204
- <div>
205
- <FileInput
206
- ref={fileInputRef}
207
- className="imageInputOuter-single"
208
- refreshCallback={handleFileUpload}
209
- hasDefault={null}
210
- accept={acceptTypes}
211
- multiple={false}
212
- disabled={disabled}
213
- customButton={
214
- <Button
215
- buttonType="secondary"
216
- className={styles.listingFileInput__addMoreButton}
217
- disabled={disabled}
218
- >
219
- <FontAwesomeIcon icon={iconImports.plus} />
220
- Add More Files
221
- </Button>
222
- }
223
- />
224
- </div>
225
- )}
226
-
227
245
  {showError && errorMessage && (
228
246
  <Text
229
247
  type="help"
@@ -38,9 +38,7 @@
38
38
  display: flex;
39
39
  align-items: center;
40
40
  gap: 0.75rem;
41
- padding: 0.75rem;
42
41
  background-color: var(--bg-white);
43
- margin-bottom: 0.5rem;
44
42
  transition: all var(--transition-base) ease-in-out;
45
43
  }
46
44
 
@@ -50,9 +48,8 @@
50
48
 
51
49
  .fileListItem__icon {
52
50
  color: var(--text-bluegrey);
53
- font-size: var(--font-size-base);
51
+ font-size: 2rem;
54
52
  flex-shrink: 0;
55
- width: 1.5rem;
56
53
  text-align: center;
57
54
  }
58
55
 
@@ -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,6 +3,7 @@
3
3
  /* Gallery container styles */
4
4
  .galleryContainer {
5
5
  margin-top: 0.75rem;
6
+ max-width: 200px;
6
7
  }
7
8
 
8
9
  /* Error border wrapper */
@@ -20,7 +21,7 @@
20
21
 
21
22
  .imageGrid {
22
23
  display: grid;
23
- grid-template-columns: repeat(var(--columns, 4), 1fr);
24
+ grid-template-columns: repeat(4, 160px);
24
25
  gap: 1rem;
25
26
  margin-top: 0.5rem;
26
27
  }
@@ -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 = () => {
Binary file
Binary file
Binary file
Binary file
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["FormOverviewStep"] = FormOverviewStep;
22
- screens["FormFieldsStep"] = FormFieldsStep;
23
- screens["FormLayoutStep"] = FormLayoutStep;
24
- screens["ListingScreen"] = ListingScreen;
25
- screens["CreateListingPage"] = CreateListingPage;
26
- screens["EditListingPage"] = EditListingPage;
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;
27
27
  return screens;
28
28
  })();
29
29
 
@@ -23,6 +23,7 @@
23
23
  */
24
24
  // Import action types from existing files
25
25
  import { actionsTypes as formActionTypes } from "../actions/formActions";
26
+ const { SUBMIT_FORM_SUCCESS } = formActionTypes;
26
27
  import {
27
28
  FETCH_FEATURES_REQUEST,
28
29
  FETCH_FEATURES_SUCCESS,
@@ -86,7 +87,7 @@ const DEFAULT_NEW_FIELD = {
86
87
  allowCaption: false, // Default to not allowing captions for image fields
87
88
  useAsSummary: false, // Default to not using as summary for description fields
88
89
  minImages: 1, // Gallery-specific default
89
- maxImages: 10, // Gallery-specific default
90
+ maxImages: 16, // Gallery-specific default
90
91
  maxFileSize: "5MB", // Gallery-specific default
91
92
  allowedTypes: [], // Gallery-specific default
92
93
  },
@@ -138,7 +139,7 @@ function createNewField(id, type = "text", existingFields = []) {
138
139
  baseField.values.label = "Image Gallery";
139
140
  baseField.values.helpText = "Upload multiple images to create a gallery";
140
141
  baseField.values.minImages = 1;
141
- baseField.values.maxImages = 10;
142
+ baseField.values.maxImages = 16;
142
143
  baseField.values.maxFileSize = "5MB";
143
144
  baseField.values.allowedTypes = ["image/jpeg", "image/png", "image/webp"];
144
145
  const {
@@ -278,27 +279,23 @@ const formReducer = (state = INITIAL_FORM_STATE, action) => {
278
279
  case formActionTypes.SET_INITIAL_VALUES: {
279
280
  // The actual definition data is in payload.featureDefinition.definition
280
281
  const definitionWrapper =
281
- payload && payload.featureDefinition
282
- ? payload.featureDefinition
283
- : payload;
282
+ (payload && payload.featureDefinition) || payload;
284
283
  const definition =
285
- definitionWrapper && definitionWrapper.definition
286
- ? definitionWrapper.definition
287
- : definitionWrapper;
284
+ (definitionWrapper && definitionWrapper.definition) ||
285
+ definitionWrapper;
288
286
 
289
287
  // Validate and map definition data to form state structure
290
288
  const mappedValues = {
291
- title: (definition && definition.title) || "",
292
- icon: (definition && definition.icon) || "star",
293
- displayName: (definition && definition.displayName) || "",
294
- layout: (definition && definition.layout) || {
289
+ title: definition?.title || "",
290
+ icon: definition?.icon || "star",
291
+ displayName: definition?.displayName || "",
292
+ layout: definition?.layout || {
295
293
  gridIcon: undefined,
296
294
  type: "round",
297
295
  },
298
- fields:
299
- definition && Array.isArray(definition.fields)
300
- ? definition.fields
301
- : state.fields,
296
+ fields: Array.isArray(definition?.fields)
297
+ ? definition.fields
298
+ : state.fields,
302
299
  };
303
300
 
304
301
  const newState = {
@@ -518,17 +515,16 @@ const definitionReducer = (state = INITIAL_STATE.definition, action) => {
518
515
  // Extract from API response for edit mode
519
516
  // Handle nested structure: data.featureDefinition.definition
520
517
  const featureDefinitionWrapper =
521
- data && data.featureDefinition ? data.featureDefinition : data;
518
+ (data && data.featureDefinition) || data;
522
519
  definition =
523
- featureDefinitionWrapper && featureDefinitionWrapper.definition
524
- ? featureDefinitionWrapper.definition
525
- : featureDefinitionWrapper;
520
+ (featureDefinitionWrapper && featureDefinitionWrapper.definition) ||
521
+ featureDefinitionWrapper;
526
522
  definitionId =
527
523
  (featureDefinitionWrapper && featureDefinitionWrapper.id) ||
528
524
  values.featureId;
529
525
 
530
526
  // Ensure fields array exists and preserves order property
531
- if (definition && definition.fields) {
527
+ if (definition?.fields) {
532
528
  // Create a new array to ensure we preserve all field properties including order
533
529
  definition.fields = definition.fields.map((field) => ({
534
530
  ...field,
@@ -566,7 +562,7 @@ const definitionReducer = (state = INITIAL_STATE.definition, action) => {
566
562
  definition: payload, // Optimistically update with new definition
567
563
  mode: "edit", // Switch to edit mode immediately
568
564
  },
569
- id: payload && payload.id,
565
+ id: payload?.id,
570
566
  error: null,
571
567
  };
572
568
 
@@ -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 {