@visns-studio/visns-components 5.8.11 → 5.8.13

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/package.json CHANGED
@@ -4,14 +4,14 @@
4
4
  "@fontsource/barlow": "^5.2.5",
5
5
  "@inovua/reactdatagrid-community": "^5.10.2",
6
6
  "@inovua/reactdatagrid-enterprise": "^5.10.2",
7
- "@nivo/bar": "^0.96.0",
8
- "@nivo/core": "^0.96.0",
9
- "@nivo/line": "^0.96.0",
10
- "@nivo/pie": "^0.96.0",
7
+ "@nivo/bar": "^0.98.0",
8
+ "@nivo/core": "^0.98.0",
9
+ "@nivo/line": "^0.98.0",
10
+ "@nivo/pie": "^0.98.0",
11
11
  "@tinymce/tinymce-react": "^6.1.0",
12
12
  "@visns-studio/visns-datagrid-community": "^1.0.14",
13
13
  "@visns-studio/visns-datagrid-enterprise": "^1.0.14",
14
- "@vitejs/plugin-react": "^4.4.1",
14
+ "@vitejs/plugin-react": "^4.5.0",
15
15
  "add": "^2.0.6",
16
16
  "akar-icons": "^1.9.31",
17
17
  "array-move": "^4.0.0",
@@ -20,12 +20,12 @@
20
20
  "dayjs": "^1.11.13",
21
21
  "fabric": "^6.6.5",
22
22
  "file-saver": "^2.0.5",
23
- "framer-motion": "^12.11.3",
23
+ "framer-motion": "^12.12.1",
24
24
  "html-react-parser": "^5.2.5",
25
25
  "lodash": "^4.17.21",
26
26
  "lodash.debounce": "^4.0.8",
27
27
  "moment": "^2.30.1",
28
- "motion": "^12.11.3",
28
+ "motion": "^12.12.1",
29
29
  "numeral": "^2.0.6",
30
30
  "pluralize": "^8.0.0",
31
31
  "qrcode.react": "^4.2.0",
@@ -41,7 +41,7 @@
41
41
  "react-promise-tracker": "^2.1.1",
42
42
  "react-quill": "^2.0.0",
43
43
  "react-reveal": "^1.2.2",
44
- "react-router-dom": "^6.30.0",
44
+ "react-router-dom": "^6.30.1",
45
45
  "react-select": "^5.10.1",
46
46
  "react-signature-pad-wrapper": "^4.1.0",
47
47
  "react-slugify": "^4.0.1",
@@ -54,13 +54,13 @@
54
54
  "reactjs-popup": "^2.0.6",
55
55
  "style-loader": "^4.0.0",
56
56
  "swapy": "^1.0.5",
57
- "sweetalert2": "^11.21.0",
57
+ "sweetalert2": "^11.21.2",
58
58
  "truncate": "^3.0.0",
59
59
  "uuid": "^11.1.0",
60
60
  "validator": "^13.15.0",
61
61
  "vite": "^6.3.5",
62
62
  "yarn": "^1.22.22",
63
- "yet-another-react-lightbox": "^3.23.1"
63
+ "yet-another-react-lightbox": "^3.23.2"
64
64
  },
65
65
  "devDependencies": {
66
66
  "@babel/core": "^7.27.1",
@@ -74,7 +74,7 @@
74
74
  "mini-css-extract-plugin": "^2.9.2",
75
75
  "react": "^18.3.1",
76
76
  "react-dom": "^18.3.1",
77
- "sass": "^1.88.0",
77
+ "sass": "^1.89.0",
78
78
  "sass-loader": "^16.0.5"
79
79
  },
80
80
  "peerDependencies": {
@@ -82,7 +82,7 @@
82
82
  "react-dom": "^17.0.0 || ^18.0.0"
83
83
  },
84
84
  "name": "@visns-studio/visns-components",
85
- "version": "5.8.11",
85
+ "version": "5.8.13",
86
86
  "description": "Various packages to assist in the development of our Custom Applications.",
87
87
  "main": "src/index.js",
