@visns-studio/visns-components 5.8.12 → 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 +6 -6
- package/src/components/crm/Autocomplete.jsx +211 -49
- package/src/components/crm/Field.jsx +28 -1
- package/src/components/crm/Form.jsx +20 -7
- package/src/components/crm/SectionHeader.jsx +29 -0
- package/src/components/crm/examples/FormStylingGuide.md +71 -0
- package/src/components/crm/examples/FormWithSectionHeaders.jsx +266 -0
- package/src/components/crm/generic/GenericIndex.jsx +2 -3
- package/src/components/crm/styles/Autocomplete.module.scss +97 -13
- package/src/components/crm/styles/Field.module.scss +88 -5
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.
|
|
8
|
-
"@nivo/core": "^0.
|
|
9
|
-
"@nivo/line": "^0.
|
|
10
|
-
"@nivo/pie": "^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.
|
|
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",
|
|
@@ -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.
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
109
|
-
const
|
|
110
|
-
|
|
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(
|
|
152
|
+
setSearch(finalValue);
|
|
113
153
|
setResults([]);
|
|
114
|
-
|
|
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
|
-
<
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
```
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import SectionHeader from '../SectionHeader';
|
|
3
|
+
import Field from '../Field';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Example form component that demonstrates how to use the SectionHeader component
|
|
7
|
+
* This shows how to implement the form seen in the screenshot with improved styling
|
|
8
|
+
*/
|
|
9
|
+
const FormWithSectionHeaders = ({ formData, setFormData, onChange }) => {
|
|
10
|
+
// Sample form settings for demonstration
|
|
11
|
+
const formSettings = {
|
|
12
|
+
// Form settings would go here
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// Sample input classes
|
|
16
|
+
const inputClass = {
|
|
17
|
+
// Input classes would go here
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<form>
|
|
22
|
+
{/* Site Details Section */}
|
|
23
|
+
<SectionHeader label="Site Details" />
|
|
24
|
+
|
|
25
|
+
{/* Site Name Field */}
|
|
26
|
+
<Field
|
|
27
|
+
settings={{
|
|
28
|
+
id: 'site_name',
|
|
29
|
+
type: 'text',
|
|
30
|
+
label: 'Site Name',
|
|
31
|
+
required: true,
|
|
32
|
+
size: 'half'
|
|
33
|
+
}}
|
|
34
|
+
inputValue={formData.site_name}
|
|
35
|
+
inputClass={inputClass}
|
|
36
|
+
onChange={onChange}
|
|
37
|
+
/>
|
|
38
|
+
|
|
39
|
+
{/* Address Fields */}
|
|
40
|
+
<Field
|
|
41
|
+
settings={{
|
|
42
|
+
id: 'address_1',
|
|
43
|
+
type: 'text',
|
|
44
|
+
label: 'Address #1',
|
|
45
|
+
required: true,
|
|
46
|
+
size: 'half'
|
|
47
|
+
}}
|
|
48
|
+
inputValue={formData.address_1}
|
|
49
|
+
inputClass={inputClass}
|
|
50
|
+
onChange={onChange}
|
|
51
|
+
/>
|
|
52
|
+
|
|
53
|
+
<Field
|
|
54
|
+
settings={{
|
|
55
|
+
id: 'address_2',
|
|
56
|
+
type: 'text',
|
|
57
|
+
label: 'Address #2',
|
|
58
|
+
size: 'half'
|
|
59
|
+
}}
|
|
60
|
+
inputValue={formData.address_2}
|
|
61
|
+
inputClass={inputClass}
|
|
62
|
+
onChange={onChange}
|
|
63
|
+
/>
|
|
64
|
+
|
|
65
|
+
{/* Suburb Field */}
|
|
66
|
+
<Field
|
|
67
|
+
settings={{
|
|
68
|
+
id: 'suburb',
|
|
69
|
+
type: 'text',
|
|
70
|
+
label: 'Suburb',
|
|
71
|
+
required: true,
|
|
72
|
+
size: 'half'
|
|
73
|
+
}}
|
|
74
|
+
inputValue={formData.suburb}
|
|
75
|
+
inputClass={inputClass}
|
|
76
|
+
onChange={onChange}
|
|
77
|
+
/>
|
|
78
|
+
|
|
79
|
+
{/* Postcode Field */}
|
|
80
|
+
<Field
|
|
81
|
+
settings={{
|
|
82
|
+
id: 'postcode',
|
|
83
|
+
type: 'text',
|
|
84
|
+
label: 'Postcode',
|
|
85
|
+
required: true,
|
|
86
|
+
size: 'half'
|
|
87
|
+
}}
|
|
88
|
+
inputValue={formData.postcode}
|
|
89
|
+
inputClass={inputClass}
|
|
90
|
+
onChange={onChange}
|
|
91
|
+
/>
|
|
92
|
+
|
|
93
|
+
{/* State Field */}
|
|
94
|
+
<Field
|
|
95
|
+
settings={{
|
|
96
|
+
id: 'state',
|
|
97
|
+
type: 'text',
|
|
98
|
+
label: 'State',
|
|
99
|
+
required: true,
|
|
100
|
+
size: 'half'
|
|
101
|
+
}}
|
|
102
|
+
inputValue={formData.state}
|
|
103
|
+
inputClass={inputClass}
|
|
104
|
+
onChange={onChange}
|
|
105
|
+
/>
|
|
106
|
+
|
|
107
|
+
{/* Country Field */}
|
|
108
|
+
<Field
|
|
109
|
+
settings={{
|
|
110
|
+
id: 'country',
|
|
111
|
+
type: 'text',
|
|
112
|
+
label: 'Country',
|
|
113
|
+
required: true,
|
|
114
|
+
size: 'half'
|
|
115
|
+
}}
|
|
116
|
+
inputValue={formData.country}
|
|
117
|
+
inputClass={inputClass}
|
|
118
|
+
onChange={onChange}
|
|
119
|
+
/>
|
|
120
|
+
|
|
121
|
+
{/* Delivery Address Section */}
|
|
122
|
+
<Field
|
|
123
|
+
settings={{
|
|
124
|
+
id: 'delivery_address_section',
|
|
125
|
+
type: 'line-break',
|
|
126
|
+
label: 'Delivery Address #1',
|
|
127
|
+
size: 'full'
|
|
128
|
+
}}
|
|
129
|
+
inputValue=""
|
|
130
|
+
inputClass={inputClass}
|
|
131
|
+
/>
|
|
132
|
+
|
|
133
|
+
{/* Delivery Address Fields */}
|
|
134
|
+
<Field
|
|
135
|
+
settings={{
|
|
136
|
+
id: 'delivery_address_1',
|
|
137
|
+
type: 'text',
|
|
138
|
+
label: 'Delivery Address #1',
|
|
139
|
+
required: true,
|
|
140
|
+
size: 'half'
|
|
141
|
+
}}
|
|
142
|
+
inputValue={formData.delivery_address_1}
|
|
143
|
+
inputClass={inputClass}
|
|
144
|
+
onChange={onChange}
|
|
145
|
+
/>
|
|
146
|
+
|
|
147
|
+
<Field
|
|
148
|
+
settings={{
|
|
149
|
+
id: 'delivery_address_2',
|
|
150
|
+
type: 'text',
|
|
151
|
+
label: 'Delivery Address #2',
|
|
152
|
+
size: 'half'
|
|
153
|
+
}}
|
|
154
|
+
inputValue={formData.delivery_address_2}
|
|
155
|
+
inputClass={inputClass}
|
|
156
|
+
onChange={onChange}
|
|
157
|
+
/>
|
|
158
|
+
|
|
159
|
+
{/* Delivery Suburb Field */}
|
|
160
|
+
<Field
|
|
161
|
+
settings={{
|
|
162
|
+
id: 'delivery_suburb',
|
|
163
|
+
type: 'text',
|
|
164
|
+
label: 'Delivery Suburb',
|
|
165
|
+
required: true,
|
|
166
|
+
size: 'half'
|
|
167
|
+
}}
|
|
168
|
+
inputValue={formData.delivery_suburb}
|
|
169
|
+
inputClass={inputClass}
|
|
170
|
+
onChange={onChange}
|
|
171
|
+
/>
|
|
172
|
+
|
|
173
|
+
{/* Delivery Postcode Field */}
|
|
174
|
+
<Field
|
|
175
|
+
settings={{
|
|
176
|
+
id: 'delivery_postcode',
|
|
177
|
+
type: 'text',
|
|
178
|
+
label: 'Delivery Postcode',
|
|
179
|
+
required: true,
|
|
180
|
+
size: 'half'
|
|
181
|
+
}}
|
|
182
|
+
inputValue={formData.delivery_postcode}
|
|
183
|
+
inputClass={inputClass}
|
|
184
|
+
onChange={onChange}
|
|
185
|
+
/>
|
|
186
|
+
|
|
187
|
+
{/* Delivery State Field */}
|
|
188
|
+
<Field
|
|
189
|
+
settings={{
|
|
190
|
+
id: 'delivery_state',
|
|
191
|
+
type: 'text',
|
|
192
|
+
label: 'Delivery State',
|
|
193
|
+
required: true,
|
|
194
|
+
size: 'half'
|
|
195
|
+
}}
|
|
196
|
+
inputValue={formData.delivery_state}
|
|
197
|
+
inputClass={inputClass}
|
|
198
|
+
onChange={onChange}
|
|
199
|
+
/>
|
|
200
|
+
|
|
201
|
+
{/* Delivery Country Field */}
|
|
202
|
+
<Field
|
|
203
|
+
settings={{
|
|
204
|
+
id: 'delivery_country',
|
|
205
|
+
type: 'text',
|
|
206
|
+
label: 'Delivery Country',
|
|
207
|
+
required: true,
|
|
208
|
+
size: 'half'
|
|
209
|
+
}}
|
|
210
|
+
inputValue={formData.delivery_country}
|
|
211
|
+
inputClass={inputClass}
|
|
212
|
+
onChange={onChange}
|
|
213
|
+
/>
|
|
214
|
+
|
|
215
|
+
{/* Contact Details Section */}
|
|
216
|
+
<SectionHeader label="Contact Details" />
|
|
217
|
+
|
|
218
|
+
{/* Firstname Field */}
|
|
219
|
+
<Field
|
|
220
|
+
settings={{
|
|
221
|
+
id: 'firstname',
|
|
222
|
+
type: 'text',
|
|
223
|
+
label: 'Firstname',
|
|
224
|
+
required: true,
|
|
225
|
+
size: 'half'
|
|
226
|
+
}}
|
|
227
|
+
inputValue={formData.firstname}
|
|
228
|
+
inputClass={inputClass}
|
|
229
|
+
onChange={onChange}
|
|
230
|
+
/>
|
|
231
|
+
|
|
232
|
+
{/* Surname Field */}
|
|
233
|
+
<Field
|
|
234
|
+
settings={{
|
|
235
|
+
id: 'surname',
|
|
236
|
+
type: 'text',
|
|
237
|
+
label: 'Surname',
|
|
238
|
+
size: 'half'
|
|
239
|
+
}}
|
|
240
|
+
inputValue={formData.surname}
|
|
241
|
+
inputClass={inputClass}
|
|
242
|
+
onChange={onChange}
|
|
243
|
+
/>
|
|
244
|
+
|
|
245
|
+
{/* Save Button */}
|
|
246
|
+
<div style={{ width: '100%', textAlign: 'center', marginTop: '20px' }}>
|
|
247
|
+
<button
|
|
248
|
+
type="submit"
|
|
249
|
+
style={{
|
|
250
|
+
background: 'var(--primary-color, #2684FF)',
|
|
251
|
+
color: 'var(--secondary-color, #fff)',
|
|
252
|
+
padding: '10px 20px',
|
|
253
|
+
border: 'none',
|
|
254
|
+
borderRadius: 'var(--br, 3px)',
|
|
255
|
+
cursor: 'pointer',
|
|
256
|
+
fontWeight: 'bold'
|
|
257
|
+
}}
|
|
258
|
+
>
|
|
259
|
+
Save & Close
|
|
260
|
+
</button>
|
|
261
|
+
</div>
|
|
262
|
+
</form>
|
|
263
|
+
);
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
export default FormWithSectionHeaders;
|
|
@@ -1496,6 +1496,8 @@ function GenericIndex({
|
|
|
1496
1496
|
|
|
1497
1497
|
// Add a ref to store the latest rowsSelected value
|
|
1498
1498
|
const rowsSelectedRef = useRef({});
|
|
1499
|
+
// Create a ref to track if we've already processed active filter combinations
|
|
1500
|
+
const activeFilterRef = useRef(null);
|
|
1499
1501
|
|
|
1500
1502
|
// Update the ref whenever rowsSelected changes
|
|
1501
1503
|
useEffect(() => {
|
|
@@ -1847,9 +1849,6 @@ function GenericIndex({
|
|
|
1847
1849
|
return;
|
|
1848
1850
|
}
|
|
1849
1851
|
|
|
1850
|
-
// Create a ref to track if we've already processed this combination
|
|
1851
|
-
const activeFilterRef = useRef(null);
|
|
1852
|
-
|
|
1853
1852
|
// Find the active filter and its child
|
|
1854
1853
|
const activeFilter = subnav?.find((filter) => {
|
|
1855
1854
|
if (filter.isParent) {
|
|
@@ -1,36 +1,120 @@
|
|
|
1
|
-
.AutocompletePlace
|
|
2
|
-
|
|
1
|
+
.AutocompletePlace {
|
|
2
|
+
position: relative;
|
|
3
|
+
width: 100%;
|
|
4
|
+
|
|
5
|
+
.toggleActive {
|
|
6
|
+
color: var(--secondary-color);
|
|
7
|
+
}
|
|
3
8
|
}
|
|
4
9
|
|
|
5
|
-
.AutocompletePlace {
|
|
10
|
+
.AutocompletePlace-inputWrapper {
|
|
6
11
|
position: relative;
|
|
12
|
+
display: flex;
|
|
13
|
+
align-items: center;
|
|
7
14
|
width: 100%;
|
|
8
15
|
|
|
9
|
-
> svg {
|
|
16
|
+
> svg:not(.AutocompletePlace-searchIcon) {
|
|
10
17
|
position: absolute;
|
|
11
|
-
right: 0.
|
|
12
|
-
top:
|
|
13
|
-
|
|
18
|
+
right: 0.5rem;
|
|
19
|
+
top: 50%;
|
|
20
|
+
transform: translateY(-50%);
|
|
14
21
|
cursor: pointer;
|
|
15
22
|
}
|
|
23
|
+
}
|
|
16
24
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
25
|
+
.AutocompletePlace-searchIcon {
|
|
26
|
+
position: absolute;
|
|
27
|
+
left: 0.75rem;
|
|
28
|
+
color: var(--paragraph-color);
|
|
29
|
+
pointer-events: none;
|
|
20
30
|
}
|
|
21
31
|
|
|
22
32
|
.AutocompletePlace-input {
|
|
23
|
-
padding: 0.65rem 3rem 0.65rem
|
|
33
|
+
padding: 0.65rem 3rem 0.65rem 2.25rem !important;
|
|
24
34
|
height: 38px;
|
|
35
|
+
width: 100%;
|
|
36
|
+
border: 1px solid rgba(var(--primary-rgb), 0.2);
|
|
37
|
+
border-radius: 4px;
|
|
38
|
+
outline: none;
|
|
39
|
+
font-size: 0.9rem;
|
|
40
|
+
|
|
41
|
+
&:focus {
|
|
42
|
+
border-color: rgba(var(--primary-rgb), 0.5);
|
|
43
|
+
box-shadow: 0 0 0 2px rgba(var(--primary-rgb), 0.1);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.AutocompletePlace-results {
|
|
48
|
+
position: absolute;
|
|
49
|
+
top: 100%;
|
|
50
|
+
left: 0;
|
|
51
|
+
right: 0;
|
|
52
|
+
z-index: 999;
|
|
53
|
+
margin: 0;
|
|
54
|
+
padding: 0;
|
|
55
|
+
background-color: #fff;
|
|
56
|
+
border: 1px solid rgba(var(--primary-rgb), 0.2);
|
|
57
|
+
border-radius: 4px;
|
|
58
|
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
|
59
|
+
max-height: 250px;
|
|
60
|
+
overflow-y: auto;
|
|
61
|
+
margin-top: 4px;
|
|
62
|
+
list-style-type: none;
|
|
63
|
+
width: 100%;
|
|
25
64
|
}
|
|
26
65
|
|
|
27
66
|
.AutocompletePlace-items {
|
|
28
67
|
list-style: none;
|
|
29
68
|
border-bottom: 1px solid rgba(var(--primary-rgb), 0.1);
|
|
30
69
|
cursor: pointer;
|
|
31
|
-
padding:
|
|
70
|
+
padding: 8px 12px;
|
|
71
|
+
font-size: 0.9rem;
|
|
72
|
+
|
|
73
|
+
&:last-child {
|
|
74
|
+
border-bottom: none;
|
|
75
|
+
}
|
|
32
76
|
|
|
33
77
|
&:hover {
|
|
34
|
-
background-color: rgba(var(--
|
|
78
|
+
background-color: rgba(var(--primary-rgb), 0.05);
|
|
35
79
|
}
|
|
36
80
|
}
|
|
81
|
+
|
|
82
|
+
.AutocompletePlace-itemMain {
|
|
83
|
+
display: flex;
|
|
84
|
+
align-items: center;
|
|
85
|
+
gap: 4px;
|
|
86
|
+
margin-bottom: 2px;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.AutocompletePlace-itemAddress {
|
|
90
|
+
font-weight: 600;
|
|
91
|
+
color: var(--primary-color);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.AutocompletePlace-itemText {
|
|
95
|
+
color: var(--paragraph-color);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.AutocompletePlace-itemSecondary {
|
|
99
|
+
font-size: 0.8rem;
|
|
100
|
+
color: rgba(var(--paragraph-rgb), 0.7);
|
|
101
|
+
white-space: nowrap;
|
|
102
|
+
overflow: hidden;
|
|
103
|
+
text-overflow: ellipsis;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.AutocompletePlace-itemSeparator {
|
|
107
|
+
opacity: 0.6;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.AutocompletePlace-loading {
|
|
111
|
+
font-style: italic;
|
|
112
|
+
color: var(--paragraph-color);
|
|
113
|
+
text-align: center;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.AutocompletePlace-noResults {
|
|
117
|
+
font-style: italic;
|
|
118
|
+
color: var(--paragraph-color);
|
|
119
|
+
text-align: center;
|
|
120
|
+
}
|
|
@@ -494,18 +494,101 @@ input[type='file']:hover {
|
|
|
494
494
|
margin-right: 5px;
|
|
495
495
|
}
|
|
496
496
|
|
|
497
|
+
/* Section header styling for form sections */
|
|
498
|
+
.sectionHeader {
|
|
499
|
+
display: flex;
|
|
500
|
+
align-items: center;
|
|
501
|
+
width: 100%;
|
|
502
|
+
padding: 0.85em 1.2em;
|
|
503
|
+
background: var(--primary-color, #2684ff);
|
|
504
|
+
border-radius: var(--br, 3px);
|
|
505
|
+
box-sizing: border-box;
|
|
506
|
+
margin: 0.5em 0 1em 0;
|
|
507
|
+
position: relative;
|
|
508
|
+
overflow: hidden;
|
|
509
|
+
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
|
|
510
|
+
|
|
511
|
+
/* Add a subtle gradient overlay for depth */
|
|
512
|
+
&::before {
|
|
513
|
+
content: '';
|
|
514
|
+
position: absolute;
|
|
515
|
+
top: 0;
|
|
516
|
+
left: 0;
|
|
517
|
+
width: 100%;
|
|
518
|
+
height: 100%;
|
|
519
|
+
background: linear-gradient(
|
|
520
|
+
to right,
|
|
521
|
+
rgba(255, 255, 255, 0.15),
|
|
522
|
+
rgba(255, 255, 255, 0.05)
|
|
523
|
+
);
|
|
524
|
+
pointer-events: none;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
svg {
|
|
528
|
+
margin-right: 8px;
|
|
529
|
+
color: var(--secondary-color, #fff);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
span {
|
|
533
|
+
font-size: 1em;
|
|
534
|
+
color: var(--secondary-color, #fff);
|
|
535
|
+
font-weight: 600;
|
|
536
|
+
letter-spacing: 0.02em;
|
|
537
|
+
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/* Line break styling for form sections */
|
|
497
542
|
.formbreak {
|
|
498
543
|
display: block;
|
|
499
544
|
width: 100%;
|
|
500
|
-
padding:
|
|
501
|
-
background
|
|
502
|
-
|
|
545
|
+
padding: 0.85em 1.2em;
|
|
546
|
+
/* Use a more reliable background that works with any color scheme */
|
|
547
|
+
background: var(--primary-color, #2684ff);
|
|
548
|
+
border-radius: var(--br, 3px);
|
|
503
549
|
box-sizing: border-box;
|
|
504
|
-
border:
|
|
550
|
+
border-left: 4px solid var(--secondary-color, #fff);
|
|
551
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
552
|
+
margin: 0.5em 0;
|
|
553
|
+
position: relative;
|
|
554
|
+
overflow: hidden;
|
|
555
|
+
|
|
556
|
+
/* Add a subtle gradient overlay for depth */
|
|
557
|
+
&::before {
|
|
558
|
+
content: '';
|
|
559
|
+
position: absolute;
|
|
560
|
+
top: 0;
|
|
561
|
+
left: 0;
|
|
562
|
+
width: 100%;
|
|
563
|
+
height: 100%;
|
|
564
|
+
background: linear-gradient(
|
|
565
|
+
to right,
|
|
566
|
+
rgba(255, 255, 255, 0.1),
|
|
567
|
+
rgba(255, 255, 255, 0)
|
|
568
|
+
);
|
|
569
|
+
pointer-events: none;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
&::after {
|
|
573
|
+
content: '';
|
|
574
|
+
position: absolute;
|
|
575
|
+
bottom: 0;
|
|
576
|
+
left: 0;
|
|
577
|
+
width: 100%;
|
|
578
|
+
height: 2px;
|
|
579
|
+
background: var(--tertiary-color, #fff);
|
|
580
|
+
opacity: 0.3;
|
|
581
|
+
}
|
|
505
582
|
|
|
506
583
|
span {
|
|
507
584
|
font-size: 0.95em;
|
|
508
|
-
color: var(--
|
|
585
|
+
color: var(--tertiary-color, #fff);
|
|
586
|
+
font-weight: 500;
|
|
587
|
+
letter-spacing: 0.01em;
|
|
588
|
+
display: flex;
|
|
589
|
+
align-items: center;
|
|
590
|
+
position: relative;
|
|
591
|
+
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
|
|
509
592
|
}
|
|
510
593
|
}
|
|
511
594
|
|