@visns-studio/visns-components 5.19.0 → 5.21.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
CHANGED
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
"react-dom": "^17.0.0 || ^18.0.0"
|
|
95
95
|
},
|
|
96
96
|
"name": "@visns-studio/visns-components",
|
|
97
|
-
"version": "5.
|
|
97
|
+
"version": "5.21.0",
|
|
98
98
|
"description": "Various packages to assist in the development of our Custom Applications.",
|
|
99
99
|
"main": "src/index.js",
|
|
100
100
|
"files": [
|
package/src/components/Field.jsx
CHANGED
|
@@ -44,6 +44,37 @@ import DatePickerPortal from './utils/DatePickerPortal';
|
|
|
44
44
|
|
|
45
45
|
import 'react-toggle/style.css';
|
|
46
46
|
import 'react-datepicker/dist/react-datepicker.css';
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Insert `token` at the textarea's caret (or replacing its selection) and
|
|
50
|
+
* fire a native input event so the controlled React value updates. Restores
|
|
51
|
+
* the caret just after the inserted token. Used by textarea fields that
|
|
52
|
+
* declare a `variables` list (click-to-insert).
|
|
53
|
+
*/
|
|
54
|
+
function insertVariableAtCursor(el, token) {
|
|
55
|
+
if (!el) return;
|
|
56
|
+
const start = el.selectionStart ?? el.value.length;
|
|
57
|
+
const end = el.selectionEnd ?? el.value.length;
|
|
58
|
+
const next = el.value.slice(0, start) + token + el.value.slice(end);
|
|
59
|
+
const setter = Object.getOwnPropertyDescriptor(
|
|
60
|
+
window.HTMLTextAreaElement.prototype,
|
|
61
|
+
'value'
|
|
62
|
+
)?.set;
|
|
63
|
+
if (setter) {
|
|
64
|
+
setter.call(el, next);
|
|
65
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
66
|
+
}
|
|
67
|
+
const caret = start + token.length;
|
|
68
|
+
requestAnimationFrame(() => {
|
|
69
|
+
el.focus();
|
|
70
|
+
try {
|
|
71
|
+
el.setSelectionRange(caret, caret);
|
|
72
|
+
} catch (e) {
|
|
73
|
+
/* noop */
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
47
78
|
function Field({
|
|
48
79
|
api,
|
|
49
80
|
autocompleteCallback,
|
|
@@ -99,6 +130,7 @@ function Field({
|
|
|
99
130
|
const [lineWidth, setLineWidth] = useState(2);
|
|
100
131
|
const [lineColor, setLineColor] = useState('rgba(0, 0, 0, 1)');
|
|
101
132
|
const sketchRef = useRef(null);
|
|
133
|
+
const variableTextareaRef = useRef(null);
|
|
102
134
|
|
|
103
135
|
/** Email Functions */
|
|
104
136
|
const validateEmail = (email) => {
|
|
@@ -2566,17 +2598,65 @@ function Field({
|
|
|
2566
2598
|
};
|
|
2567
2599
|
|
|
2568
2600
|
return (
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2601
|
+
<>
|
|
2602
|
+
{Array.isArray(settings.variables) &&
|
|
2603
|
+
settings.variables.length > 0 && (
|
|
2604
|
+
<div
|
|
2605
|
+
style={{
|
|
2606
|
+
display: 'flex',
|
|
2607
|
+
flexWrap: 'wrap',
|
|
2608
|
+
gap: '6px',
|
|
2609
|
+
marginBottom: '8px',
|
|
2610
|
+
}}
|
|
2611
|
+
>
|
|
2612
|
+
{settings.variables.map((v) => {
|
|
2613
|
+
const token = v.token ?? v;
|
|
2614
|
+
return (
|
|
2615
|
+
<button
|
|
2616
|
+
type="button"
|
|
2617
|
+
key={token}
|
|
2618
|
+
title={`Insert ${token}`}
|
|
2619
|
+
onClick={() =>
|
|
2620
|
+
insertVariableAtCursor(
|
|
2621
|
+
variableTextareaRef.current,
|
|
2622
|
+
token
|
|
2623
|
+
)
|
|
2624
|
+
}
|
|
2625
|
+
style={{
|
|
2626
|
+
cursor: 'pointer',
|
|
2627
|
+
fontSize: '12px',
|
|
2628
|
+
fontWeight: 500,
|
|
2629
|
+
padding: '3px 9px',
|
|
2630
|
+
borderRadius: '999px',
|
|
2631
|
+
border:
|
|
2632
|
+
'1px solid rgba(var(--primary-rgb), 0.3)',
|
|
2633
|
+
background:
|
|
2634
|
+
'rgba(var(--primary-rgb), 0.06)',
|
|
2635
|
+
color: 'var(--primary-color)',
|
|
2636
|
+
}}
|
|
2637
|
+
>
|
|
2638
|
+
{v.label ?? token}
|
|
2639
|
+
</button>
|
|
2640
|
+
);
|
|
2641
|
+
})}
|
|
2642
|
+
</div>
|
|
2643
|
+
)}
|
|
2644
|
+
<textarea
|
|
2645
|
+
ref={variableTextareaRef}
|
|
2646
|
+
data-name={settings.id}
|
|
2647
|
+
className={inputClass[settings.id]}
|
|
2648
|
+
onChange={onChange}
|
|
2649
|
+
value={
|
|
2650
|
+
inputValue && inputValue !== 'null'
|
|
2651
|
+
? inputValue
|
|
2652
|
+
: ''
|
|
2653
|
+
}
|
|
2654
|
+
rows={
|
|
2655
|
+
settings.rows ||
|
|
2656
|
+
getTextareaRows(settings.textareaHeight)
|
|
2657
|
+
}
|
|
2658
|
+
></textarea>
|
|
2659
|
+
</>
|
|
2580
2660
|
);
|
|
2581
2661
|
case 'text':
|
|
2582
2662
|
return (
|
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
Users,
|
|
9
9
|
User,
|
|
10
10
|
Search,
|
|
11
|
+
UserSearch,
|
|
12
|
+
ClipboardCheck,
|
|
11
13
|
LogOut,
|
|
12
14
|
Shield,
|
|
13
15
|
Image,
|
|
@@ -236,6 +238,9 @@ function Navigation({
|
|
|
236
238
|
phone: Phone,
|
|
237
239
|
shield: Shield,
|
|
238
240
|
signOut: LogOut,
|
|
241
|
+
search: Search,
|
|
242
|
+
userSearch: UserSearch,
|
|
243
|
+
clipboardCheck: ClipboardCheck,
|
|
239
244
|
};
|
|
240
245
|
|
|
241
246
|
// Check if the setting's URL matches the current page
|
|
@@ -302,7 +307,9 @@ function Navigation({
|
|
|
302
307
|
target={n.target ? n.target : '_self'}
|
|
303
308
|
title={n.label}
|
|
304
309
|
>
|
|
305
|
-
|
|
310
|
+
{IconComponent && (
|
|
311
|
+
<IconComponent size={19} strokeWidth={2} />
|
|
312
|
+
)}
|
|
306
313
|
</Link>
|
|
307
314
|
</li>
|
|
308
315
|
);
|
|
@@ -1145,17 +1145,58 @@ export const renderOptionColumn = ({
|
|
|
1145
1145
|
let newData;
|
|
1146
1146
|
|
|
1147
1147
|
if (data && column.options && column.options[data[column.id]]) {
|
|
1148
|
+
const rawValue = data[column.id];
|
|
1149
|
+
|
|
1148
1150
|
if (
|
|
1149
1151
|
column.key &&
|
|
1150
1152
|
data[column.key] > 0 &&
|
|
1151
|
-
column.options[
|
|
1153
|
+
column.options[rawValue].includes('#')
|
|
1152
1154
|
) {
|
|
1153
|
-
newData = column.options[
|
|
1155
|
+
newData = column.options[rawValue].replace(
|
|
1154
1156
|
/#/g,
|
|
1155
1157
|
data[column.key]
|
|
1156
1158
|
);
|
|
1157
1159
|
} else {
|
|
1158
|
-
newData = column.options[
|
|
1160
|
+
newData = column.options[rawValue];
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// Coloured badge variant — opt in via column.badge. Per-value
|
|
1164
|
+
// colours come from column.badgeColours[rawValue] ({ bg, fg,
|
|
1165
|
+
// border }); unmapped values fall back to a neutral pill.
|
|
1166
|
+
// Without column.badge the original plain-text behaviour is
|
|
1167
|
+
// unchanged (backward compatible).
|
|
1168
|
+
if (column.badge) {
|
|
1169
|
+
const palette =
|
|
1170
|
+
(column.badgeColours &&
|
|
1171
|
+
column.badgeColours[rawValue]) || {
|
|
1172
|
+
bg: '#eceff1',
|
|
1173
|
+
fg: '#37474f',
|
|
1174
|
+
};
|
|
1175
|
+
|
|
1176
|
+
return (
|
|
1177
|
+
<CellWithTooltip value={newData} columnType="option">
|
|
1178
|
+
<span
|
|
1179
|
+
style={{
|
|
1180
|
+
display: 'inline-block',
|
|
1181
|
+
padding: '2px 10px',
|
|
1182
|
+
borderRadius: '999px',
|
|
1183
|
+
fontSize: '11px',
|
|
1184
|
+
fontWeight: 600,
|
|
1185
|
+
lineHeight: 1.6,
|
|
1186
|
+
letterSpacing: '0.02em',
|
|
1187
|
+
textTransform: 'uppercase',
|
|
1188
|
+
whiteSpace: 'nowrap',
|
|
1189
|
+
backgroundColor: palette.bg,
|
|
1190
|
+
color: palette.fg,
|
|
1191
|
+
border: palette.border
|
|
1192
|
+
? `1px solid ${palette.border}`
|
|
1193
|
+
: '1px solid transparent',
|
|
1194
|
+
}}
|
|
1195
|
+
>
|
|
1196
|
+
{newData}
|
|
1197
|
+
</span>
|
|
1198
|
+
</CellWithTooltip>
|
|
1199
|
+
);
|
|
1159
1200
|
}
|
|
1160
1201
|
|
|
1161
1202
|
return (
|
|
@@ -231,8 +231,9 @@ function GenericDetail({
|
|
|
231
231
|
layout = 'full',
|
|
232
232
|
setting,
|
|
233
233
|
setActiveNav = null,
|
|
234
|
-
urlParam,
|
|
234
|
+
urlParam = 'dataId',
|
|
235
235
|
userProfile,
|
|
236
|
+
actions = null,
|
|
236
237
|
}) {
|
|
237
238
|
const gridRef = useRef(null);
|
|
238
239
|
const currentConfigRef = useRef(null);
|
|
@@ -2828,6 +2829,17 @@ function GenericDetail({
|
|
|
2828
2829
|
{!inPlaceEditing ? (
|
|
2829
2830
|
<button
|
|
2830
2831
|
className={styles.btn}
|
|
2832
|
+
title="Edit"
|
|
2833
|
+
aria-label="Edit"
|
|
2834
|
+
style={{
|
|
2835
|
+
display: 'inline-flex',
|
|
2836
|
+
alignItems: 'center',
|
|
2837
|
+
justifyContent:
|
|
2838
|
+
'center',
|
|
2839
|
+
gap: '7px',
|
|
2840
|
+
minWidth: '92px',
|
|
2841
|
+
height: '42px',
|
|
2842
|
+
}}
|
|
2831
2843
|
onClick={() => {
|
|
2832
2844
|
if (
|
|
2833
2845
|
activeTabConfig.form
|
|
@@ -2846,7 +2858,11 @@ function GenericDetail({
|
|
|
2846
2858
|
}
|
|
2847
2859
|
}}
|
|
2848
2860
|
>
|
|
2849
|
-
|
|
2861
|
+
<Pencil
|
|
2862
|
+
size={18}
|
|
2863
|
+
strokeWidth={2.25}
|
|
2864
|
+
/>
|
|
2865
|
+
<span>Edit</span>
|
|
2850
2866
|
</button>
|
|
2851
2867
|
) : (
|
|
2852
2868
|
editingTabId ===
|
|
@@ -3786,11 +3802,22 @@ function GenericDetail({
|
|
|
3786
3802
|
}`}
|
|
3787
3803
|
>
|
|
3788
3804
|
<Breadcrumb data={data} page={page} />
|
|
3789
|
-
{total > 0 && (
|
|
3790
|
-
<div
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3805
|
+
{(actions || total > 0) && (
|
|
3806
|
+
<div
|
|
3807
|
+
style={{
|
|
3808
|
+
display: 'flex',
|
|
3809
|
+
alignItems: 'center',
|
|
3810
|
+
gap: '0.75rem',
|
|
3811
|
+
}}
|
|
3812
|
+
>
|
|
3813
|
+
{total > 0 && (
|
|
3814
|
+
<div className={styles.titleInfo}>
|
|
3815
|
+
<span>
|
|
3816
|
+
[<strong>{total}</strong> Total]
|
|
3817
|
+
</span>
|
|
3818
|
+
</div>
|
|
3819
|
+
)}
|
|
3820
|
+
{actions}
|
|
3794
3821
|
</div>
|
|
3795
3822
|
)}
|
|
3796
3823
|
</div>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import '../styles/global.css';
|
|
2
2
|
|
|
3
|
-
import React, { useCallback, useEffect, useInsertionEffect, useRef, useState } from 'react';
|
|
4
|
-
import { useParams, Outlet } from 'react-router-dom';
|
|
3
|
+
import React, { useCallback, useEffect, useInsertionEffect, useMemo, useRef, useState } from 'react';
|
|
4
|
+
import { useParams, useSearchParams, Outlet } from 'react-router-dom';
|
|
5
5
|
import { toast } from 'react-toastify';
|
|
6
6
|
import { saveAs } from 'file-saver';
|
|
7
7
|
import _ from 'lodash';
|
|
@@ -35,6 +35,61 @@ function useWindowDimensions() {
|
|
|
35
35
|
return { windowHeight, windowWidth };
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Returns a copy of `filters` with the `show` flags adjusted so the subnav
|
|
40
|
+
* item (or child) whose id matches `tabId` is active, enabling deep-links
|
|
41
|
+
* such as /page?tab=tourFormats. If `tabId` is empty or matches nothing,
|
|
42
|
+
* the original filters are returned unchanged (default behaviour preserved).
|
|
43
|
+
*/
|
|
44
|
+
function applyTabParam(filters, tabId) {
|
|
45
|
+
if (!filters || !tabId) {
|
|
46
|
+
return filters;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// The left subnav highlight is driven by each item's `class` (and
|
|
50
|
+
// `active`) — not `show` — so we must move all three. Reuse whatever
|
|
51
|
+
// class the currently-active item uses so we stay theme-consistent.
|
|
52
|
+
const activeClass =
|
|
53
|
+
filters.find((f) => f.show && f.class)?.class || 'subactive';
|
|
54
|
+
const activeChildClass =
|
|
55
|
+
filters
|
|
56
|
+
.flatMap((f) => f.children || [])
|
|
57
|
+
.find((c) => c.show && c.class)?.class || 'subactivechildren';
|
|
58
|
+
|
|
59
|
+
let matched = false;
|
|
60
|
+
const next = filters.map((f) => {
|
|
61
|
+
if (f.isParent && Array.isArray(f.children)) {
|
|
62
|
+
const childMatch = f.children.some((c) => c.id === tabId);
|
|
63
|
+
if (childMatch) matched = true;
|
|
64
|
+
return {
|
|
65
|
+
...f,
|
|
66
|
+
show: childMatch,
|
|
67
|
+
active: childMatch,
|
|
68
|
+
class: childMatch ? f.class || activeClass : '',
|
|
69
|
+
children: f.children.map((c) => {
|
|
70
|
+
const isMatch = c.id === tabId;
|
|
71
|
+
return {
|
|
72
|
+
...c,
|
|
73
|
+
show: isMatch,
|
|
74
|
+
active: isMatch,
|
|
75
|
+
class: isMatch ? c.class || activeChildClass : '',
|
|
76
|
+
};
|
|
77
|
+
}),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const isMatch = f.id === tabId;
|
|
81
|
+
if (isMatch) matched = true;
|
|
82
|
+
return {
|
|
83
|
+
...f,
|
|
84
|
+
show: isMatch,
|
|
85
|
+
active: isMatch,
|
|
86
|
+
class: isMatch ? activeClass : '',
|
|
87
|
+
};
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return matched ? next : filters;
|
|
91
|
+
}
|
|
92
|
+
|
|
38
93
|
function useConfig(setting, tabs, filters, setRowsSelected, tableSetting) {
|
|
39
94
|
const [config, setConfig] = useState({});
|
|
40
95
|
const [subnav, setSubnav] = useState(filters || []);
|
|
@@ -258,10 +313,20 @@ function GenericIndex({
|
|
|
258
313
|
};
|
|
259
314
|
}, [setting?.tabletMode]);
|
|
260
315
|
|
|
316
|
+
// Honour a ?tab=<id> deep-link by seeding the active subnav item.
|
|
317
|
+
// Memoised so a stable reference is passed to useConfig (otherwise its
|
|
318
|
+
// reset effect would clobber the user's tab clicks every render).
|
|
319
|
+
const [searchParams] = useSearchParams();
|
|
320
|
+
const tabParam = searchParams.get('tab');
|
|
321
|
+
const effectiveFilters = useMemo(
|
|
322
|
+
() => applyTabParam(setting.filters, tabParam),
|
|
323
|
+
[setting.filters, tabParam]
|
|
324
|
+
);
|
|
325
|
+
|
|
261
326
|
const { config, setConfig, subnav, setSubnav } = useConfig(
|
|
262
327
|
setting,
|
|
263
328
|
setting.tabs,
|
|
264
|
-
|
|
329
|
+
effectiveFilters,
|
|
265
330
|
setRowsSelected,
|
|
266
331
|
setting.tableSetting
|
|
267
332
|
);
|