@visns-studio/visns-components 6.24.3 → 6.25.0
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 +4 -2
- package/src/components/Autocomplete.jsx +189 -119
- package/src/components/DataGrid.jsx +472 -31
- package/src/components/Navigation.jsx +475 -51
- package/src/components/auth/ClientAuthFrame.jsx +5 -0
- package/src/components/auth/ClientAuthScreen.jsx +29 -0
- package/src/components/columns/ColumnRenderers.jsx +3 -46
- package/src/components/columns/StackedRow.jsx +186 -0
- package/src/components/controls/DataGridSearch.jsx +110 -2
- package/src/components/controls/DataGridSortSheet.jsx +155 -0
- package/src/components/generic/GenericAuth.jsx +50 -18
- package/src/components/generic/GenericDashboard.jsx +20 -1
- package/src/components/generic/GenericDetail.jsx +446 -259
- package/src/components/mapboxSearchBox.js +640 -0
- package/src/components/navBadges.js +63 -1
- package/src/components/navDrawer.js +147 -0
- package/src/components/sms/SmsThreadPanel.jsx +34 -6
- package/src/components/sms/smsHelpers.js +15 -0
- package/src/components/styles/ClientAuth.module.scss +39 -0
- package/src/components/styles/DataGrid.module.scss +158 -5
- package/src/components/styles/Field.module.scss +52 -1
- package/src/components/styles/Form.module.scss +82 -0
- package/src/components/styles/GenericClientPortal.module.scss +72 -20
- package/src/components/styles/GenericDashboard.module.scss +50 -0
- package/src/components/styles/GenericDetail.module.scss +63 -1
- package/src/components/styles/GenericDynamic.module.scss +23 -0
- package/src/components/styles/GenericFormBuilder.module.scss +11 -0
- package/src/components/styles/GenericIndex.module.scss +6 -1
- package/src/components/styles/Navigation.module.scss +460 -7
- package/src/components/styles/Sms.module.scss +92 -0
- package/src/components/styles/StackedRow.module.scss +182 -0
- package/src/components/styles/TicketConversation.module.scss +76 -0
- package/src/components/styles/Vault.module.scss +192 -0
- package/src/components/styles/density.css +10 -0
- package/src/components/styles/global-datagrid.css +220 -0
- package/src/components/styles/global.css +20 -0
- package/src/components/tickets/TicketConversation.jsx +13 -8
- package/src/components/utils/ConfirmDialog.js +22 -3
- package/src/components/utils/cardLayout.js +666 -0
- package/src/components/utils/contactChannels.js +130 -0
- package/src/components/utils/editPlacement.js +95 -0
- package/src/components/utils/useDensity.js +303 -7
- package/src/index.js +32 -0
package/package.json
CHANGED
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
|
94
94
|
},
|
|
95
95
|
"name": "@visns-studio/visns-components",
|
|
96
|
-
"version": "6.
|
|
96
|
+
"version": "6.25.0",
|
|
97
97
|
"description": "Various packages to assist in the development of our Custom Applications.",
|
|
98
98
|
"main": "src/index.js",
|
|
99
99
|
"files": [
|
|
@@ -104,7 +104,9 @@
|
|
|
104
104
|
"build": "vite build",
|
|
105
105
|
"test": "node --test tests/*.test.mjs",
|
|
106
106
|
"dev:vault": "vite --config dev/vite.config.js",
|
|
107
|
-
"dev:sms": "FIXTURE=sms vite --config dev/vite.config.js"
|
|
107
|
+
"dev:sms": "FIXTURE=sms vite --config dev/vite.config.js",
|
|
108
|
+
"dev:grid": "FIXTURE=grid vite --config dev/vite.config.js",
|
|
109
|
+
"check:grid": "node dev/checkGridFixture.mjs"
|
|
108
110
|
},
|
|
109
111
|
"repository": {
|
|
110
112
|
"type": "git",
|
|
@@ -3,6 +3,17 @@ import AwesomeDebouncePromise from 'awesome-debounce-promise';
|
|
|
3
3
|
import { ToggleLeft, ToggleRight, Search } from 'lucide-react';
|
|
4
4
|
import styles from './styles/Autocomplete.module.scss';
|
|
5
5
|
import fetchUtil from '../utils/fetchUtil';
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_TYPES,
|
|
8
|
+
adaptSuggestions,
|
|
9
|
+
adaptToLegacyFeature,
|
|
10
|
+
buildRetrieveUrl,
|
|
11
|
+
buildSuggestUrl,
|
|
12
|
+
createSessionTokenManager,
|
|
13
|
+
featureFromRetrieve,
|
|
14
|
+
isTypesParamRejection,
|
|
15
|
+
warnOnce,
|
|
16
|
+
} from './mapboxSearchBox';
|
|
6
17
|
|
|
7
18
|
// Create the debounced function outside the component to prevent recreation
|
|
8
19
|
const createDebouncedSearch = (fn) =>
|
|
@@ -31,66 +42,87 @@ const VisnsAutocomplete = memo((props) => {
|
|
|
31
42
|
const inputRef = useRef(null);
|
|
32
43
|
const resultsRef = useRef(null);
|
|
33
44
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
value
|
|
44
|
-
)}.json?types=address,poi&access_token=${api}`;
|
|
45
|
-
|
|
46
|
-
if (country) {
|
|
47
|
-
url += `&country=${country}`;
|
|
48
|
-
}
|
|
45
|
+
/*
|
|
46
|
+
* The Search Box session, held for the life of the mounted field.
|
|
47
|
+
*
|
|
48
|
+
* One token covers every keystroke of a search and the retrieve that ends
|
|
49
|
+
* it — that pairing is what Mapbox bills as a single session. See
|
|
50
|
+
* mapboxSearchBox.js; the manager is a ref because a token that changed
|
|
51
|
+
* with the render cycle would be a token per keystroke.
|
|
52
|
+
*/
|
|
53
|
+
const sessionRef = useRef(null);
|
|
49
54
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
55
|
+
if (!sessionRef.current) {
|
|
56
|
+
sessionRef.current = createSessionTokenManager();
|
|
57
|
+
}
|
|
54
58
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
+
/*
|
|
60
|
+
* The `types` filter, held so a refusal is only paid for once.
|
|
61
|
+
*
|
|
62
|
+
* `types=address,poi` is what the documentation says and what live calls
|
|
63
|
+
* accept. If a future Mapbox disagrees, the first refusal drops the filter
|
|
64
|
+
* for the rest of the page rather than erroring on every keystroke.
|
|
65
|
+
*/
|
|
66
|
+
const typesRef = useRef(DEFAULT_TYPES);
|
|
59
67
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
68
|
+
/*
|
|
69
|
+
* Read the two biasing values off the prop as primitives.
|
|
70
|
+
*
|
|
71
|
+
* `mapbox` is usually rebuilt by the parent on every render, so depending
|
|
72
|
+
* on the object identity made `performSearch` a new function each time —
|
|
73
|
+
* and the "autocomplete switched back on" effect below keys on exactly
|
|
74
|
+
* that, so it would re-fire on every render of the parent form.
|
|
75
|
+
*/
|
|
76
|
+
const proximity = mapbox?.proximity;
|
|
77
|
+
const bbox = mapbox?.bbox;
|
|
64
78
|
|
|
65
79
|
const performSearch = useCallback(
|
|
66
80
|
(id, value) => {
|
|
67
|
-
if (id <= 0 || !value) {
|
|
81
|
+
if (id <= 0 || !value || !api) {
|
|
68
82
|
setResults([]);
|
|
69
83
|
setIsLoading(false);
|
|
70
84
|
return;
|
|
71
85
|
}
|
|
72
86
|
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
} catch (e) {
|
|
86
|
-
console.error('Error parsing response data:', e);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
87
|
+
const request = (types) => {
|
|
88
|
+
const url = buildSuggestUrl({
|
|
89
|
+
query: value,
|
|
90
|
+
accessToken: api,
|
|
91
|
+
// `current()` — NOT `consume()`. Every keystroke of this
|
|
92
|
+
// search rides the same token; only the retrieve spends it.
|
|
93
|
+
sessionToken: sessionRef.current.current(),
|
|
94
|
+
country,
|
|
95
|
+
proximity,
|
|
96
|
+
bbox,
|
|
97
|
+
types,
|
|
98
|
+
});
|
|
89
99
|
|
|
90
|
-
|
|
100
|
+
if (!url) {
|
|
101
|
+
setIsLoading(false);
|
|
102
|
+
return Promise.resolve();
|
|
103
|
+
}
|
|
91
104
|
|
|
92
|
-
|
|
105
|
+
return fetchUtil.get(url).then((response) => {
|
|
106
|
+
setResults(adaptSuggestions(response.data));
|
|
93
107
|
setIsLoading(false);
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
request(typesRef.current)
|
|
112
|
+
.catch((error) => {
|
|
113
|
+
if (typesRef.current && isTypesParamRejection(error)) {
|
|
114
|
+
warnOnce(
|
|
115
|
+
'searchbox-types',
|
|
116
|
+
'Mapbox refused the Search Box `types` filter; retrying without it.',
|
|
117
|
+
error?.response?.data
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
typesRef.current = '';
|
|
121
|
+
|
|
122
|
+
return request('');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
throw error;
|
|
94
126
|
})
|
|
95
127
|
.catch((error) => {
|
|
96
128
|
console.error('Error fetching address suggestions:', error);
|
|
@@ -98,7 +130,7 @@ const VisnsAutocomplete = memo((props) => {
|
|
|
98
130
|
setIsLoading(false);
|
|
99
131
|
});
|
|
100
132
|
},
|
|
101
|
-
[
|
|
133
|
+
[api, bbox, country, proximity]
|
|
102
134
|
);
|
|
103
135
|
|
|
104
136
|
// Create the debounced search function using our memoized performSearch
|
|
@@ -126,36 +158,15 @@ const VisnsAutocomplete = memo((props) => {
|
|
|
126
158
|
[field, overwrite, performSearchDebounced, setFormData]
|
|
127
159
|
);
|
|
128
160
|
|
|
129
|
-
|
|
161
|
+
/**
|
|
162
|
+
* Hand a finished pick — already adapted to the legacy v5 feature shape —
|
|
163
|
+
* to whoever owns this field's value.
|
|
164
|
+
*/
|
|
165
|
+
const applyFeature = useCallback(
|
|
130
166
|
(place) => {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
// If place_name is not available, construct from components
|
|
135
|
-
const fallbackValue = (() => {
|
|
136
|
-
const address = place.address || '';
|
|
137
|
-
const text = place.text || '';
|
|
138
|
-
const context = place.context
|
|
139
|
-
? place.context
|
|
140
|
-
.filter((ctx) => ctx.text)
|
|
141
|
-
.map((ctx) => ctx.text)
|
|
142
|
-
.join(', ')
|
|
143
|
-
: '';
|
|
144
|
-
|
|
145
|
-
if (address && text) {
|
|
146
|
-
return context
|
|
147
|
-
? `${address} ${text}, ${context}`
|
|
148
|
-
: `${address} ${text}`;
|
|
149
|
-
} else if (text) {
|
|
150
|
-
return context ? `${text}, ${context}` : text;
|
|
151
|
-
}
|
|
152
|
-
return '';
|
|
153
|
-
})();
|
|
154
|
-
|
|
155
|
-
const finalValue = displayValue || fallbackValue;
|
|
156
|
-
|
|
157
|
-
setResults([]);
|
|
158
|
-
setIsFocused(false);
|
|
167
|
+
if (!place) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
159
170
|
|
|
160
171
|
/*
|
|
161
172
|
* A form that handles the pick owns this field's value.
|
|
@@ -179,10 +190,14 @@ const VisnsAutocomplete = memo((props) => {
|
|
|
179
190
|
return;
|
|
180
191
|
}
|
|
181
192
|
|
|
193
|
+
// Standalone use (no form to parse the place): the field keeps
|
|
194
|
+
// the full formatted address, which is the only useful value. The
|
|
195
|
+
// adapter always builds a `place_name`, from the record's own
|
|
196
|
+
// `full_address` or from its name and locality.
|
|
197
|
+
const finalValue = place.place_name || '';
|
|
198
|
+
|
|
182
199
|
setSearch(finalValue);
|
|
183
200
|
|
|
184
|
-
// Standalone use (no form to parse the place): the field keeps
|
|
185
|
-
// the full formatted address, which is the only useful value.
|
|
186
201
|
setFormData((prevState) => ({
|
|
187
202
|
...prevState,
|
|
188
203
|
[field]: finalValue,
|
|
@@ -191,10 +206,89 @@ const VisnsAutocomplete = memo((props) => {
|
|
|
191
206
|
[field, onSelect, setFormData]
|
|
192
207
|
);
|
|
193
208
|
|
|
209
|
+
/*
|
|
210
|
+
* A click on a row is the second half of the Search Box exchange: the
|
|
211
|
+
* suggestion carries a `mapbox_id` and nothing else useful, so the full
|
|
212
|
+
* record — coordinates, context, the whole formatted address — is fetched
|
|
213
|
+
* by `retrieve` on the same session token, then adapted back to the v5
|
|
214
|
+
* shape the forms read.
|
|
215
|
+
*
|
|
216
|
+
* IT FAILS SOFT, ALWAYS. A retrieve that 500s, times out or comes back
|
|
217
|
+
* empty must not cost the user their click and must never throw into the
|
|
218
|
+
* form: a suggestion already carries the name, the street line and the
|
|
219
|
+
* same context object, so it adapts into the same feature minus the
|
|
220
|
+
* coordinates. That is worth far more than an empty address field, and
|
|
221
|
+
* nothing in this library reads the coordinates.
|
|
222
|
+
*/
|
|
223
|
+
const handleItemClicked = useCallback(
|
|
224
|
+
(row) => {
|
|
225
|
+
setResults([]);
|
|
226
|
+
setIsFocused(false);
|
|
227
|
+
|
|
228
|
+
const fromSuggestion = () =>
|
|
229
|
+
adaptToLegacyFeature(row?.suggestion || row);
|
|
230
|
+
|
|
231
|
+
if (!row?.mapbox_id || !api) {
|
|
232
|
+
// Shouldn't happen — suggest returns an id on every row.
|
|
233
|
+
warnOnce(
|
|
234
|
+
'searchbox-no-id',
|
|
235
|
+
'Mapbox suggestion arrived without a mapbox_id; filling from the suggestion alone.',
|
|
236
|
+
row
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
sessionRef.current.reset();
|
|
240
|
+
applyFeature(fromSuggestion());
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const url = buildRetrieveUrl({
|
|
245
|
+
mapboxId: row.mapbox_id,
|
|
246
|
+
accessToken: api,
|
|
247
|
+
// Spends the token: this pick closes the session, and the next
|
|
248
|
+
// thing typed opens a new one.
|
|
249
|
+
sessionToken: sessionRef.current.consume(),
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
fetchUtil
|
|
253
|
+
.get(url)
|
|
254
|
+
.then((response) => {
|
|
255
|
+
const place = featureFromRetrieve(response.data);
|
|
256
|
+
|
|
257
|
+
if (!place) {
|
|
258
|
+
warnOnce(
|
|
259
|
+
'searchbox-retrieve-empty',
|
|
260
|
+
'Mapbox retrieve returned no feature; filling from the suggestion instead.',
|
|
261
|
+
response?.data
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
applyFeature(fromSuggestion());
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
applyFeature(place);
|
|
269
|
+
})
|
|
270
|
+
.catch((error) => {
|
|
271
|
+
warnOnce(
|
|
272
|
+
'searchbox-retrieve-failed',
|
|
273
|
+
'Mapbox retrieve failed; filling from the suggestion instead.',
|
|
274
|
+
error
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
applyFeature(fromSuggestion());
|
|
278
|
+
});
|
|
279
|
+
},
|
|
280
|
+
[api, applyFeature]
|
|
281
|
+
);
|
|
282
|
+
|
|
194
283
|
const handleOverwrite = useCallback(() => {
|
|
195
284
|
setOverwrite((prevState) => {
|
|
196
285
|
if (!prevState) {
|
|
197
286
|
setResults([]);
|
|
287
|
+
|
|
288
|
+
// Switching autocomplete off abandons the session rather than
|
|
289
|
+
// ending it. Keeping the token would bill whatever is typed
|
|
290
|
+
// after it is switched back on against this search.
|
|
291
|
+
sessionRef.current.reset();
|
|
198
292
|
}
|
|
199
293
|
return !prevState;
|
|
200
294
|
});
|
|
@@ -331,34 +425,34 @@ const VisnsAutocomplete = memo((props) => {
|
|
|
331
425
|
Loading...
|
|
332
426
|
</li>
|
|
333
427
|
) : Array.isArray(results) && results.length > 0 ? (
|
|
334
|
-
|
|
428
|
+
/*
|
|
429
|
+
* Suggest hands back the row already split in two:
|
|
430
|
+
* `name` — the business or the street line — over
|
|
431
|
+
* `place_formatted`, the locality it sits in. So
|
|
432
|
+
* "Epiq" reads above "Perth 6000, Australia" rather
|
|
433
|
+
* than the v5 rows' number/street pair stitched back
|
|
434
|
+
* together from a context array. Same two elements,
|
|
435
|
+
* same classes.
|
|
436
|
+
*/
|
|
437
|
+
results.map((row) => (
|
|
335
438
|
<li
|
|
336
|
-
key={
|
|
439
|
+
key={row.id}
|
|
337
440
|
className={styles['AutocompletePlace-items']}
|
|
338
|
-
onClick={() => handleItemClicked(
|
|
441
|
+
onClick={() => handleItemClicked(row)}
|
|
339
442
|
>
|
|
340
443
|
<div
|
|
341
444
|
className={
|
|
342
445
|
styles['AutocompletePlace-itemMain']
|
|
343
446
|
}
|
|
344
447
|
>
|
|
345
|
-
{place.address && (
|
|
346
|
-
<span
|
|
347
|
-
className={
|
|
348
|
-
styles[
|
|
349
|
-
'AutocompletePlace-itemAddress'
|
|
350
|
-
]
|
|
351
|
-
}
|
|
352
|
-
>
|
|
353
|
-
{place.address}
|
|
354
|
-
</span>
|
|
355
|
-
)}
|
|
356
448
|
<span
|
|
357
449
|
className={
|
|
358
|
-
styles[
|
|
450
|
+
styles[
|
|
451
|
+
'AutocompletePlace-itemAddress'
|
|
452
|
+
]
|
|
359
453
|
}
|
|
360
454
|
>
|
|
361
|
-
{
|
|
455
|
+
{row.name}
|
|
362
456
|
</span>
|
|
363
457
|
</div>
|
|
364
458
|
<div
|
|
@@ -368,31 +462,7 @@ const VisnsAutocomplete = memo((props) => {
|
|
|
368
462
|
]
|
|
369
463
|
}
|
|
370
464
|
>
|
|
371
|
-
{
|
|
372
|
-
place.context
|
|
373
|
-
.filter((ctx) => ctx.text)
|
|
374
|
-
.map((ctx, index) => (
|
|
375
|
-
<span key={ctx.id || index}>
|
|
376
|
-
{ctx.text}
|
|
377
|
-
</span>
|
|
378
|
-
))
|
|
379
|
-
.reduce(
|
|
380
|
-
(prev, curr, i) => [
|
|
381
|
-
prev,
|
|
382
|
-
<span
|
|
383
|
-
key={`sep-${i}`}
|
|
384
|
-
className={
|
|
385
|
-
styles[
|
|
386
|
-
'AutocompletePlace-itemSeparator'
|
|
387
|
-
]
|
|
388
|
-
}
|
|
389
|
-
>
|
|
390
|
-
,{' '}
|
|
391
|
-
</span>,
|
|
392
|
-
curr,
|
|
393
|
-
],
|
|
394
|
-
[]
|
|
395
|
-
)}
|
|
465
|
+
{row.place_formatted}
|
|
396
466
|
</div>
|
|
397
467
|
</li>
|
|
398
468
|
))
|