88
88
  "files": [
@@ -1,6 +1,6 @@
1
- import React, { useState, useEffect, useCallback, useMemo, memo } from 'react';
1
+ import { useState, useEffect, useCallback, useMemo, memo, useRef } from 'react';
2
2
  import AwesomeDebouncePromise from 'awesome-debounce-promise';
3
- import { ToggleOffFill, ToggleOnFill } from 'akar-icons';
3
+ import { ToggleOffFill, ToggleOnFill, Search } from 'akar-icons';
4
4
  import styles from './styles/Autocomplete.module.scss';
5
5
  import fetchUtil from '../../utils/fetchUtil';
6
6
 
@@ -20,12 +20,16 @@ const VisnsAutocomplete = memo((props) => {
20
20
  onSelect,
21
21
  setFormData,
22
22
  style,
23
+ placeholder = 'Type an address',
23
24
  } = props;
24
25
 
25
26
  const [search, setSearch] = useState('');
26
27
  const [results, setResults] = useState([]);
27
28
  const [isLoading, setIsLoading] = useState(false);
28
29
  const [overwrite, setOverwrite] = useState(false);
30
+ const [isFocused, setIsFocused] = useState(false);
31
+ const inputRef = useRef(null);
32
+ const resultsRef = useRef(null);
29
33
 
30
34
  // Memoize URL building logic to avoid recalculations
31
35
  const buildSearchUrl = useCallback(
@@ -67,11 +71,25 @@ const VisnsAutocomplete = memo((props) => {
67
71
  fetchUtil
68
72
  .get(url)
69
73
  .then((response) => {
70
- setResults(response.data.features || []);
74
+ // Ensure we're accessing the features array from the response
75
+ // Check if response.data is a string (sometimes happens with certain APIs)
76
+ let parsedData = response.data;
77
+ if (typeof response.data === 'string') {
78
+ try {
79
+ parsedData = JSON.parse(response.data);
80
+ } catch (e) {
81
+ console.error('Error parsing response data:', e);
82
+ }
83
+ }
84
+
85
+ const features = parsedData?.features || [];
86
+
87
+ setResults(features);
71
88
  setIsLoading(false);
72
89
  })
73
90
  .catch((error) => {
74
- console.log(error);
91
+ console.error('Error fetching address suggestions:', error);
92
+ setResults([]);
75
93
  setIsLoading(false);
76
94
  });
77
95
  },
@@ -105,15 +123,48 @@ const VisnsAutocomplete = memo((props) => {
105
123
 
106
124
  const handleItemClicked = useCallback(
107
125
  (place) => {
108
- const address = place.address || '';
109
- const text = place.text || '';
110
- const search = address && text ? `${address} ${text}` : text;
126
+ // Use place_name as the primary display value as it contains the full formatted address
127
+ const displayValue = place.place_name || '';
128
+
129
+ // If place_name is not available, construct from components
130
+ const fallbackValue = (() => {
131
+ const address = place.address || '';
132
+ const text = place.text || '';
133
+ const context = place.context
134
+ ? place.context
135
+ .filter((ctx) => ctx.text)
136
+ .map((ctx) => ctx.text)
137
+ .join(', ')
138
+ : '';
139
+
140
+ if (address && text) {
141
+ return context
142
+ ? `${address} ${text}, ${context}`
143
+ : `${address} ${text}`;
144
+ } else if (text) {
145
+ return context ? `${text}, ${context}` : text;
146
+ }
147
+ return '';
148
+ })();
149
+
150
+ const finalValue = displayValue || fallbackValue;
111
151
 
112
- setSearch(search);
152
+ setSearch(finalValue);
113
153
  setResults([]);
114
- onSelect(place, field);
154
+ setIsFocused(false);
155
+
156
+ // Update form data with the selected value
157
+ setFormData((prevState) => ({
158
+ ...prevState,
159
+ [field]: finalValue,
160
+ }));
161
+
162
+ // Call the onSelect callback with the place object and field
163
+ if (onSelect) {
164
+ onSelect(place, field);
165
+ }
115
166
  },
116
- [field, onSelect]
167
+ [field, onSelect, setFormData]
117
168
  );
118
169
 
119
170
  const handleOverwrite = useCallback(() => {
@@ -148,54 +199,165 @@ const VisnsAutocomplete = memo((props) => {
148
199
  [style]
149
200
  );
150
201
 
202
+ // Handle focus and blur events
203
+ const handleFocus = useCallback(() => {
204
+ setIsFocused(true);
205
+ if (!overwrite && search && search.length > 1) {
206
+ setIsLoading(true);
207
+ performSearch(1, search);
208
+ }
209
+ }, [overwrite, search, performSearch]);
210
+
211
+ const handleBlur = useCallback(() => {
212
+ // Delay hiding results to allow for click events on results
213
+ setTimeout(() => {
214
+ setIsFocused(false);
215
+ }, 200);
216
+ }, []);
217
+
218
+ // Handle click outside to close results
219
+ useEffect(() => {
220
+ const handleClickOutside = (event) => {
221
+ if (
222
+ resultsRef.current &&
223
+ !resultsRef.current.contains(event.target) &&
224
+ inputRef.current &&
225
+ !inputRef.current.contains(event.target)
226
+ ) {
227
+ setIsFocused(false);
228
+ }
229
+ };
230
+
231
+ document.addEventListener('mousedown', handleClickOutside);
232
+ return () => {
233
+ document.removeEventListener('mousedown', handleClickOutside);
234
+ };
235
+ }, []);
236
+
151
237
  return (
152
238
  <div className={styles.AutocompletePlace}>
153
- <input
154
- className={styles['AutocompletePlace-input']}
155
- style={inputStyle}
156
- type="text"
157
- value={search || ''}
158
- onChange={handleSearchChange}
159
- placeholder="Type an address"
160
- autoComplete="off"
161
- />
162
-
163
- {overwrite ? (
164
- <ToggleOnFill
165
- data-tooltip-id="system-tooltip"
166
- data-tooltip-content="Enable Autocomplete"
167
- strokeWidth={2}
168
- size={24}
169
- onClick={handleOverwrite}
170
- className={styles.toggleActive}
239
+ <div className={styles['AutocompletePlace-inputWrapper']}>
240
+ <Search
241
+ size={16}
242
+ className={styles['AutocompletePlace-searchIcon']}
171
243
  />
172
- ) : (
173
- <ToggleOffFill
174
- data-tooltip-id="system-tooltip"
175
- data-tooltip-content="Disable Autocomplete"
176
- strokeWidth={2}
177
- size={24}
178
- onClick={handleOverwrite}
244
+ <input
245
+ ref={inputRef}
246
+ className={styles['AutocompletePlace-input']}
247
+ style={inputStyle}
248
+ type="text"
249
+ value={search || ''}
250
+ onChange={handleSearchChange}
251
+ onFocus={handleFocus}
252
+ onBlur={handleBlur}
253
+ placeholder={placeholder}
254
+ autoComplete="off"
179
255
  />
180
- )}
181
256
 
182
- {results.length > 0 && (
183
- <ul className="AutocompletePlace-results">
184
- {results.map((place) => (
257
+ {overwrite ? (
258
+ <ToggleOnFill
259
+ data-tooltip-id="system-tooltip"
260
+ data-tooltip-content="Enable Autocomplete"
261
+ strokeWidth={2}
262
+ size={24}
263
+ onClick={handleOverwrite}
264
+ className={styles.toggleActive}
265
+ />
266
+ ) : (
267
+ <ToggleOffFill
268
+ data-tooltip-id="system-tooltip"
269
+ data-tooltip-content="Disable Autocomplete"
270
+ strokeWidth={2}
271
+ size={24}
272
+ onClick={handleOverwrite}
273
+ />
274
+ )}
275
+ </div>
276
+
277
+ {isFocused && (search.length > 1 || isLoading) && (
278
+ <ul
279
+ ref={resultsRef}
280
+ className={styles['AutocompletePlace-results']}
281
+ >
282
+ {isLoading ? (
185
283
  <li
186
- key={place.id}
187
- className={styles['AutocompletePlace-items']}
188
- onClick={() => handleItemClicked(place)}
284
+ className={`${styles['AutocompletePlace-items']} ${styles['AutocompletePlace-loading']}`}
189
285
  >
190
- {place.place_name}
191
- </li>
192
- ))}
193
-
194
- {isLoading && (
195
- <li className={styles['AutocompletePlace-items']}>
196
286
  Loading...
197
287
  </li>
198
- )}
288
+ ) : Array.isArray(results) && results.length > 0 ? (
289
+ results.map((place) => (
290
+ <li
291
+ key={place.id}
292
+ className={styles['AutocompletePlace-items']}
293
+ onClick={() => handleItemClicked(place)}
294
+ >
295
+ <div
296
+ className={
297
+ styles['AutocompletePlace-itemMain']
298
+ }
299
+ >
300
+ {place.address && (
301
+ <span
302
+ className={
303
+ styles[
304
+ 'AutocompletePlace-itemAddress'
305
+ ]
306
+ }
307
+ >
308
+ {place.address}
309
+ </span>
310
+ )}
311
+ <span
312
+ className={
313
+ styles['AutocompletePlace-itemText']
314
+ }
315
+ >
316
+ {place.text}
317
+ </span>
318
+ </div>
319
+ <div
320
+ className={
321
+ styles[
322
+ 'AutocompletePlace-itemSecondary'
323
+ ]
324
+ }
325
+ >
326
+ {place.context &&
327
+ place.context
328
+ .filter((ctx) => ctx.text)
329
+ .map((ctx, index) => (
330
+ <span key={ctx.id || index}>
331
+ {ctx.text}
332
+ </span>
333
+ ))
334
+ .reduce(
335
+ (prev, curr, i) => [
336
+ prev,
337
+ <span
338
+ key={`sep-${i}`}
339
+ className={
340
+ styles[
341
+ 'AutocompletePlace-itemSeparator'
342
+ ]
343
+ }
344
+ >
345
+ ,{' '}
346
+ </span>,
347
+ curr,
348
+ ],
349
+ []
350
+ )}
351
+ </div>
352
+ </li>
353
+ ))
354
+ ) : search.length > 1 ? (
355
+ <li
356
+ className={`${styles['AutocompletePlace-items']} ${styles['AutocompletePlace-noResults']}`}
357
+ >
358
+ No results found
359
+ </li>
360
+ ) : null}
199
361
  </ul>
200
362
  )}
201
363
  </div>
@@ -1033,8 +1033,35 @@ function Field({
1033
1033
  return (
1034
1034
  <div className={styles.formbreak}>
1035
1035
  <span>
1036
+ <ChevronRight
1037
+ strokeWidth={2}
1038
+ size={16}
1039
+ style={{
1040
+ display: 'inline-block',
1041
+ marginRight: '8px',
1042
+ verticalAlign: 'middle',
1043
+ position: 'relative',
1044
+ top: '-1px',
1045
+ color: 'var(--secondary-color, #fff)',
1046
+ opacity: 0.9,
1047
+ filter: 'drop-shadow(0 1px 1px rgba(0,0,0,0.1))',
1048
+ }}
1049
+ />
1036
1050
  {settings.label}{' '}
1037
- {settings.required === true ? '*' : ''}
1051
+ {settings.required === true ? (
1052
+ <span
1053
+ style={{
1054
+ color: 'var(--error-color, #ff6b6b)',
1055
+ textShadow: '0 1px 1px rgba(0,0,0,0.2)',
1056
+ marginLeft: '2px',
1057
+ fontWeight: 'bold',
1058
+ }}
1059
+ >
1060
+ *
1061
+ </span>
1062
+ ) : (
1063
+ ''
1064
+ )}
1038
1065
  </span>
1039
1066
  </div>
1040
1067
  );
@@ -459,16 +459,29 @@ function Form({
459
459
  ? [context[3]?.text, context[4]?.text]
460
460
  : [context[2]?.text, context[3]?.text];
461
461
 
462
+ // Determine the prefix based on the id parameter
463
+ let prefix = '';
464
+ if (id !== 'address1') {
465
+ // Extract prefix from id (e.g., 'delivery_address1' -> 'delivery_')
466
+ const match = id.match(/^(.+?)address1$/);
467
+ if (match && match[1]) {
468
+ prefix = match[1];
469
+ }
470
+ }
471
+
472
+ // Create the formDataUpdate object with the appropriate field names
462
473
  const formDataUpdate = {
463
- address1,
464
- address2,
465
- suburb,
466
- postcode,
467
- state,
468
- country,
474
+ [`${prefix}address1`]: address1,
475
+ [`${prefix}address2`]: address2,
476
+ [`${prefix}suburb`]: suburb,
477
+ [`${prefix}postcode`]: postcode,
478
+ [`${prefix}state`]: state,
479
+ [`${prefix}country`]: country,
469
480
  };
470
481
 
471
- if (formData.same_address === true || id !== 'address1') {
482
+ // If same_address is true and we're updating the primary address,
483
+ // also update the postal address fields
484
+ if (formData.same_address === true && id === 'address1') {
472
485
  Object.assign(formDataUpdate, {
473
486
  postal_address1: address1,
474
487
  postal_address2: address2,
@@ -0,0 +1,29 @@
1
+ import React from 'react';
2
+ import { ChevronRight } from 'akar-icons';
3
+ import styles from './styles/Field.module.scss';
4
+
5
+ /**
6
+ * SectionHeader component for form sections
7
+ * @param {Object} props - Component props
8
+ * @param {string} props.label - The section header text
9
+ * @param {React.ReactNode} [props.icon] - Optional custom icon (defaults to ChevronRight)
10
+ * @returns {React.ReactElement} The section header component
11
+ */
12
+ const SectionHeader = ({ label, icon }) => {
13
+ return (
14
+ <div className={styles.sectionHeader}>
15
+ {icon || (
16
+ <ChevronRight
17
+ strokeWidth={2}
18
+ size={20}
19
+ style={{
20
+ filter: 'drop-shadow(0 1px 1px rgba(0,0,0,0.2))',
21
+ }}
22
+ />
23
+ )}
24
+ <span>{label}</span>
25
+ </div>
26
+ );
27
+ };
28
+
29
+ export default SectionHeader;
@@ -0,0 +1,71 @@
1
+ # Form Styling Guide
2
+
3
+ This guide explains how to use the improved styling for form sections and line breaks.
4
+
5
+ ## Section Headers
6
+
7
+ The `SectionHeader` component provides a visually appealing header for form sections. It uses the project's color scheme with fallbacks to ensure good contrast.
8
+
9
+ ### Usage
10
+
11
+ ```jsx
12
+ import SectionHeader from '../SectionHeader';
13
+
14
+ // In your form component
15
+ <SectionHeader label="Site Details" />
16
+ ```
17
+
18
+ ### Props
19
+
20
+ - `label` (string, required): The text to display in the header
21
+ - `icon` (React element, optional): Custom icon to replace the default ChevronRight icon
22
+
23
+ ## Line Breaks
24
+
25
+ The `line-break` field type has been enhanced to provide better visual separation between form sections.
26
+
27
+ ### Usage
28
+
29
+ ```jsx
30
+ <Field
31
+ settings={{
32
+ id: 'delivery_address_section',
33
+ type: 'line-break',
34
+ label: 'Delivery Address #1',
35
+ size: 'full'
36
+ }}
37
+ inputValue=""
38
+ inputClass={inputClass}
39
+ />
40
+ ```
41
+
42
+ ## Color Scheme Compatibility
43
+
44
+ Both components are designed to work with the project's color scheme variables:
45
+
46
+ - `--primary-color`: Used for background
47
+ - `--secondary-color`: Used for text and accents
48
+ - `--error-color`: Used for required field indicators (*)
49
+
50
+ Fallback colors are provided to ensure good contrast even if the project variables don't provide enough contrast.
51
+
52
+ ## Example
53
+
54
+ See `FormWithSectionHeaders.jsx` for a complete example of how to implement a form with the new styling.
55
+
56
+ ## Tips for Best Results
57
+
58
+ 1. Use `SectionHeader` for main form sections (e.g., "Site Details", "Contact Details")
59
+ 2. Use `line-break` for subsections within a main section
60
+ 3. Ensure your project's color scheme provides good contrast between primary and secondary colors
61
+ 4. If you experience contrast issues, you can override the styles locally:
62
+
63
+ ```jsx
64
+ <SectionHeader
65
+ label="Custom Section"
66
+ style={{
67
+ background: '#3a86ff',
68
+ color: '#ffffff'
69
+ }}
70
+ />
71
+ ```