@visns-studio/visns-components 5.15.35 → 5.16.1
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
|
@@ -12,11 +12,11 @@
|
|
|
12
12
|
"@nivo/core": "^0.87.0",
|
|
13
13
|
"@nivo/line": "^0.87.0",
|
|
14
14
|
"@nivo/pie": "^0.87.0",
|
|
15
|
-
"@react-pdf/renderer": "^4.
|
|
16
|
-
"@tanstack/react-query": "^5.
|
|
15
|
+
"@react-pdf/renderer": "^4.5.1",
|
|
16
|
+
"@tanstack/react-query": "^5.99.2",
|
|
17
17
|
"@tinymce/miniature": "^6.0.0",
|
|
18
18
|
"@tinymce/tinymce-react": "^6.3.0",
|
|
19
|
-
"@uiw/react-color": "^2.
|
|
19
|
+
"@uiw/react-color": "^2.10.1",
|
|
20
20
|
"@visns-studio/visns-datagrid-community": "1.1.0",
|
|
21
21
|
"@visns-studio/visns-datagrid-enterprise": "1.1.0",
|
|
22
22
|
"@vitejs/plugin-react": "^4.7.0",
|
|
@@ -30,10 +30,10 @@
|
|
|
30
30
|
"framer-motion": "^12.38.0",
|
|
31
31
|
"html-react-parser": "^5.2.17",
|
|
32
32
|
"html2canvas": "^1.4.1",
|
|
33
|
-
"lodash": "^4.
|
|
33
|
+
"lodash": "^4.18.1",
|
|
34
34
|
"lodash.debounce": "^4.0.8",
|
|
35
35
|
"lucide-react": "^0.544.0",
|
|
36
|
-
"mapbox-gl": "^3.
|
|
36
|
+
"mapbox-gl": "^3.22.0",
|
|
37
37
|
"moment": "^2.30.1",
|
|
38
38
|
"motion": "^12.38.0",
|
|
39
39
|
"numeral": "^2.0.6",
|
|
@@ -57,9 +57,9 @@
|
|
|
57
57
|
"react-slugify": "^4.0.1",
|
|
58
58
|
"react-sortable-hoc": "^2.0.0",
|
|
59
59
|
"react-to-print": "^3.3.0",
|
|
60
|
-
"react-toastify": "^11.0
|
|
60
|
+
"react-toastify": "^11.1.0",
|
|
61
61
|
"react-toggle": "^4.1.3",
|
|
62
|
-
"react-tooltip": "^5.30.
|
|
62
|
+
"react-tooltip": "^5.30.1",
|
|
63
63
|
"react-window": "^1.8.11",
|
|
64
64
|
"reactjs-popup": "^2.0.6",
|
|
65
65
|
"style-loader": "^4.0.0",
|
|
@@ -68,11 +68,11 @@
|
|
|
68
68
|
"tesseract.js": "^6.0.1",
|
|
69
69
|
"truncate": "^3.0.0",
|
|
70
70
|
"uuid": "^11.1.0",
|
|
71
|
-
"validator": "^13.15.
|
|
72
|
-
"vite": "^6.4.
|
|
71
|
+
"validator": "^13.15.35",
|
|
72
|
+
"vite": "^6.4.2",
|
|
73
73
|
"xlsx": "^0.18.5",
|
|
74
74
|
"yarn": "^1.22.22",
|
|
75
|
-
"yet-another-react-lightbox": "^3.
|
|
75
|
+
"yet-another-react-lightbox": "^3.31.0"
|
|
76
76
|
},
|
|
77
77
|
"devDependencies": {
|
|
78
78
|
"@babel/core": "^7.29.0",
|
|
@@ -83,10 +83,10 @@
|
|
|
83
83
|
"copy-webpack-plugin": "^13.0.1",
|
|
84
84
|
"css-loader": "^7.1.4",
|
|
85
85
|
"css-minimizer-webpack-plugin": "^7.0.4",
|
|
86
|
-
"mini-css-extract-plugin": "^2.10.
|
|
86
|
+
"mini-css-extract-plugin": "^2.10.2",
|
|
87
87
|
"react": "^18.3.1",
|
|
88
88
|
"react-dom": "^18.3.1",
|
|
89
|
-
"sass": "^1.
|
|
89
|
+
"sass": "^1.99.0",
|
|
90
90
|
"sass-loader": "^16.0.7"
|
|
91
91
|
},
|
|
92
92
|
"peerDependencies": {
|
|
@@ -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.16.1",
|
|
98
98
|
"description": "Various packages to assist in the development of our Custom Applications.",
|
|
99
99
|
"main": "src/index.js",
|
|
100
100
|
"files": [
|
|
@@ -472,16 +472,19 @@ const DataGrid = forwardRef(
|
|
|
472
472
|
setColumnsMetadata(res.metadata);
|
|
473
473
|
}
|
|
474
474
|
|
|
475
|
-
//
|
|
475
|
+
// Group rows together while preserving the backend's sortBy order:
|
|
476
|
+
// first appearance of each group key determines group order, so when
|
|
477
|
+
// the backend sorts by e.g. pickling_completed_at desc, groups appear
|
|
478
|
+
// in the order their most-recent member finished.
|
|
476
479
|
if (ajaxSetting?.groupBy?.length > 0) {
|
|
477
480
|
const groupField = ajaxSetting.groupBy[0]; // groupBy is array of strings
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
const
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
481
|
+
const buckets = new Map();
|
|
482
|
+
for (const row of res.data) {
|
|
483
|
+
const key = (row[groupField] ?? '').toString();
|
|
484
|
+
if (!buckets.has(key)) buckets.set(key, []);
|
|
485
|
+
buckets.get(key).push(row);
|
|
486
|
+
}
|
|
487
|
+
res.data = Array.from(buckets.values()).flat();
|
|
485
488
|
|
|
486
489
|
// Initialize all groups as collapsed when data is first loaded
|
|
487
490
|
if (
|
|
@@ -3933,6 +3936,89 @@ const DataGrid = forwardRef(
|
|
|
3933
3936
|
/>
|
|
3934
3937
|
)}
|
|
3935
3938
|
</strong>
|
|
3939
|
+
{/* Display fields surfaced from the group's rows. Assumes value is consistent
|
|
3940
|
+
across the group (e.g. set via groupUpdate). Skips empty values. */}
|
|
3941
|
+
{(() => {
|
|
3942
|
+
const displayFields =
|
|
3943
|
+
ajaxSetting
|
|
3944
|
+
?.groupBySetting
|
|
3945
|
+
?.displayFields ||
|
|
3946
|
+
[];
|
|
3947
|
+
if (
|
|
3948
|
+
!displayFields.length ||
|
|
3949
|
+
!ajaxSetting?.groupBy
|
|
3950
|
+
)
|
|
3951
|
+
return null;
|
|
3952
|
+
const groupField =
|
|
3953
|
+
ajaxSetting
|
|
3954
|
+
.groupBy[0];
|
|
3955
|
+
const groupRow =
|
|
3956
|
+
dataRef.current?.find(
|
|
3957
|
+
(row) =>
|
|
3958
|
+
row[
|
|
3959
|
+
groupField
|
|
3960
|
+
] ===
|
|
3961
|
+
groupValue
|
|
3962
|
+
);
|
|
3963
|
+
if (!groupRow)
|
|
3964
|
+
return null;
|
|
3965
|
+
return displayFields.map(
|
|
3966
|
+
(fieldId) => {
|
|
3967
|
+
const value =
|
|
3968
|
+
groupRow[
|
|
3969
|
+
fieldId
|
|
3970
|
+
];
|
|
3971
|
+
if (
|
|
3972
|
+
value ===
|
|
3973
|
+
null ||
|
|
3974
|
+
value ===
|
|
3975
|
+
undefined ||
|
|
3976
|
+
value ===
|
|
3977
|
+
''
|
|
3978
|
+
)
|
|
3979
|
+
return null;
|
|
3980
|
+
const colDef =
|
|
3981
|
+
columns?.find(
|
|
3982
|
+
(
|
|
3983
|
+
c
|
|
3984
|
+
) =>
|
|
3985
|
+
c.id ===
|
|
3986
|
+
fieldId
|
|
3987
|
+
);
|
|
3988
|
+
const label =
|
|
3989
|
+
colDef?.label ||
|
|
3990
|
+
fieldId;
|
|
3991
|
+
return (
|
|
3992
|
+
<span
|
|
3993
|
+
key={`group-display-${fieldId}`}
|
|
3994
|
+
className={
|
|
3995
|
+
styles.groupHeaderDisplayField
|
|
3996
|
+
}
|
|
3997
|
+
>
|
|
3998
|
+
<span
|
|
3999
|
+
className={
|
|
4000
|
+
styles.groupHeaderDisplayFieldLabel
|
|
4001
|
+
}
|
|
4002
|
+
>
|
|
4003
|
+
{
|
|
4004
|
+
label
|
|
4005
|
+
}
|
|
4006
|
+
:
|
|
4007
|
+
</span>{' '}
|
|
4008
|
+
<span
|
|
4009
|
+
className={
|
|
4010
|
+
styles.groupHeaderDisplayFieldValue
|
|
4011
|
+
}
|
|
4012
|
+
>
|
|
4013
|
+
{
|
|
4014
|
+
value
|
|
4015
|
+
}
|
|
4016
|
+
</span>
|
|
4017
|
+
</span>
|
|
4018
|
+
);
|
|
4019
|
+
}
|
|
4020
|
+
);
|
|
4021
|
+
})()}
|
|
3936
4022
|
{/* Click indicator */}
|
|
3937
4023
|
<span
|
|
3938
4024
|
className={
|
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import { X as CircleX } from 'lucide-react';
|
|
3
|
+
import Select, { components } from 'react-select';
|
|
4
|
+
import styles from '../styles/GenericFormBuilder.module.scss';
|
|
5
|
+
|
|
6
|
+
// Field types that cannot produce a value we can compare against — disabled in the picker.
|
|
7
|
+
const NON_VALUE_TYPES = new Set([
|
|
8
|
+
'plaintext',
|
|
9
|
+
'plaintextheading',
|
|
10
|
+
'signature',
|
|
11
|
+
'canvas',
|
|
12
|
+
'file',
|
|
13
|
+
'image',
|
|
14
|
+
'videoUpload',
|
|
15
|
+
'table_text',
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
// Field types whose `options` array should populate the Value dropdown.
|
|
19
|
+
const OPTION_VALUE_TYPES = new Set([
|
|
20
|
+
'checkbox',
|
|
21
|
+
'dropdown',
|
|
22
|
+
'radio',
|
|
23
|
+
'toggle',
|
|
24
|
+
'table_radio',
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
// Stored as a literal string so the existing PDF blade (pdf/form.blade.php:132) keeps working.
|
|
28
|
+
const FILLED_IN_SENTINEL = 'not null';
|
|
29
|
+
|
|
30
|
+
const operatorOf = (conditionalValue) =>
|
|
31
|
+
conditionalValue === FILLED_IN_SENTINEL ? 'is filled in' : 'is';
|
|
32
|
+
|
|
33
|
+
// Group fields by their preceding `type: 'section'` header. Fields that can't be
|
|
34
|
+
// used in a condition (non-value types, or fields that appear after the current
|
|
35
|
+
// one so their value isn't yet known) are omitted from the list entirely.
|
|
36
|
+
const buildGroupedOptions = (allFields, currentField, typeLabelFor) => {
|
|
37
|
+
const currentIndex = allFields.findIndex(
|
|
38
|
+
(f) => f.id === currentField.id
|
|
39
|
+
);
|
|
40
|
+
// Brand-new field not yet in the array → treat as "at the end" so every
|
|
41
|
+
// existing field qualifies as an earlier reference.
|
|
42
|
+
const currentPos = currentIndex === -1 ? allFields.length : currentIndex;
|
|
43
|
+
|
|
44
|
+
const groups = [];
|
|
45
|
+
let group = { label: 'General', options: [] };
|
|
46
|
+
groups.push(group);
|
|
47
|
+
|
|
48
|
+
allFields.forEach((f, idx) => {
|
|
49
|
+
if (!f || !f.id) return;
|
|
50
|
+
if (f.type === 'section') {
|
|
51
|
+
group = { label: f.label || 'Section', options: [] };
|
|
52
|
+
groups.push(group);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (f.id === currentField.id) return;
|
|
56
|
+
if (NON_VALUE_TYPES.has(f.type)) return;
|
|
57
|
+
if (idx >= currentPos) return;
|
|
58
|
+
|
|
59
|
+
group.options.push({
|
|
60
|
+
value: f.id,
|
|
61
|
+
label: f.label || '(untitled)',
|
|
62
|
+
type: f.type,
|
|
63
|
+
typeLabel: typeLabelFor(f.type),
|
|
64
|
+
sectionLabel: group.label,
|
|
65
|
+
hasOptions: OPTION_VALUE_TYPES.has(f.type),
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Within each group, surface option-bearing fields first; preserve document
|
|
70
|
+
// order inside each bucket via a stable sort index.
|
|
71
|
+
groups.forEach((g) => {
|
|
72
|
+
g.options = g.options
|
|
73
|
+
.map((o, i) => ({ ...o, __i: i }))
|
|
74
|
+
.sort((a, b) => {
|
|
75
|
+
if (a.hasOptions !== b.hasOptions)
|
|
76
|
+
return a.hasOptions ? -1 : 1;
|
|
77
|
+
return a.__i - b.__i;
|
|
78
|
+
})
|
|
79
|
+
.map(({ __i, ...rest }) => rest);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return groups.filter((g) => g.options.length > 0);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// Custom Option renderer — shows the field label alongside a small type pill
|
|
86
|
+
// so duplicate labels stay distinguishable.
|
|
87
|
+
const FieldOption = (props) => {
|
|
88
|
+
const { data } = props;
|
|
89
|
+
return (
|
|
90
|
+
<components.Option {...props}>
|
|
91
|
+
<div
|
|
92
|
+
style={{
|
|
93
|
+
display: 'flex',
|
|
94
|
+
alignItems: 'center',
|
|
95
|
+
justifyContent: 'space-between',
|
|
96
|
+
gap: 8,
|
|
97
|
+
}}
|
|
98
|
+
>
|
|
99
|
+
<span
|
|
100
|
+
style={{
|
|
101
|
+
overflow: 'hidden',
|
|
102
|
+
textOverflow: 'ellipsis',
|
|
103
|
+
whiteSpace: 'nowrap',
|
|
104
|
+
}}
|
|
105
|
+
>
|
|
106
|
+
{data.label}
|
|
107
|
+
</span>
|
|
108
|
+
<span
|
|
109
|
+
style={{
|
|
110
|
+
fontSize: '0.72rem',
|
|
111
|
+
color: '#666',
|
|
112
|
+
background: 'rgba(0,0,0,0.06)',
|
|
113
|
+
borderRadius: 4,
|
|
114
|
+
padding: '1px 6px',
|
|
115
|
+
flexShrink: 0,
|
|
116
|
+
}}
|
|
117
|
+
>
|
|
118
|
+
{data.typeLabel}
|
|
119
|
+
</span>
|
|
120
|
+
</div>
|
|
121
|
+
</components.Option>
|
|
122
|
+
);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// Collapsed (selected) display carries the section breadcrumb so duplicate labels
|
|
126
|
+
// (e.g. many "Notes" fields) remain distinguishable after selection.
|
|
127
|
+
const FieldSingleValue = (props) => {
|
|
128
|
+
const { data } = props;
|
|
129
|
+
return (
|
|
130
|
+
<components.SingleValue {...props}>
|
|
131
|
+
{data.sectionLabel && data.sectionLabel !== 'General' ? (
|
|
132
|
+
<span style={{ color: '#888' }}>
|
|
133
|
+
{data.sectionLabel} ›{' '}
|
|
134
|
+
</span>
|
|
135
|
+
) : null}
|
|
136
|
+
{data.label}
|
|
137
|
+
{data.typeLabel ? (
|
|
138
|
+
<span style={{ color: '#888' }}> — {data.typeLabel}</span>
|
|
139
|
+
) : null}
|
|
140
|
+
</components.SingleValue>
|
|
141
|
+
);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
function ConditionalDisplayEditor({
|
|
145
|
+
currentField,
|
|
146
|
+
allFields = [],
|
|
147
|
+
fieldTypes = [],
|
|
148
|
+
onChange,
|
|
149
|
+
}) {
|
|
150
|
+
const conditionalFieldId = currentField.conditional_field || '';
|
|
151
|
+
const conditionalValue = currentField.conditional_value || '';
|
|
152
|
+
const operator = operatorOf(conditionalValue);
|
|
153
|
+
|
|
154
|
+
const selectedField = allFields.find((f) => f.id === conditionalFieldId);
|
|
155
|
+
const referenceMissing = conditionalFieldId && !selectedField;
|
|
156
|
+
|
|
157
|
+
const typeLabelFor = (type) =>
|
|
158
|
+
fieldTypes.find((t) => t.value === type)?.label || type;
|
|
159
|
+
|
|
160
|
+
const groupedOptions = useMemo(
|
|
161
|
+
() => buildGroupedOptions(allFields, currentField, typeLabelFor),
|
|
162
|
+
// typeLabelFor is pure over fieldTypes; depend on its source array.
|
|
163
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
164
|
+
[allFields, currentField.id, fieldTypes]
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
const selectedOption = useMemo(() => {
|
|
168
|
+
if (!conditionalFieldId) return null;
|
|
169
|
+
for (const g of groupedOptions) {
|
|
170
|
+
const match = g.options.find((o) => o.value === conditionalFieldId);
|
|
171
|
+
if (match) return match;
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}, [groupedOptions, conditionalFieldId]);
|
|
175
|
+
|
|
176
|
+
const targetHasOptions =
|
|
177
|
+
selectedField && OPTION_VALUE_TYPES.has(selectedField.type);
|
|
178
|
+
const targetOptions = targetHasOptions ? selectedField.options || [] : [];
|
|
179
|
+
const selectedValueOption = targetOptions.find(
|
|
180
|
+
(o) => o.id === conditionalValue
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
const emitChange = (patch) => {
|
|
184
|
+
if (onChange) onChange(patch);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const handleFieldChange = (option) => {
|
|
188
|
+
emitChange({
|
|
189
|
+
conditional_field: option ? option.value : '',
|
|
190
|
+
conditional_value: '',
|
|
191
|
+
});
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const handleOperatorChange = (e) => {
|
|
195
|
+
const nextOperator = e.target.value;
|
|
196
|
+
if (nextOperator === 'is filled in') {
|
|
197
|
+
emitChange({ conditional_value: FILLED_IN_SENTINEL });
|
|
198
|
+
} else if (conditionalValue === FILLED_IN_SENTINEL) {
|
|
199
|
+
emitChange({ conditional_value: '' });
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const handleValueChange = (e) => {
|
|
204
|
+
emitChange({ conditional_value: e.target.value });
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const handleClear = (e) => {
|
|
208
|
+
e.preventDefault();
|
|
209
|
+
emitChange({ conditional_field: '', conditional_value: '' });
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const renderValueControl = () => {
|
|
213
|
+
if (operator === 'is filled in') return null;
|
|
214
|
+
if (!selectedField) return null;
|
|
215
|
+
|
|
216
|
+
if (targetHasOptions) {
|
|
217
|
+
return (
|
|
218
|
+
<select
|
|
219
|
+
name="conditional_value"
|
|
220
|
+
value={conditionalValue}
|
|
221
|
+
onChange={handleValueChange}
|
|
222
|
+
>
|
|
223
|
+
<option value="">Please select a value</option>
|
|
224
|
+
{targetOptions.map((opt, i) => (
|
|
225
|
+
<option
|
|
226
|
+
value={opt.id}
|
|
227
|
+
key={`cond-value-${opt.id}-${i}`}
|
|
228
|
+
>
|
|
229
|
+
{opt.label}
|
|
230
|
+
</option>
|
|
231
|
+
))}
|
|
232
|
+
</select>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const inputType = selectedField.type === 'number' ? 'number' : 'text';
|
|
237
|
+
return (
|
|
238
|
+
<input
|
|
239
|
+
name="conditional_value"
|
|
240
|
+
value={conditionalValue}
|
|
241
|
+
type={inputType}
|
|
242
|
+
onChange={handleValueChange}
|
|
243
|
+
/>
|
|
244
|
+
);
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const fieldBreadcrumb = selectedOption
|
|
248
|
+
? `${
|
|
249
|
+
selectedOption.sectionLabel &&
|
|
250
|
+
selectedOption.sectionLabel !== 'General'
|
|
251
|
+
? `${selectedOption.sectionLabel} › `
|
|
252
|
+
: ''
|
|
253
|
+
}${selectedOption.label}`
|
|
254
|
+
: selectedField
|
|
255
|
+
? selectedField.label
|
|
256
|
+
: '…';
|
|
257
|
+
|
|
258
|
+
const renderPreview = () => {
|
|
259
|
+
if (!conditionalFieldId && !conditionalValue) {
|
|
260
|
+
return (
|
|
261
|
+
<span>
|
|
262
|
+
No condition set — this field will always be shown.
|
|
263
|
+
</span>
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (referenceMissing) return null;
|
|
268
|
+
|
|
269
|
+
if (operator === 'is filled in') {
|
|
270
|
+
return (
|
|
271
|
+
<span>
|
|
272
|
+
This field will only show when{' '}
|
|
273
|
+
<strong>{fieldBreadcrumb}</strong> is filled in.
|
|
274
|
+
</span>
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (!conditionalValue) {
|
|
279
|
+
return (
|
|
280
|
+
<span>
|
|
281
|
+
Pick a value for <strong>{fieldBreadcrumb}</strong> to
|
|
282
|
+
finish the rule.
|
|
283
|
+
</span>
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const valueLabel = targetHasOptions
|
|
288
|
+
? selectedValueOption?.label || conditionalValue
|
|
289
|
+
: conditionalValue;
|
|
290
|
+
|
|
291
|
+
return (
|
|
292
|
+
<span>
|
|
293
|
+
This field will only show when{' '}
|
|
294
|
+
<strong>{fieldBreadcrumb}</strong> is{' '}
|
|
295
|
+
<strong>{valueLabel}</strong>.
|
|
296
|
+
</span>
|
|
297
|
+
);
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const hasAnyRule = !!(conditionalFieldId || conditionalValue);
|
|
301
|
+
|
|
302
|
+
const reactSelectStyles = {
|
|
303
|
+
menu: (provided) => ({ ...provided, zIndex: 99999 }),
|
|
304
|
+
menuPortal: (base) => ({ ...base, zIndex: 99999 }),
|
|
305
|
+
control: (base) => ({
|
|
306
|
+
...base,
|
|
307
|
+
minHeight: 44,
|
|
308
|
+
borderRadius: 6,
|
|
309
|
+
}),
|
|
310
|
+
groupHeading: (base) => ({
|
|
311
|
+
...base,
|
|
312
|
+
fontSize: '0.7rem',
|
|
313
|
+
textTransform: 'uppercase',
|
|
314
|
+
letterSpacing: '0.04em',
|
|
315
|
+
color: 'var(--primary-color)',
|
|
316
|
+
fontWeight: 700,
|
|
317
|
+
padding: '6px 12px',
|
|
318
|
+
background: 'rgba(var(--primary-rgb), 0.05)',
|
|
319
|
+
}),
|
|
320
|
+
option: (base, state) => ({
|
|
321
|
+
...base,
|
|
322
|
+
cursor: state.isDisabled ? 'not-allowed' : 'pointer',
|
|
323
|
+
}),
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
return (
|
|
327
|
+
<>
|
|
328
|
+
<div className={`${styles.formItem} ${styles.fwItem}`}>
|
|
329
|
+
<div className={styles.gridtxt__header}>
|
|
330
|
+
<span>Conditional Field Display Criteria</span>
|
|
331
|
+
</div>
|
|
332
|
+
</div>
|
|
333
|
+
|
|
334
|
+
{referenceMissing ? (
|
|
335
|
+
<div
|
|
336
|
+
className={`${styles.formItem} ${styles.fwItem}`}
|
|
337
|
+
style={{
|
|
338
|
+
background: '#fff8e1',
|
|
339
|
+
border: '1px solid #f4c544',
|
|
340
|
+
borderRadius: 6,
|
|
341
|
+
padding: '8px 12px',
|
|
342
|
+
color: '#7a5c00',
|
|
343
|
+
fontSize: '0.85rem',
|
|
344
|
+
}}
|
|
345
|
+
>
|
|
346
|
+
The field this rule references no longer exists. Pick
|
|
347
|
+
another field or clear the rule.
|
|
348
|
+
</div>
|
|
349
|
+
) : null}
|
|
350
|
+
|
|
351
|
+
<div className={styles.formItem}>
|
|
352
|
+
<label className={styles.fi__label}>
|
|
353
|
+
<Select
|
|
354
|
+
inputId="conditional_field"
|
|
355
|
+
classNamePrefix="visns-select"
|
|
356
|
+
options={groupedOptions}
|
|
357
|
+
value={selectedOption}
|
|
358
|
+
onChange={handleFieldChange}
|
|
359
|
+
isClearable
|
|
360
|
+
isSearchable
|
|
361
|
+
placeholder="Always show this field (type to search…)"
|
|
362
|
+
menuPortalTarget={
|
|
363
|
+
typeof document !== 'undefined'
|
|
364
|
+
? document.body
|
|
365
|
+
: undefined
|
|
366
|
+
}
|
|
367
|
+
menuPlacement="auto"
|
|
368
|
+
components={{
|
|
369
|
+
Option: FieldOption,
|
|
370
|
+
SingleValue: FieldSingleValue,
|
|
371
|
+
}}
|
|
372
|
+
styles={reactSelectStyles}
|
|
373
|
+
noOptionsMessage={() => 'No matching fields'}
|
|
374
|
+
/>
|
|
375
|
+
<span
|
|
376
|
+
className={`${styles.fi__span} ${styles.prefocus}`}
|
|
377
|
+
>
|
|
378
|
+
Show only when
|
|
379
|
+
</span>
|
|
380
|
+
</label>
|
|
381
|
+
</div>
|
|
382
|
+
|
|
383
|
+
<div className={styles.formItem}>
|
|
384
|
+
<label className={styles.fi__label}>
|
|
385
|
+
<select
|
|
386
|
+
name="conditional_operator"
|
|
387
|
+
value={operator}
|
|
388
|
+
onChange={handleOperatorChange}
|
|
389
|
+
disabled={!conditionalFieldId}
|
|
390
|
+
>
|
|
391
|
+
<option value="is">is</option>
|
|
392
|
+
<option value="is filled in">is filled in</option>
|
|
393
|
+
</select>
|
|
394
|
+
<span className={styles.fi__span}>Condition</span>
|
|
395
|
+
</label>
|
|
396
|
+
</div>
|
|
397
|
+
|
|
398
|
+
{(() => {
|
|
399
|
+
const valueControl = renderValueControl();
|
|
400
|
+
if (!valueControl) return null;
|
|
401
|
+
return (
|
|
402
|
+
<div className={styles.formItem}>
|
|
403
|
+
<label className={styles.fi__label}>
|
|
404
|
+
{valueControl}
|
|
405
|
+
<span className={styles.fi__span}>Value</span>
|
|
406
|
+
</label>
|
|
407
|
+
</div>
|
|
408
|
+
);
|
|
409
|
+
})()}
|
|
410
|
+
|
|
411
|
+
<div
|
|
412
|
+
className={`${styles.formItem} ${styles.fwItem}`}
|
|
413
|
+
style={{
|
|
414
|
+
background: 'rgba(0,0,0,0.03)',
|
|
415
|
+
borderRadius: 6,
|
|
416
|
+
padding: '8px 12px',
|
|
417
|
+
color: 'var(--paragraph-color)',
|
|
418
|
+
fontSize: '0.85rem',
|
|
419
|
+
display: 'flex',
|
|
420
|
+
alignItems: 'center',
|
|
421
|
+
justifyContent: 'space-between',
|
|
422
|
+
gap: 8,
|
|
423
|
+
}}
|
|
424
|
+
>
|
|
425
|
+
<div>{renderPreview()}</div>
|
|
426
|
+
{hasAnyRule ? (
|
|
427
|
+
<button
|
|
428
|
+
type="button"
|
|
429
|
+
onClick={handleClear}
|
|
430
|
+
title="Clear rule"
|
|
431
|
+
style={{
|
|
432
|
+
background: 'transparent',
|
|
433
|
+
border: 'none',
|
|
434
|
+
cursor: 'pointer',
|
|
435
|
+
display: 'inline-flex',
|
|
436
|
+
alignItems: 'center',
|
|
437
|
+
color: '#666',
|
|
438
|
+
}}
|
|
439
|
+
>
|
|
440
|
+
<CircleX size={16} />
|
|
441
|
+
</button>
|
|
442
|
+
) : null}
|
|
443
|
+
</div>
|
|
444
|
+
</>
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export default ConditionalDisplayEditor;
|
|
@@ -47,6 +47,7 @@ import 'react-datepicker/dist/react-datepicker.css';
|
|
|
47
47
|
import Breadcrumb from '../Breadcrumb';
|
|
48
48
|
import CustomFetch from '../Fetch';
|
|
49
49
|
import SketchConfig from '../sketch/json/config.json';
|
|
50
|
+
import ConditionalDisplayEditor from './ConditionalDisplayEditor';
|
|
50
51
|
|
|
51
52
|
import styles from '../styles/GenericFormBuilder.module.scss'; // Import the CSS module
|
|
52
53
|
|
|
@@ -69,6 +70,8 @@ function GenericFormBuilder({ setting, urlParam, userProfile }) {
|
|
|
69
70
|
size: 'half',
|
|
70
71
|
required: 'no',
|
|
71
72
|
textareaHeight: 'normal',
|
|
73
|
+
conditional_field: '',
|
|
74
|
+
conditional_value: '',
|
|
72
75
|
});
|
|
73
76
|
const [dataOption, setDataOption] = useState({
|
|
74
77
|
id: '',
|
|
@@ -657,6 +660,8 @@ function GenericFormBuilder({ setting, urlParam, userProfile }) {
|
|
|
657
660
|
size: 'half',
|
|
658
661
|
required: 'no',
|
|
659
662
|
textareaHeight: 'normal',
|
|
663
|
+
conditional_field: '',
|
|
664
|
+
conditional_value: '',
|
|
660
665
|
}));
|
|
661
666
|
setModalType(() => ({
|
|
662
667
|
type: 'create',
|
|
@@ -739,11 +744,32 @@ function GenericFormBuilder({ setting, urlParam, userProfile }) {
|
|
|
739
744
|
let newDetail = [...data.detail];
|
|
740
745
|
newDetail.splice(index, 1);
|
|
741
746
|
|
|
747
|
+
let orphanCount = 0;
|
|
748
|
+
newDetail = newDetail.map((item) => {
|
|
749
|
+
if (item.conditional_field === id) {
|
|
750
|
+
orphanCount += 1;
|
|
751
|
+
return {
|
|
752
|
+
...item,
|
|
753
|
+
conditional_field: '',
|
|
754
|
+
conditional_value: '',
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
return item;
|
|
758
|
+
});
|
|
759
|
+
|
|
742
760
|
setData((items) => ({
|
|
743
761
|
...items,
|
|
744
762
|
detail: newDetail,
|
|
745
763
|
}));
|
|
746
764
|
|
|
765
|
+
if (orphanCount > 0) {
|
|
766
|
+
toast.info(
|
|
767
|
+
`Cleared ${orphanCount} conditional rule${
|
|
768
|
+
orphanCount === 1 ? '' : 's'
|
|
769
|
+
} that referenced the deleted field.`
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
|
|
747
773
|
handleCloseModal();
|
|
748
774
|
}
|
|
749
775
|
},
|
|
@@ -2214,63 +2240,17 @@ function GenericFormBuilder({ setting, urlParam, userProfile }) {
|
|
|
2214
2240
|
</div>
|
|
2215
2241
|
) : null}
|
|
2216
2242
|
|
|
2217
|
-
<
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
<div className={styles.formItem}>
|
|
2229
|
-
<label className={styles.fi__label}>
|
|
2230
|
-
<select
|
|
2231
|
-
name="conditional_field"
|
|
2232
|
-
value={
|
|
2233
|
-
dataField.conditional_field
|
|
2234
|
-
}
|
|
2235
|
-
onChange={handleChangeForm}
|
|
2236
|
-
>
|
|
2237
|
-
<option>
|
|
2238
|
-
Please Select an Option
|
|
2239
|
-
</option>
|
|
2240
|
-
{data.detail &&
|
|
2241
|
-
data.detail.length > 0 &&
|
|
2242
|
-
data.detail.map(
|
|
2243
|
-
(item, key) => (
|
|
2244
|
-
<option
|
|
2245
|
-
value={item.id}
|
|
2246
|
-
key={`conditional-field-${item.id}-${key}`}
|
|
2247
|
-
>
|
|
2248
|
-
{item.label}
|
|
2249
|
-
</option>
|
|
2250
|
-
)
|
|
2251
|
-
)}
|
|
2252
|
-
</select>
|
|
2253
|
-
<span className={styles.fi__span}>
|
|
2254
|
-
Field
|
|
2255
|
-
</span>
|
|
2256
|
-
</label>
|
|
2257
|
-
</div>
|
|
2258
|
-
|
|
2259
|
-
<div className={styles.formItem}>
|
|
2260
|
-
<label className={styles.fi__label}>
|
|
2261
|
-
<input
|
|
2262
|
-
name="conditional_value"
|
|
2263
|
-
value={
|
|
2264
|
-
dataField.conditional_value
|
|
2265
|
-
}
|
|
2266
|
-
type="text"
|
|
2267
|
-
onChange={handleChangeForm}
|
|
2268
|
-
/>
|
|
2269
|
-
<span className={styles.fi__span}>
|
|
2270
|
-
Value
|
|
2271
|
-
</span>
|
|
2272
|
-
</label>
|
|
2273
|
-
</div>
|
|
2243
|
+
<ConditionalDisplayEditor
|
|
2244
|
+
currentField={dataField}
|
|
2245
|
+
allFields={data.detail || []}
|
|
2246
|
+
fieldTypes={fieldTypes}
|
|
2247
|
+
onChange={(patch) =>
|
|
2248
|
+
setDataField((prev) => ({
|
|
2249
|
+
...prev,
|
|
2250
|
+
...patch,
|
|
2251
|
+
}))
|
|
2252
|
+
}
|
|
2253
|
+
/>
|
|
2274
2254
|
|
|
2275
2255
|
<div
|
|
2276
2256
|
className={`${styles.formItem} ${styles.fwItem} ${styles.lastItem}`}
|
|
@@ -1050,6 +1050,32 @@
|
|
|
1050
1050
|
user-select: none;
|
|
1051
1051
|
}
|
|
1052
1052
|
|
|
1053
|
+
.groupHeaderDisplayField {
|
|
1054
|
+
display: inline-flex;
|
|
1055
|
+
align-items: baseline;
|
|
1056
|
+
gap: 4px;
|
|
1057
|
+
padding: 2px 8px;
|
|
1058
|
+
margin-left: 4px;
|
|
1059
|
+
background: rgba(59, 130, 246, 0.08);
|
|
1060
|
+
border: 1px solid rgba(59, 130, 246, 0.18);
|
|
1061
|
+
border-radius: 10px;
|
|
1062
|
+
font-size: 0.72rem;
|
|
1063
|
+
line-height: 1.2;
|
|
1064
|
+
white-space: nowrap;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
.groupHeaderDisplayFieldLabel {
|
|
1068
|
+
color: #6b7280;
|
|
1069
|
+
font-weight: 500;
|
|
1070
|
+
text-transform: none;
|
|
1071
|
+
letter-spacing: 0;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
.groupHeaderDisplayFieldValue {
|
|
1075
|
+
color: var(--secondary-color, #1f2937);
|
|
1076
|
+
font-weight: 600;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1053
1079
|
.groupHeaderContainer {
|
|
1054
1080
|
display: flex;
|
|
1055
1081
|
align-items: center;
|