@evoke-platform/ui-components 1.4.0-testing.2 → 1.4.0-testing.20
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/dist/published/components/core/Alert/Alert.js +1 -1
- package/dist/published/components/core/Autocomplete/Autocomplete.js +3 -3
- package/dist/published/components/core/DatePicker/DatePicker.js +1 -1
- package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.js +63 -64
- package/dist/published/components/custom/CriteriaBuilder/PropertyTree.js +3 -3
- package/dist/published/components/custom/CriteriaBuilder/ValueEditor.js +25 -17
- package/dist/published/components/custom/CriteriaBuilder/index.d.ts +2 -1
- package/dist/published/components/custom/CriteriaBuilder/index.js +2 -1
- package/dist/published/components/custom/CriteriaBuilder/utils.d.ts +13 -0
- package/dist/published/components/custom/CriteriaBuilder/utils.js +60 -3
- package/dist/published/components/custom/Form/Common/Form.js +19 -7
- package/dist/published/components/custom/Form/Common/FormComponentWrapper.js +2 -1
- package/dist/published/components/custom/Form/FormComponents/CriteriaComponent/Criteria.js +52 -1
- package/dist/published/components/custom/Form/FormComponents/ObjectComponent/ObjectComponent.js +9 -5
- package/dist/published/components/custom/Form/FormComponents/ObjectComponent/ObjectPropertyInput.js +3 -2
- package/dist/published/components/custom/Form/FormComponents/RepeatableFieldComponent/RepeatableField.js +1 -1
- package/dist/published/components/custom/Form/tests/Form.test.js +46 -0
- package/dist/published/components/custom/Form/tests/test-data.js +99 -0
- package/dist/published/components/custom/Form/utils.js +81 -39
- package/dist/published/components/custom/FormField/AddressFieldComponent/addressFieldComponent.js +1 -1
- package/dist/published/components/custom/HistoryLog/DisplayedProperty.d.ts +2 -1
- package/dist/published/components/custom/HistoryLog/DisplayedProperty.js +5 -2
- package/dist/published/components/custom/HistoryLog/HistoryData.d.ts +1 -0
- package/dist/published/components/custom/HistoryLog/HistoryData.js +9 -3
- package/dist/published/components/custom/HistoryLog/index.js +24 -2
- package/dist/published/components/custom/index.d.ts +1 -1
- package/dist/published/components/custom/index.js +1 -1
- package/dist/published/index.d.ts +1 -1
- package/dist/published/index.js +1 -1
- package/package.json +2 -2
@@ -31,7 +31,7 @@ const colorMap = {
|
|
31
31
|
const Alert = (props) => {
|
32
32
|
const { children, action, severity, onClose, color } = props;
|
33
33
|
const getIcon = () => {
|
34
|
-
const iconColor = color ? colorMap[color] ?? color : severity ? colorMap[severity] : colorMap['success'];
|
34
|
+
const iconColor = color ? (colorMap[color] ?? color) : severity ? colorMap[severity] : colorMap['success'];
|
35
35
|
switch (severity) {
|
36
36
|
case 'error':
|
37
37
|
return React.createElement(ErrorRounded, { sx: { ...styles.icon, color: iconColor } });
|
@@ -14,7 +14,7 @@ const Autocomplete = (props) => {
|
|
14
14
|
? option
|
15
15
|
: typeof option?.label === 'boolean'
|
16
16
|
? new Boolean(option?.label).toString()
|
17
|
-
: option?.label ?? '',
|
17
|
+
: (option?.label ?? ''),
|
18
18
|
value: option?.value ?? option,
|
19
19
|
};
|
20
20
|
})
|
@@ -62,7 +62,7 @@ const Autocomplete = (props) => {
|
|
62
62
|
marginTop: '3px',
|
63
63
|
borderRadius: '8px',
|
64
64
|
...props.sx,
|
65
|
-
}, options: sortedOptions, popupIcon:
|
65
|
+
}, options: sortedOptions, popupIcon: props.popupIcon ? props.popupIcon : props.readOnly || props.disabled ? null : React.createElement(ExpandMore, null) }),
|
66
66
|
props.error && React.createElement(FieldError, { required: props.required, label: props.errorMessage })));
|
67
67
|
}
|
68
68
|
else {
|
@@ -78,7 +78,7 @@ const Autocomplete = (props) => {
|
|
78
78
|
backgroundColor: props.readOnly ? '#f4f6f8' : 'auto',
|
79
79
|
borderRadius: '8px',
|
80
80
|
...props.sx,
|
81
|
-
}, options: sortedOptions, popupIcon:
|
81
|
+
}, options: sortedOptions, popupIcon: props.popupIcon ? props.popupIcon : props.readOnly || props.disabled ? null : React.createElement(ExpandMore, null) }),
|
82
82
|
props.error && React.createElement(FieldError, { required: props.required, label: props.errorMessage })));
|
83
83
|
}
|
84
84
|
};
|
@@ -21,7 +21,7 @@ const DatePicker = (props) => {
|
|
21
21
|
if (newValue instanceof Error) {
|
22
22
|
newValue = new InvalidDate(keyboardInputValue);
|
23
23
|
}
|
24
|
-
handleChange(newValue, keyboardInputValue);
|
24
|
+
handleChange(newValue instanceof InvalidDate || newValue === null ? newValue : LocalDate.from(newValue), keyboardInputValue);
|
25
25
|
};
|
26
26
|
return (React.createElement(UIThemeProvider, null,
|
27
27
|
React.createElement(MUIDatePicker, { value: value, onChange: onChange, renderInput: (params) => React.createElement(TextField, { ...params }), PaperProps: {
|
@@ -12,23 +12,8 @@ import { Box } from '../../layout';
|
|
12
12
|
import { OverflowTextField } from '../OverflowTextField';
|
13
13
|
import { difference } from '../util';
|
14
14
|
import PropertyTree from './PropertyTree';
|
15
|
-
import { parseMongoDB, traversePropertyPath } from './utils';
|
15
|
+
import { ALL_OPERATORS, parseMongoDB, traversePropertyPath } from './utils';
|
16
16
|
import ValueEditor from './ValueEditor';
|
17
|
-
const ALL_OPERATORS = [
|
18
|
-
{ name: '=', label: 'Is' },
|
19
|
-
{ name: '!=', label: 'Is not' },
|
20
|
-
{ name: '<', label: 'Less than' },
|
21
|
-
{ name: '>', label: 'Greater than' },
|
22
|
-
{ name: '<=', label: 'Less than or equal to' },
|
23
|
-
{ name: '>=', label: 'Greater than or equal to' },
|
24
|
-
{ name: 'contains', label: 'Contains' },
|
25
|
-
{ name: 'beginsWith', label: 'Starts with' },
|
26
|
-
{ name: 'endsWith', label: 'Ends with' },
|
27
|
-
{ name: 'null', label: 'Is empty' },
|
28
|
-
{ name: 'notNull', label: 'Is not empty' },
|
29
|
-
{ name: 'in', label: 'In' },
|
30
|
-
{ name: 'notIn', label: 'Not in' },
|
31
|
-
];
|
32
17
|
const styles = {
|
33
18
|
buttons: {
|
34
19
|
padding: '6px 16px',
|
@@ -139,7 +124,7 @@ const customSelector = (props) => {
|
|
139
124
|
.map((option) => ({ name: option.name, label: option.label }));
|
140
125
|
val = val === '=' ? '' : options.find((option) => option.name === val).name;
|
141
126
|
}
|
142
|
-
else if (inputType === 'document') {
|
127
|
+
else if (inputType === 'document' || inputType === 'criteria') {
|
143
128
|
opts = options
|
144
129
|
.filter((option) => ['null', 'notNull'].includes(option.name))
|
145
130
|
.map((option) => ({ name: option.name, label: option.label }));
|
@@ -185,7 +170,7 @@ const customSelector = (props) => {
|
|
185
170
|
break;
|
186
171
|
case 'Fields':
|
187
172
|
placeholder = 'Select Property';
|
188
|
-
width = '
|
173
|
+
width = '37%';
|
189
174
|
val = options.find((option) => option.name === val)?.name;
|
190
175
|
break;
|
191
176
|
}
|
@@ -281,22 +266,7 @@ export const valueEditor = (props) => {
|
|
281
266
|
};
|
282
267
|
const CriteriaBuilder = (props) => {
|
283
268
|
const { properties, criteria, setCriteria, originalCriteria, enablePresetValues, presetValues, operators, disabled, disabledCriteria, hideBorder, presetGroupLabel, customValueEditor, treeViewOpts, disableRegexEscapeChars, } = props;
|
284
|
-
const [query, setQuery] = useState(undefined);
|
285
269
|
const [propertyTreeMap, setPropertyTreeMap] = useState();
|
286
|
-
useEffect(() => {
|
287
|
-
if (criteria || originalCriteria) {
|
288
|
-
const criteriaToParse = criteria || originalCriteria || {};
|
289
|
-
const updatedQuery = parseMongoDB(criteriaToParse);
|
290
|
-
!isEmpty(treeViewOpts) && updatePropertyTreeMap(updatedQuery);
|
291
|
-
setQuery({
|
292
|
-
...updatedQuery,
|
293
|
-
rules: processRules(updatedQuery.rules, true),
|
294
|
-
});
|
295
|
-
}
|
296
|
-
else {
|
297
|
-
setQuery({ combinator: 'and', rules: [] });
|
298
|
-
}
|
299
|
-
}, [originalCriteria]);
|
300
270
|
const processRules = (rules, isSavedValue) => {
|
301
271
|
return rules.map((rule) => {
|
302
272
|
if ('rules' in rule) {
|
@@ -308,13 +278,7 @@ const CriteriaBuilder = (props) => {
|
|
308
278
|
else {
|
309
279
|
const propertyType = properties.find((property) => property.id === rule.field)?.type;
|
310
280
|
let adjustedValue = rule.value;
|
311
|
-
if ((
|
312
|
-
((propertyType === 'string' || propertyType === 'richText') &&
|
313
|
-
(rule.operator === 'in' || rule.operator === 'notIn'))) &&
|
314
|
-
isSavedValue) {
|
315
|
-
adjustedValue = rule.value?.split(',');
|
316
|
-
}
|
317
|
-
else if ((rule.operator === 'null' || rule.operator === 'notNull') && rule.value) {
|
281
|
+
if ((rule.operator === 'null' || rule.operator === 'notNull') && rule.value) {
|
318
282
|
adjustedValue = null;
|
319
283
|
}
|
320
284
|
return {
|
@@ -325,31 +289,67 @@ const CriteriaBuilder = (props) => {
|
|
325
289
|
}
|
326
290
|
});
|
327
291
|
};
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
292
|
+
useEffect(() => {
|
293
|
+
if ((criteria || originalCriteria) &&
|
294
|
+
!isEmpty(treeViewOpts) &&
|
295
|
+
treeViewOpts.object &&
|
296
|
+
treeViewOpts.fetchObject) {
|
297
|
+
const { object, fetchObject } = treeViewOpts;
|
298
|
+
// this retrieves the properties from a treeview for each property in the query
|
299
|
+
// they are then used in the custom query builder components to determine the input type etc
|
300
|
+
const updatePropertyTreeMap = async () => {
|
301
|
+
const newQuery = parseMongoDB(criteria || originalCriteria || {});
|
302
|
+
const ids = [];
|
303
|
+
const traverseRulesForIds = (rules) => {
|
304
|
+
rules.forEach((rule) => {
|
305
|
+
if ('rules' in rule) {
|
306
|
+
traverseRulesForIds(rule.rules);
|
307
|
+
}
|
308
|
+
else {
|
309
|
+
ids.push(rule.field);
|
310
|
+
}
|
311
|
+
});
|
312
|
+
};
|
313
|
+
traverseRulesForIds(newQuery.rules);
|
314
|
+
let newPropertyTreeMap = {};
|
315
|
+
const newPropertyTreeMapPromises = [];
|
316
|
+
for (const id of ids) {
|
317
|
+
if (!propertyTreeMap?.[id]) {
|
318
|
+
newPropertyTreeMapPromises.push(traversePropertyPath(id, object, fetchObject)
|
319
|
+
.then((property) => {
|
320
|
+
if (property) {
|
321
|
+
return {
|
322
|
+
[id]: property,
|
323
|
+
};
|
324
|
+
}
|
325
|
+
return {};
|
326
|
+
})
|
327
|
+
.catch((err) => {
|
328
|
+
console.error(err);
|
329
|
+
return {};
|
330
|
+
}));
|
331
|
+
}
|
339
332
|
}
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
333
|
+
newPropertyTreeMap = (await Promise.all(newPropertyTreeMapPromises)).reduce((acc, currentProperty) => ({ ...acc, ...currentProperty }), {});
|
334
|
+
setPropertyTreeMap((prevPropertyTreeMap) => ({
|
335
|
+
...prevPropertyTreeMap,
|
336
|
+
...newPropertyTreeMap,
|
337
|
+
}));
|
338
|
+
};
|
339
|
+
updatePropertyTreeMap().catch((err) => console.error(err));
|
340
|
+
}
|
341
|
+
}, [criteria, originalCriteria, treeViewOpts]);
|
342
|
+
const initializeQuery = () => {
|
343
|
+
const criteriaToParse = criteria || originalCriteria;
|
344
|
+
const updatedQuery = criteriaToParse ? parseMongoDB(criteriaToParse || {}) : undefined;
|
345
|
+
return updatedQuery
|
346
|
+
? {
|
347
|
+
...updatedQuery,
|
348
|
+
rules: processRules(updatedQuery.rules, true),
|
349
349
|
}
|
350
|
-
|
351
|
-
});
|
350
|
+
: { combinator: 'and', rules: [] };
|
352
351
|
};
|
352
|
+
const [query, setQuery] = useState(initializeQuery);
|
353
353
|
const handleClearAll = () => {
|
354
354
|
handleQueryChange({ combinator: 'and', rules: [] });
|
355
355
|
};
|
@@ -437,7 +437,6 @@ const CriteriaBuilder = (props) => {
|
|
437
437
|
'.ruleGroup:not(.ruleGroup .ruleGroup)': {
|
438
438
|
borderStyle: 'hidden',
|
439
439
|
background: '#fff',
|
440
|
-
maxWidth: '70vw',
|
441
440
|
},
|
442
441
|
'.ruleGroup-header': {
|
443
442
|
display: 'block',
|
@@ -525,7 +524,7 @@ const CriteriaBuilder = (props) => {
|
|
525
524
|
justifyContent: 'space-between',
|
526
525
|
alignItems: 'center',
|
527
526
|
marginBottom: '10px',
|
528
|
-
|
527
|
+
width: '100%',
|
529
528
|
} },
|
530
529
|
React.createElement(Box, null,
|
531
530
|
React.createElement(Button, { sx: {
|
@@ -102,7 +102,7 @@ const PropertyTree = ({ fetchObject, handleTreePropertySelect, rootObject, value
|
|
102
102
|
}
|
103
103
|
};
|
104
104
|
return (React.createElement(Autocomplete, { "aria-label": "Property Selector", value: value, fullWidth: true, sx: {
|
105
|
-
width: '
|
105
|
+
width: '37%',
|
106
106
|
}, disableClearable: true, options: propertyOptions.map((property) => {
|
107
107
|
return {
|
108
108
|
label: objectPropertyNamePathMap[property.id],
|
@@ -123,8 +123,8 @@ const PropertyTree = ({ fetchObject, handleTreePropertySelect, rootObject, value
|
|
123
123
|
}, getOptionLabel: (option) => {
|
124
124
|
// Retrieve the full name path from the map
|
125
125
|
const namePath = typeof option === 'string'
|
126
|
-
? objectPropertyNamePathMap[option] ?? ''
|
127
|
-
: objectPropertyNamePathMap[option.value] ?? '';
|
126
|
+
? (objectPropertyNamePathMap[option] ?? '')
|
127
|
+
: (objectPropertyNamePathMap[option.value] ?? '');
|
128
128
|
return truncateNamePath(namePath, NAME_PATH_LIMIT);
|
129
129
|
}, renderInput: (params) => {
|
130
130
|
const fullDisplayName = value && objectPropertyNamePathMap[value];
|
@@ -1,10 +1,10 @@
|
|
1
1
|
import { Instant, LocalDate, LocalDateTime, LocalTime, ZoneId } from '@js-joda/core';
|
2
|
-
import { ClearRounded } from '@mui/icons-material';
|
2
|
+
import { ClearRounded, CodeRounded } from '@mui/icons-material';
|
3
3
|
import { Box, darken, lighten, styled } from '@mui/material';
|
4
4
|
import { TimePicker } from '@mui/x-date-pickers';
|
5
5
|
import React, { useEffect, useRef, useState } from 'react';
|
6
6
|
import { InvalidDate } from '../../../util';
|
7
|
-
import { Autocomplete, Chip, DatePicker, DateTimePicker, LocalizationProvider, Menu, MenuItem, TextField, Typography, } from '../../core';
|
7
|
+
import { Autocomplete, Chip, DatePicker, DateTimePicker, IconButton, LocalizationProvider, Menu, MenuItem, TextField, Typography, } from '../../core';
|
8
8
|
import { NumericFormat } from '../FormField/InputFieldComponent';
|
9
9
|
const GroupHeader = styled('div')(({ theme }) => ({
|
10
10
|
position: 'sticky',
|
@@ -27,10 +27,7 @@ const ValueEditor = (props) => {
|
|
27
27
|
if (!!context.treeViewOpts && !!property) {
|
28
28
|
inputType = property.type;
|
29
29
|
if (property.enum) {
|
30
|
-
values = property.enum.map((item) => ({
|
31
|
-
name: item,
|
32
|
-
label: item,
|
33
|
-
}));
|
30
|
+
values = property.enum.map((item) => ({ name: item, label: item }));
|
34
31
|
}
|
35
32
|
}
|
36
33
|
const [invalidDateTime, setInvalidDateTime] = useState(false);
|
@@ -42,8 +39,8 @@ const ValueEditor = (props) => {
|
|
42
39
|
const disabled = ['null', 'notNull'].includes(operator);
|
43
40
|
const presetValues = context.presetValues?.filter((val) => !val.type || val.type === inputType) ?? [];
|
44
41
|
const isPresetValue = (value) => value?.startsWith('{{{') && value?.endsWith('}}}');
|
45
|
-
const
|
46
|
-
const
|
42
|
+
const presetDisplayValue = presetValues?.find((option) => option.value.name === value)?.label;
|
43
|
+
const isPresetValueSelected = presetValues && typeof value === 'string' && isPresetValue(value) && !!presetDisplayValue;
|
47
44
|
let readOnly = context.disabled;
|
48
45
|
if (!readOnly && context.disabledCriteria) {
|
49
46
|
readOnly =
|
@@ -54,6 +51,7 @@ const ValueEditor = (props) => {
|
|
54
51
|
width: '33%',
|
55
52
|
background: readOnly ? '#f4f6f8' : '#fff',
|
56
53
|
borderRadius: '8px',
|
54
|
+
'& .MuiAutocomplete-tag': { backgroundColor: '#edeff1' },
|
57
55
|
},
|
58
56
|
};
|
59
57
|
useEffect(() => {
|
@@ -178,9 +176,7 @@ const ValueEditor = (props) => {
|
|
178
176
|
console.error('Error processing date value:', error);
|
179
177
|
setInvalidDateTime(true);
|
180
178
|
}
|
181
|
-
}, onClose: onClose, PopperProps: {
|
182
|
-
anchorEl,
|
183
|
-
}, renderInput: (params) => (React.createElement(Box, { sx: styles.input, ref: setAnchorEl },
|
179
|
+
}, onClose: onClose, PopperProps: { anchorEl }, renderInput: (params) => (React.createElement(Box, { sx: styles.input, ref: setAnchorEl },
|
184
180
|
React.createElement(TextField, { ...params, disabled: disabled, onClick: onClick, placeholder: "Value", size: "small", inputRef: inputRef, error: invalidDateTime }))), readOnly: readOnly })));
|
185
181
|
}
|
186
182
|
else if (inputType === 'number' || inputType === 'integer') {
|
@@ -274,14 +270,26 @@ const ValueEditor = (props) => {
|
|
274
270
|
}
|
275
271
|
};
|
276
272
|
return (React.createElement(React.Fragment, null,
|
277
|
-
isPresetValueSelected ? (React.createElement(
|
278
|
-
borderRadius: '8px',
|
279
|
-
fontSize: '14px',
|
273
|
+
isPresetValueSelected ? (React.createElement(Box, { ref: inputRef, sx: {
|
280
274
|
width: '33%',
|
281
|
-
|
282
|
-
padding: '0 5px',
|
275
|
+
display: 'flex',
|
283
276
|
justifyContent: 'space-between',
|
284
|
-
|
277
|
+
alignItems: 'center',
|
278
|
+
height: '40px',
|
279
|
+
border: readOnly ? undefined : '1px solid #d5d5d5',
|
280
|
+
borderRadius: '8px',
|
281
|
+
backgroundColor: readOnly ? '#edeff1' : '#ffffff',
|
282
|
+
} },
|
283
|
+
React.createElement(Chip, { label: presetDisplayValue, sx: {
|
284
|
+
fontSize: '14px',
|
285
|
+
margin: '6px',
|
286
|
+
backgroundColor: '#edeff1',
|
287
|
+
borderRadius: '6px',
|
288
|
+
color: '#212B36',
|
289
|
+
height: '28px',
|
290
|
+
}, icon: React.createElement(CodeRounded, { sx: { height: '18px' } }) }),
|
291
|
+
!readOnly && (React.createElement(IconButton, { onClick: clearValue, sx: { padding: '3px', margin: '3px' } },
|
292
|
+
React.createElement(ClearRounded, { fontSize: "small", sx: { color: 'rgba(0, 0, 0, 0.54)' } }))))) : (getEditor()),
|
285
293
|
!!presetValues?.length && (React.createElement(Menu, { open: openPresetValues, anchorEl: inputRef?.current, PaperProps: { sx: { borderRadius: '8px', width: inputRef?.current?.offsetWidth } }, onClose: onClose }, presetValues &&
|
286
294
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
287
295
|
presetValues.map((option) => (React.createElement(MenuItem, { ...props, onClick: () => setPresetValue(option.value.name), sx: { padding: '8px', minHeight: '25px' } },
|
@@ -1,3 +1,4 @@
|
|
1
|
+
import { Property } from '@evoke-platform/context';
|
1
2
|
import { RuleGroupType } from 'react-querybuilder';
|
2
3
|
import { ExpandedProperty, Obj, ObjectProperty } from '../../../types';
|
3
4
|
/**
|
@@ -61,4 +62,16 @@ export declare const truncateNamePath: (namePath: string, limit?: number) => str
|
|
61
62
|
* @returns {RuleGroupType} - Correctly formatted rule or rules for the query builder.
|
62
63
|
*/
|
63
64
|
export declare function parseMongoDB(mongoQuery: Record<string, unknown>): RuleGroupType;
|
65
|
+
export declare const ALL_OPERATORS: {
|
66
|
+
name: string;
|
67
|
+
label: string;
|
68
|
+
}[];
|
69
|
+
/**
|
70
|
+
* Gets a human readable representation of a MongoDB query.
|
71
|
+
*
|
72
|
+
* @param {Record<string, unknown>} [mongoQuery] - The MongoDB query
|
73
|
+
* @param {Property[]} [properties] - The object properties referenced in the query
|
74
|
+
* @returns {string} The resulting query string.
|
75
|
+
*/
|
76
|
+
export declare const getReadableQuery: (mongoQuery?: Record<string, unknown>, properties?: Property[]) => string;
|
64
77
|
export {};
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { isArray, isEmpty } from 'lodash';
|
1
|
+
import { isArray, isEmpty, startCase } from 'lodash';
|
2
2
|
/**
|
3
3
|
* Recursively updates a node in a tree structure by applying an updater function to the node with the specified ID.
|
4
4
|
*
|
@@ -246,14 +246,14 @@ export function parseMongoDB(mongoQuery) {
|
|
246
246
|
return {
|
247
247
|
field: key,
|
248
248
|
operator: 'in',
|
249
|
-
value:
|
249
|
+
value: value.$in ?? [],
|
250
250
|
};
|
251
251
|
}
|
252
252
|
else if ('$nin' in value) {
|
253
253
|
return {
|
254
254
|
field: key,
|
255
255
|
operator: 'notIn',
|
256
|
-
value:
|
256
|
+
value: value.$nin ?? [],
|
257
257
|
};
|
258
258
|
}
|
259
259
|
else {
|
@@ -302,3 +302,60 @@ export function parseMongoDB(mongoQuery) {
|
|
302
302
|
};
|
303
303
|
}
|
304
304
|
}
|
305
|
+
export const ALL_OPERATORS = [
|
306
|
+
{ name: '=', label: 'Is' },
|
307
|
+
{ name: '!=', label: 'Is not' },
|
308
|
+
{ name: '<', label: 'Less than' },
|
309
|
+
{ name: '>', label: 'Greater than' },
|
310
|
+
{ name: '<=', label: 'Less than or equal to' },
|
311
|
+
{ name: '>=', label: 'Greater than or equal to' },
|
312
|
+
{ name: 'contains', label: 'Contains' },
|
313
|
+
{ name: 'beginsWith', label: 'Starts with' },
|
314
|
+
{ name: 'endsWith', label: 'Ends with' },
|
315
|
+
{ name: 'null', label: 'Is empty' },
|
316
|
+
{ name: 'notNull', label: 'Is not empty' },
|
317
|
+
{ name: 'in', label: 'In' },
|
318
|
+
{ name: 'notIn', label: 'Not in' },
|
319
|
+
];
|
320
|
+
/**
|
321
|
+
* Gets a human readable representation of a MongoDB query.
|
322
|
+
*
|
323
|
+
* @param {Record<string, unknown>} [mongoQuery] - The MongoDB query
|
324
|
+
* @param {Property[]} [properties] - The object properties referenced in the query
|
325
|
+
* @returns {string} The resulting query string.
|
326
|
+
*/
|
327
|
+
export const getReadableQuery = (mongoQuery, properties) => {
|
328
|
+
function isPresetValue(value) {
|
329
|
+
return typeof value === 'string' && value.startsWith('{{{') && value.endsWith('}}}');
|
330
|
+
}
|
331
|
+
function parseValue(val) {
|
332
|
+
if (val && Array.isArray(val)) {
|
333
|
+
return val.map((v) => (isPresetValue(v) ? startCase(v.slice(3, -3)) : v)).join(', ');
|
334
|
+
}
|
335
|
+
else {
|
336
|
+
return isPresetValue(val) ? startCase(val.slice(3, -3)) : `${val}`;
|
337
|
+
}
|
338
|
+
}
|
339
|
+
function getOperatorLabel(operator) {
|
340
|
+
const operatorObj = ALL_OPERATORS.find((o) => o.name === operator);
|
341
|
+
const defaultLabel = operatorObj ? operatorObj.label.toLowerCase() : operator;
|
342
|
+
if (['<', '>', '<=', '>='].includes(operator)) {
|
343
|
+
return `is ${defaultLabel}`;
|
344
|
+
}
|
345
|
+
return defaultLabel;
|
346
|
+
}
|
347
|
+
function buildQueryString(rule) {
|
348
|
+
if ('combinator' in rule) {
|
349
|
+
return rule?.rules?.map(buildQueryString).filter(Boolean).join(` ${rule.combinator.toLowerCase()} `);
|
350
|
+
}
|
351
|
+
else {
|
352
|
+
const property = properties?.find((p) => p.id === rule.field);
|
353
|
+
return `${property?.name ?? rule.field} ${getOperatorLabel(rule.operator)} ${parseValue(rule.value)}`;
|
354
|
+
}
|
355
|
+
}
|
356
|
+
if (!mongoQuery) {
|
357
|
+
return '';
|
358
|
+
}
|
359
|
+
const parsedQuery = parseMongoDB(mongoQuery);
|
360
|
+
return buildQueryString(parsedQuery);
|
361
|
+
};
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import { useApp, useAuthenticationContext, } from '@evoke-platform/context';
|
2
2
|
import { Components, Form as FormIO, Utils } from '@formio/react';
|
3
3
|
import { flatten } from 'flat';
|
4
|
-
import { isEmpty, isEqual, isObject, pick, toPairs } from 'lodash';
|
4
|
+
import { isEmpty, isEqual, isObject, omit, pick, toPairs } from 'lodash';
|
5
5
|
import React, { useEffect, useRef, useState } from 'react';
|
6
6
|
import '../../../../styles/form-component.css';
|
7
7
|
import { Skeleton, Snackbar } from '../../../core';
|
@@ -126,7 +126,7 @@ export function Form(props) {
|
|
126
126
|
const allDefaultPages = { ...defaultPages, ...foundDefaultPages };
|
127
127
|
// visibleObjectProperties
|
128
128
|
if (input && object?.properties) {
|
129
|
-
const allCriteriaInputs = getAllCriteriaInputs(action?.inputProperties ? flattenFormComponents(action.inputProperties) : action?.parameters ?? []);
|
129
|
+
const allCriteriaInputs = getAllCriteriaInputs(action?.inputProperties ? flattenFormComponents(action.inputProperties) : (action?.parameters ?? []));
|
130
130
|
if (input.length || action?.type !== 'delete') {
|
131
131
|
// formIO builder-configured input properties exist
|
132
132
|
const newComponentProps = await addObjectPropertiesToComponentProps(object.properties, input, allCriteriaInputs, instance, {
|
@@ -139,6 +139,18 @@ export function Form(props) {
|
|
139
139
|
if (!hideButtons && !isReadOnly) {
|
140
140
|
newComponentProps.push(BottomButtons);
|
141
141
|
}
|
142
|
+
if (action?.type !== 'create') {
|
143
|
+
// Add an additional, hidden _instance property on update/delete actions
|
144
|
+
// so that instance data is accessible in conditional display logic.
|
145
|
+
const hiddenComponent = {
|
146
|
+
key: '_instance',
|
147
|
+
type: 'hidden',
|
148
|
+
input: true,
|
149
|
+
tableView: false,
|
150
|
+
defaultValue: instance,
|
151
|
+
};
|
152
|
+
newComponentProps.push(hiddenComponent);
|
153
|
+
}
|
142
154
|
setComponentProps(newComponentProps);
|
143
155
|
}
|
144
156
|
else {
|
@@ -313,8 +325,8 @@ export function Form(props) {
|
|
313
325
|
const savedValue = submittedFields[docProperty.id];
|
314
326
|
const originalValue = instance?.[docProperty.id];
|
315
327
|
const documentsToRemove = requestSuccess
|
316
|
-
? originalValue?.filter((file) => !savedValue?.some((f) => f.id === file.id)) ?? []
|
317
|
-
: savedValue?.filter((file) => !originalValue?.some((f) => f.id === file.id)) ?? [];
|
328
|
+
? (originalValue?.filter((file) => !savedValue?.some((f) => f.id === file.id)) ?? [])
|
329
|
+
: (savedValue?.filter((file) => !originalValue?.some((f) => f.id === file.id)) ?? []);
|
318
330
|
for (const doc of documentsToRemove) {
|
319
331
|
try {
|
320
332
|
await apiServices?.delete(getPrefixedUrl(`/objects/${object?.id}/instances/${instance?.id}/documents/${doc.id}`));
|
@@ -416,7 +428,7 @@ export function Form(props) {
|
|
416
428
|
key: 'save-draft',
|
417
429
|
variant: 'outlined',
|
418
430
|
isModal: !!closeModal,
|
419
|
-
onClick: async (data, setError, setSubmitting) => await saveHandler(data, 'draft', setError, setSubmitting),
|
431
|
+
onClick: async (data, setError, setSubmitting) => await saveHandler(omit(data, '_instance'), 'draft', setError, setSubmitting),
|
420
432
|
style: { lineHeight: '2.75', margin: '5px', padding: '0 10px' },
|
421
433
|
}
|
422
434
|
: undefined,
|
@@ -437,7 +449,7 @@ export function Form(props) {
|
|
437
449
|
variant: 'contained',
|
438
450
|
isModal: !!closeModal,
|
439
451
|
onClick: (data, setError, setSubmitting) => {
|
440
|
-
saveHandler(data, 'submit', setError, setSubmitting);
|
452
|
+
saveHandler(omit(data, '_instance'), 'submit', setError, setSubmitting);
|
441
453
|
},
|
442
454
|
},
|
443
455
|
],
|
@@ -447,7 +459,7 @@ export function Form(props) {
|
|
447
459
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
448
460
|
, {
|
449
461
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
450
|
-
onChange: (e) => !isEqual(e.data, formData) && setFormData(e.data), key: closeModal ? undefined : formKey, form: {
|
462
|
+
onChange: (e) => !isEqual(omit(e.data, '_instance'), formData) && setFormData(omit(e.data, '_instance')), key: closeModal ? undefined : formKey, form: {
|
451
463
|
display: 'form',
|
452
464
|
components: componentProps,
|
453
465
|
}, formReady: handleFormReady })) : (React.createElement(Box, null,
|
@@ -81,7 +81,8 @@ export const FormComponentWrapper = (props) => {
|
|
81
81
|
property && onChange(property.id, '');
|
82
82
|
} },
|
83
83
|
React.createElement(HighlightOffOutlined, { sx: clearBtnStyles }))))),
|
84
|
-
React.createElement(
|
84
|
+
React.createElement(Box, { sx: { ...(displayOption === 'radioButton' && { display: 'flex' }) } },
|
85
|
+
React.createElement(Typography, { variant: "caption", sx: descriptionStyles }, description)),
|
85
86
|
React.createElement(Box, { sx: { display: 'flex', flexDirection: 'row' } },
|
86
87
|
React.createElement(PrefixSuffix, { prefix: prefix, height: fieldHeight }),
|
87
88
|
React.createElement(Box, { sx: { width: '100%', paddingTop: '6px' } }, children),
|
@@ -18,7 +18,58 @@ export const Criteria = (props) => {
|
|
18
18
|
setLoadingError(true);
|
19
19
|
}
|
20
20
|
if (properties) {
|
21
|
-
|
21
|
+
const flattenProperties = properties.flatMap((prop) => {
|
22
|
+
if (prop.type === 'object' || prop.type === 'user') {
|
23
|
+
return [
|
24
|
+
{
|
25
|
+
id: `${prop.id}.id`,
|
26
|
+
name: `${prop.name} Id`,
|
27
|
+
type: 'string',
|
28
|
+
},
|
29
|
+
{
|
30
|
+
id: `${prop.id}.name`,
|
31
|
+
name: `${prop.name} Name`,
|
32
|
+
type: 'string',
|
33
|
+
},
|
34
|
+
];
|
35
|
+
}
|
36
|
+
else if (prop.type === 'address') {
|
37
|
+
return [
|
38
|
+
{
|
39
|
+
id: `${prop.id}.line1`,
|
40
|
+
name: `${prop.name} Line 1`,
|
41
|
+
type: 'string',
|
42
|
+
},
|
43
|
+
{
|
44
|
+
id: `${prop.id}.line2`,
|
45
|
+
name: `${prop.name} Line 2`,
|
46
|
+
type: 'string',
|
47
|
+
},
|
48
|
+
{
|
49
|
+
id: `${prop.id}.city`,
|
50
|
+
name: `${prop.name} City`,
|
51
|
+
type: 'string',
|
52
|
+
},
|
53
|
+
{
|
54
|
+
id: `${prop.id}.county`,
|
55
|
+
name: `${prop.name} County`,
|
56
|
+
type: 'string',
|
57
|
+
},
|
58
|
+
{
|
59
|
+
id: `${prop.id}.state`,
|
60
|
+
name: `${prop.name} State`,
|
61
|
+
type: 'string',
|
62
|
+
},
|
63
|
+
{
|
64
|
+
id: `${prop.id}.zipCode`,
|
65
|
+
name: `${prop.name} Zip Code`,
|
66
|
+
type: 'string',
|
67
|
+
},
|
68
|
+
];
|
69
|
+
}
|
70
|
+
return prop;
|
71
|
+
});
|
72
|
+
setProperties(flattenProperties);
|
22
73
|
setLoadingError(false);
|
23
74
|
}
|
24
75
|
setLoading(false);
|
package/dist/published/components/custom/Form/FormComponents/ObjectComponent/ObjectComponent.js
CHANGED
@@ -16,7 +16,7 @@ export class ObjectComponent extends ReactComponent {
|
|
16
16
|
delete this.errorDetails['api-error'];
|
17
17
|
const updatedValue = pick(value, 'id', 'name');
|
18
18
|
// set the value on the form instance at this.root.data
|
19
|
-
this.setValue(!isNil(updatedValue) ? updatedValue : '');
|
19
|
+
this.setValue(!isNil(updatedValue) && !isEmpty(updatedValue) ? updatedValue : '');
|
20
20
|
// update the value in the component instance
|
21
21
|
this.updateValue(!isNil(updatedValue) ? updatedValue : {}, { modified: true });
|
22
22
|
this.handleValidation();
|
@@ -71,7 +71,7 @@ export class ObjectComponent extends ReactComponent {
|
|
71
71
|
}
|
72
72
|
this.updatedCriteria = updateCriteriaInputs(this.criteria ?? {}, data, this.component.user);
|
73
73
|
if (this.visible) {
|
74
|
-
this.
|
74
|
+
this.attach(this.element);
|
75
75
|
}
|
76
76
|
});
|
77
77
|
}
|
@@ -115,7 +115,7 @@ export class ObjectComponent extends ReactComponent {
|
|
115
115
|
}
|
116
116
|
this.updatedDefaultValueCriteria = updateCriteriaInputs(this.defaultValueCriteria ?? {}, data, this.component.user);
|
117
117
|
if (this.visible) {
|
118
|
-
this.
|
118
|
+
this.attach(this.element);
|
119
119
|
}
|
120
120
|
});
|
121
121
|
}
|
@@ -158,7 +158,6 @@ export class ObjectComponent extends ReactComponent {
|
|
158
158
|
delete this.errorDetails['api-error'];
|
159
159
|
}
|
160
160
|
this.attach(this.element);
|
161
|
-
this.attachReact(this.element);
|
162
161
|
});
|
163
162
|
if (this.component.defaultValue) {
|
164
163
|
this.expandInstance();
|
@@ -216,11 +215,16 @@ export class ObjectComponent extends ReactComponent {
|
|
216
215
|
if (!root) {
|
217
216
|
root = element;
|
218
217
|
}
|
218
|
+
let updatedValue;
|
219
|
+
if (this.shouldSetValue)
|
220
|
+
updatedValue = this.dataForSetting;
|
221
|
+
else
|
222
|
+
updatedValue = this.dataValue;
|
219
223
|
const updatedComponent = {
|
220
224
|
...this.component,
|
221
225
|
instance: {
|
222
226
|
...this.component.instance,
|
223
|
-
[this.component.key]: isEmpty(
|
227
|
+
[this.component.key]: isEmpty(updatedValue) || isNil(updatedValue) || updatedValue.length === 0 ? null : updatedValue,
|
224
228
|
},
|
225
229
|
defaultValueCriteria: this.updatedDefaultValueCriteria,
|
226
230
|
};
|
package/dist/published/components/custom/Form/FormComponents/ObjectComponent/ObjectPropertyInput.js
CHANGED
@@ -227,7 +227,7 @@ export const ObjectPropertyInput = (props) => {
|
|
227
227
|
return option.value === value?.value;
|
228
228
|
}, options: options.map((o) => ({ label: o.name, value: o.id })), getOptionLabel: (option) => {
|
229
229
|
return typeof option === 'string'
|
230
|
-
? options.find((o) => o.id === option)?.name ?? ''
|
230
|
+
? (options.find((o) => o.id === option)?.name ?? '')
|
231
231
|
: option.label;
|
232
232
|
}, onKeyDownCapture: (e) => {
|
233
233
|
if (instance?.[property.id]?.id || selectedInstance?.id) {
|
@@ -292,7 +292,8 @@ export const ObjectPropertyInput = (props) => {
|
|
292
292
|
}
|
293
293
|
: {}),
|
294
294
|
} })), readOnly: !loadingOptions && !canUpdateProperty, error: error, sortBy: "NONE" }))) : (React.createElement(Box, { sx: {
|
295
|
-
padding: (instance?.[property.id]?.name ??
|
295
|
+
padding: (instance?.[property.id]?.name ??
|
296
|
+
selectedInstance?.name)
|
296
297
|
? '16.5px 14px'
|
297
298
|
: '10.5px 0',
|
298
299
|
} },
|
@@ -402,7 +402,7 @@ const RepeatableField = (props) => {
|
|
402
402
|
React.createElement(Tooltip, { title: "Delete" },
|
403
403
|
React.createElement(TrashCan, { sx: { ':hover': { color: '#A12723' } } })))))))))))),
|
404
404
|
hasCreateAction && (React.createElement(Button, { variant: "contained", sx: styles.addButton, onClick: addRow }, "Add"))),
|
405
|
-
relatedObject && openDialog && (React.createElement(ActionDialog, { object: relatedObject, open: openDialog, apiServices: apiServices, onClose: () => setOpenDialog(false), instanceInput: dialogType === 'update' ? relatedInstances.find((i) => i.id === selectedRow) ?? {} : {}, handleSubmit: save,
|
405
|
+
relatedObject && openDialog && (React.createElement(ActionDialog, { object: relatedObject, open: openDialog, apiServices: apiServices, onClose: () => setOpenDialog(false), instanceInput: dialogType === 'update' ? (relatedInstances.find((i) => i.id === selectedRow) ?? {}) : {}, handleSubmit: save,
|
406
406
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
407
407
|
objectInputCommonProps: { apiServices }, action: relatedObject?.actions?.find((a) => a.id ===
|
408
408
|
(dialogType === 'create' ? '_create' : dialogType === 'update' ? '_update' : '_delete')), instanceId: selectedRow, queryAddresses: queryAddresses, user: user, associatedObject: instance.id && property.relatedPropertyId
|
@@ -109,4 +109,50 @@ describe('Form component', () => {
|
|
109
109
|
await waitFor(() => expect(within(openAutocomplete).queryByRole('option', { name: 'NP Specialty Type #2' })).to.be.null);
|
110
110
|
});
|
111
111
|
});
|
112
|
+
describe('visibility configuration', () => {
|
113
|
+
it('shows fields based on instance data using JsonLogic', async () => {
|
114
|
+
server.use(http.get('/data/objects/license/instances/rnLicense', () => HttpResponse.json(rnLicense)));
|
115
|
+
render(React.createElement(Form, { actionId: 'jsonLogicDisplayTest', actionType: 'update', object: specialtyObject, apiServices: apiServices, instance: {
|
116
|
+
id: '123',
|
117
|
+
objectId: 'specialty',
|
118
|
+
name: 'Test Specialty Object Instance',
|
119
|
+
} }));
|
120
|
+
// Validate that specialty type dropdown renders
|
121
|
+
await screen.findByRole('combobox', { name: 'Specialty Type' });
|
122
|
+
});
|
123
|
+
it('hides fields based on instance data using JsonLogic', async () => {
|
124
|
+
server.use(http.get('/data/objects/license/instances/rnLicense', () => HttpResponse.json(rnLicense)));
|
125
|
+
render(React.createElement(Form, { actionId: 'jsonLogicDisplayTest', actionType: 'update', object: specialtyObject, apiServices: apiServices, instance: {
|
126
|
+
id: '123',
|
127
|
+
objectId: 'specialty',
|
128
|
+
name: 'Test Specialty Object Instance -- hidden',
|
129
|
+
} }));
|
130
|
+
// Validate that license dropdown renders
|
131
|
+
await screen.findByRole('combobox', { name: 'License' });
|
132
|
+
// Validate that specialty type dropdown does not render
|
133
|
+
expect(screen.queryByRole('combobox', { name: 'Specialty Type' })).to.be.null;
|
134
|
+
});
|
135
|
+
it('shows fields based on instance data using simple conditions', async () => {
|
136
|
+
server.use(http.get('/data/objects/license/instances/rnLicense', () => HttpResponse.json(rnLicense)));
|
137
|
+
render(React.createElement(Form, { actionId: 'simpleConditionDisplayTest', actionType: 'update', object: specialtyObject, apiServices: apiServices, instance: {
|
138
|
+
id: '123',
|
139
|
+
objectId: 'specialty',
|
140
|
+
name: 'Test Specialty Object Instance',
|
141
|
+
} }));
|
142
|
+
// Validate that specialty type dropdown renders
|
143
|
+
await screen.findByRole('combobox', { name: 'Specialty Type' });
|
144
|
+
});
|
145
|
+
it('hides fields based on instance data using simple conditions', async () => {
|
146
|
+
server.use(http.get('/data/objects/license/instances/rnLicense', () => HttpResponse.json(rnLicense)));
|
147
|
+
render(React.createElement(Form, { actionId: 'simpleConditionDisplayTest', actionType: 'update', object: specialtyObject, apiServices: apiServices, instance: {
|
148
|
+
id: '123',
|
149
|
+
objectId: 'specialty',
|
150
|
+
name: 'Test Specialty Object Instance -- hidden',
|
151
|
+
} }));
|
152
|
+
// Validate that license dropdown renders
|
153
|
+
await screen.findByRole('combobox', { name: 'License' });
|
154
|
+
// Validate that specialty type dropdown does not render
|
155
|
+
expect(screen.queryByRole('combobox', { name: 'Specialty Type' })).to.be.null;
|
156
|
+
});
|
157
|
+
});
|
112
158
|
});
|
@@ -170,6 +170,105 @@ export const specialtyObject = {
|
|
170
170
|
],
|
171
171
|
},
|
172
172
|
},
|
173
|
+
{
|
174
|
+
id: 'jsonLogicDisplayTest',
|
175
|
+
name: 'JsonLogic Display Test',
|
176
|
+
type: 'update',
|
177
|
+
outputEvent: 'Specialty Updated',
|
178
|
+
parameters: [
|
179
|
+
{
|
180
|
+
id: 'specialtyType',
|
181
|
+
name: 'Specialty Type',
|
182
|
+
type: 'object',
|
183
|
+
objectId: 'specialtyType',
|
184
|
+
},
|
185
|
+
{
|
186
|
+
id: 'license',
|
187
|
+
name: 'License',
|
188
|
+
type: 'object',
|
189
|
+
objectId: 'license',
|
190
|
+
},
|
191
|
+
],
|
192
|
+
form: {
|
193
|
+
entries: [
|
194
|
+
{
|
195
|
+
parameterId: 'specialtyType',
|
196
|
+
type: 'input',
|
197
|
+
display: {
|
198
|
+
label: 'Specialty Type',
|
199
|
+
relatedObjectDisplay: 'dropdown',
|
200
|
+
visibility: {
|
201
|
+
'===': [
|
202
|
+
{
|
203
|
+
var: 'instance.name',
|
204
|
+
},
|
205
|
+
'Test Specialty Object Instance',
|
206
|
+
],
|
207
|
+
},
|
208
|
+
},
|
209
|
+
},
|
210
|
+
{
|
211
|
+
parameterId: 'license',
|
212
|
+
type: 'input',
|
213
|
+
display: {
|
214
|
+
label: 'License',
|
215
|
+
relatedObjectDisplay: 'dropdown',
|
216
|
+
},
|
217
|
+
},
|
218
|
+
],
|
219
|
+
},
|
220
|
+
},
|
221
|
+
{
|
222
|
+
id: 'simpleConditionDisplayTest',
|
223
|
+
name: 'Simple Condition Display Test',
|
224
|
+
type: 'update',
|
225
|
+
outputEvent: 'Specialty Updated',
|
226
|
+
parameters: [
|
227
|
+
{
|
228
|
+
id: 'specialtyType',
|
229
|
+
name: 'Specialty Type',
|
230
|
+
type: 'object',
|
231
|
+
objectId: 'specialtyType',
|
232
|
+
},
|
233
|
+
{
|
234
|
+
id: 'license',
|
235
|
+
name: 'License',
|
236
|
+
type: 'object',
|
237
|
+
objectId: 'license',
|
238
|
+
},
|
239
|
+
],
|
240
|
+
form: {
|
241
|
+
entries: [
|
242
|
+
{
|
243
|
+
parameterId: 'specialtyType',
|
244
|
+
type: 'input',
|
245
|
+
display: {
|
246
|
+
label: 'Specialty Type',
|
247
|
+
relatedObjectDisplay: 'dropdown',
|
248
|
+
visibility: {
|
249
|
+
operator: 'all',
|
250
|
+
conditions: [
|
251
|
+
{
|
252
|
+
property: 'name',
|
253
|
+
operator: 'eq',
|
254
|
+
value: 'Test Specialty Object Instance',
|
255
|
+
isInstanceProperty: true,
|
256
|
+
},
|
257
|
+
],
|
258
|
+
},
|
259
|
+
},
|
260
|
+
},
|
261
|
+
{
|
262
|
+
parameterId: 'license',
|
263
|
+
type: 'input',
|
264
|
+
display: {
|
265
|
+
label: 'License',
|
266
|
+
relatedObjectDisplay: 'dropdown',
|
267
|
+
},
|
268
|
+
},
|
269
|
+
],
|
270
|
+
},
|
271
|
+
},
|
173
272
|
],
|
174
273
|
};
|
175
274
|
export const specialtyTypeObject = {
|
@@ -100,15 +100,7 @@ export function convertFormToComponents(entries, parameters, object) {
|
|
100
100
|
? convertFormToComponents(section.entries, parameters, object)
|
101
101
|
: [],
|
102
102
|
})),
|
103
|
-
conditional:
|
104
|
-
? {
|
105
|
-
show: entry.visibility.conditions[0].operator === 'eq',
|
106
|
-
when: entry.visibility.conditions[0].property,
|
107
|
-
eq: entry.visibility.conditions[0].value,
|
108
|
-
}
|
109
|
-
: {
|
110
|
-
json: entry.visibility,
|
111
|
-
},
|
103
|
+
conditional: convertVisibilityToConditional(entry.visibility),
|
112
104
|
};
|
113
105
|
}
|
114
106
|
else if (entry.type === 'columns') {
|
@@ -124,15 +116,7 @@ export function convertFormToComponents(entries, parameters, object) {
|
|
124
116
|
? convertFormToComponents(column.entries, parameters, object)
|
125
117
|
: [],
|
126
118
|
})),
|
127
|
-
conditional:
|
128
|
-
? {
|
129
|
-
show: entry.visibility.conditions[0].operator === 'eq',
|
130
|
-
when: entry.visibility.conditions[0].property,
|
131
|
-
eq: entry.visibility.conditions[0].value,
|
132
|
-
}
|
133
|
-
: {
|
134
|
-
json: entry.visibility,
|
135
|
-
},
|
119
|
+
conditional: convertVisibilityToConditional(entry.visibility),
|
136
120
|
};
|
137
121
|
}
|
138
122
|
else if (entry.type === 'content') {
|
@@ -140,15 +124,7 @@ export function convertFormToComponents(entries, parameters, object) {
|
|
140
124
|
type: 'Content',
|
141
125
|
key: nanoid(),
|
142
126
|
html: entry.html,
|
143
|
-
conditional:
|
144
|
-
? {
|
145
|
-
show: entry.visibility.conditions[0].operator === 'eq',
|
146
|
-
when: entry.visibility.conditions[0].property,
|
147
|
-
eq: entry.visibility.conditions[0].value,
|
148
|
-
}
|
149
|
-
: {
|
150
|
-
json: entry.visibility,
|
151
|
-
},
|
127
|
+
conditional: convertVisibilityToConditional(entry.visibility),
|
152
128
|
};
|
153
129
|
}
|
154
130
|
else {
|
@@ -266,17 +242,7 @@ export function convertFormToComponents(entries, parameters, object) {
|
|
266
242
|
widget: (parameter.type === 'string' && parameter.enum) || parameter.type === 'array'
|
267
243
|
? 'choicejs'
|
268
244
|
: undefined,
|
269
|
-
conditional: displayOptions?.visibility
|
270
|
-
typeof displayOptions?.visibility !== 'string' &&
|
271
|
-
displayOptions.visibility.conditions?.length
|
272
|
-
? {
|
273
|
-
show: displayOptions?.visibility.conditions[0].operator === 'eq',
|
274
|
-
when: displayOptions?.visibility.conditions[0].property,
|
275
|
-
eq: displayOptions?.visibility.conditions[0].value,
|
276
|
-
}
|
277
|
-
: {
|
278
|
-
json: displayOptions?.visibility,
|
279
|
-
},
|
245
|
+
conditional: convertVisibilityToConditional(displayOptions?.visibility),
|
280
246
|
viewLayout: displayOptions?.viewLayout,
|
281
247
|
};
|
282
248
|
}
|
@@ -678,6 +644,9 @@ formComponents, allCriteriaInputs, instance, objectPropertyInputProps, associate
|
|
678
644
|
associatedObject?.propertyId === property.id) {
|
679
645
|
defaultValue = { id: associatedObject.instanceId };
|
680
646
|
component.hidden = true;
|
647
|
+
// If "conditional" is defined, the "hidden" property isn't respected.
|
648
|
+
// Remove the "conditional" property after setting "hidden" to true.
|
649
|
+
delete component.conditional;
|
681
650
|
}
|
682
651
|
return {
|
683
652
|
...component,
|
@@ -750,9 +719,12 @@ formComponents, allCriteriaInputs, instance, objectPropertyInputProps, associate
|
|
750
719
|
// Set the associated instance as a default value and hide the field.
|
751
720
|
if (associatedObject?.instanceId &&
|
752
721
|
associatedObject?.propertyId &&
|
753
|
-
item.property
|
722
|
+
item.property?.id === associatedObject.propertyId) {
|
754
723
|
item.defaultValue = { id: associatedObject.instanceId };
|
755
724
|
item.hidden = true;
|
725
|
+
// If "conditional" is defined, the "hidden" property isn't respected.
|
726
|
+
// Remove the "conditional" property after setting "hidden" to true.
|
727
|
+
delete item.conditional;
|
756
728
|
}
|
757
729
|
}
|
758
730
|
if (nestedFieldProperty) {
|
@@ -767,6 +739,7 @@ formComponents, allCriteriaInputs, instance, objectPropertyInputProps, associate
|
|
767
739
|
item.defaultPages = defaultPages;
|
768
740
|
item.navigateTo = navigateTo;
|
769
741
|
item.allCriteriaInputs = allCriteriaInputs;
|
742
|
+
item.properties = properties;
|
770
743
|
item.isModal = isModal;
|
771
744
|
item.fieldHeight = fieldHeight;
|
772
745
|
item.richTextEditor = ['RepeatableField', 'Object'].includes(item.type)
|
@@ -1313,3 +1286,72 @@ export function normalizeDates(instances, object) {
|
|
1313
1286
|
});
|
1314
1287
|
});
|
1315
1288
|
}
|
1289
|
+
/**
|
1290
|
+
* Given an object entry in a JsonLogic object, map it to the correct value.
|
1291
|
+
* @param entry An entry in a JsonLogic object.
|
1292
|
+
* @returns entry with all values starting with "instance" replaced with "data._instance"
|
1293
|
+
*/
|
1294
|
+
function processJsonLogicEntry(entry) {
|
1295
|
+
if (entry !== Object(entry)) {
|
1296
|
+
// entry is a primitive
|
1297
|
+
return typeof entry === 'string' ? entry.replace(/^instance/, 'data._instance') : entry;
|
1298
|
+
}
|
1299
|
+
else if (isArray(entry)) {
|
1300
|
+
return entry.map((element) => processJsonLogicEntry(element));
|
1301
|
+
}
|
1302
|
+
else if (typeof entry === 'object' && entry !== null) {
|
1303
|
+
let result = {};
|
1304
|
+
const entries = Object.entries(entry);
|
1305
|
+
for (const [key, val] of entries) {
|
1306
|
+
result = {
|
1307
|
+
...result,
|
1308
|
+
[key]: processJsonLogicEntry(val),
|
1309
|
+
};
|
1310
|
+
}
|
1311
|
+
return result;
|
1312
|
+
}
|
1313
|
+
return entry;
|
1314
|
+
}
|
1315
|
+
/**
|
1316
|
+
* Given a JsonLogic, replace all keys (if any) starting with "instance" with
|
1317
|
+
* "data._instance".
|
1318
|
+
*
|
1319
|
+
* @param jsonLogic A JsonLogic instance.
|
1320
|
+
* @returns jsonLogic, with all keys starting with "instance" replaced with "data._instance".
|
1321
|
+
*/
|
1322
|
+
function normalizeInstanceDataInJsonLogic(jsonLogic) {
|
1323
|
+
if (typeof jsonLogic === 'object' && jsonLogic !== null) {
|
1324
|
+
// jsonLogic is a Record<string, unknown>
|
1325
|
+
let result = {};
|
1326
|
+
const entries = Object.entries(jsonLogic);
|
1327
|
+
for (const [key, entry] of entries) {
|
1328
|
+
result = {
|
1329
|
+
...result,
|
1330
|
+
[key]: processJsonLogicEntry(entry),
|
1331
|
+
};
|
1332
|
+
}
|
1333
|
+
return result;
|
1334
|
+
}
|
1335
|
+
// jsonLogic is a primitive
|
1336
|
+
return jsonLogic;
|
1337
|
+
}
|
1338
|
+
/**
|
1339
|
+
*
|
1340
|
+
* @param visibility A form's visibility entry.
|
1341
|
+
* @returns The form's visibility entry, converted to formio-readable form.
|
1342
|
+
*/
|
1343
|
+
function convertVisibilityToConditional(visibility) {
|
1344
|
+
if (isObject(visibility) && 'conditions' in visibility && isArray(visibility.conditions)) {
|
1345
|
+
const [condition] = visibility.conditions;
|
1346
|
+
return {
|
1347
|
+
show: condition.operator === 'eq',
|
1348
|
+
when: condition.isInstanceProperty ? `_instance.${condition.property}` : condition.property,
|
1349
|
+
eq: condition.value,
|
1350
|
+
};
|
1351
|
+
}
|
1352
|
+
return visibility
|
1353
|
+
? {
|
1354
|
+
json: normalizeInstanceDataInJsonLogic(visibility),
|
1355
|
+
}
|
1356
|
+
: undefined;
|
1357
|
+
}
|
package/dist/published/components/custom/FormField/AddressFieldComponent/addressFieldComponent.js
CHANGED
@@ -58,7 +58,7 @@ const AddressFieldComponent = (props) => {
|
|
58
58
|
backgroundColor: '#f4f6f8',
|
59
59
|
},
|
60
60
|
}
|
61
|
-
: undefined, required: required, error: error, errorMessage: errorMessage, InputProps: { readOnly: readOnly }, fullWidth: true, size: size ?? 'medium', type: 'text', multiline: property.type === 'string' && !readOnly && isMultiLineText, rows: isMultiLineText ? rows ?? 3 : undefined, value: value, ...(additionalProps ?? {}) })
|
61
|
+
: undefined, required: required, error: error, errorMessage: errorMessage, InputProps: { readOnly: readOnly }, fullWidth: true, size: size ?? 'medium', type: 'text', multiline: property.type === 'string' && !readOnly && isMultiLineText, rows: isMultiLineText ? (rows ?? 3) : undefined, value: value, ...(additionalProps ?? {}) })
|
62
62
|
// Casting to `React.ReactNode` is necessary to resolve TypeScript errors
|
63
63
|
// due to compatibility issues with the outdated `react-input-mask` version
|
64
64
|
// and the newer `@types/react` package.
|
@@ -1,8 +1,9 @@
|
|
1
|
-
import { Property } from '@evoke-platform/context';
|
1
|
+
import { Obj, Property } from '@evoke-platform/context';
|
2
2
|
import React from 'react';
|
3
3
|
type DisplayedPropertyProps = {
|
4
4
|
property: Property;
|
5
5
|
value: unknown;
|
6
|
+
referencedObject?: Obj;
|
6
7
|
};
|
7
8
|
declare const DisplayedProperty: (props: DisplayedPropertyProps) => React.JSX.Element;
|
8
9
|
export default DisplayedProperty;
|
@@ -2,9 +2,10 @@ import { DateTime } from 'luxon';
|
|
2
2
|
import React from 'react';
|
3
3
|
import { CardMedia, Typography } from '../../core';
|
4
4
|
import { Grid } from '../../layout';
|
5
|
+
import { getReadableQuery } from '../CriteriaBuilder';
|
5
6
|
import { RichTextViewer } from '../RichTextViewer';
|
6
7
|
const DisplayedProperty = (props) => {
|
7
|
-
const { property, value } = props;
|
8
|
+
const { property, value, referencedObject } = props;
|
8
9
|
const getAddressAsString = (address) => {
|
9
10
|
let stringAddress = '';
|
10
11
|
if (address?.line1)
|
@@ -22,7 +23,7 @@ const DisplayedProperty = (props) => {
|
|
22
23
|
return stringAddress;
|
23
24
|
};
|
24
25
|
const formatData = (property, value) => {
|
25
|
-
if (property?.objectId) {
|
26
|
+
if (property?.objectId && (property?.type === 'object' || property?.type === 'collection')) {
|
26
27
|
return value?.name ?? value?.id;
|
27
28
|
}
|
28
29
|
switch (property?.type) {
|
@@ -47,6 +48,8 @@ const DisplayedProperty = (props) => {
|
|
47
48
|
return value ? DateTime.fromISO(value).toFormat('MM/dd/yyyy hh:mm a') : undefined;
|
48
49
|
case 'document':
|
49
50
|
return value && Array.isArray(value) ? value.map((v) => v.name).join(', ') : undefined;
|
51
|
+
case 'criteria':
|
52
|
+
return getReadableQuery(value ?? {}, referencedObject?.properties);
|
50
53
|
}
|
51
54
|
return value;
|
52
55
|
};
|
@@ -30,7 +30,7 @@ const styles = {
|
|
30
30
|
},
|
31
31
|
};
|
32
32
|
const HistoricalData = (props) => {
|
33
|
-
const { records, documentHistory, object } = props;
|
33
|
+
const { records, documentHistory, object, referencedObjects } = props;
|
34
34
|
const getPastDocumentVersion = (history) => {
|
35
35
|
const documentVersions = documentHistory?.[history.subject?.id ?? 'unknown'] ?? [];
|
36
36
|
const currentVersion = documentVersions?.map((v) => v.timestamp).indexOf(history.timestamp);
|
@@ -65,11 +65,17 @@ const HistoricalData = (props) => {
|
|
65
65
|
fontWeight: 600,
|
66
66
|
minWidth: 'fit-content',
|
67
67
|
alignSelf: 'flex-start',
|
68
|
+
lineHeight: '17px',
|
69
|
+
padding: '3px 8px',
|
68
70
|
} }, property.name)),
|
69
|
-
React.createElement(DisplayedProperty, { property: property, value: d.historicalValue
|
71
|
+
React.createElement(DisplayedProperty, { property: property, value: d.historicalValue, referencedObject: property.objectId
|
72
|
+
? referencedObjects?.find((o) => o.id === property.objectId)
|
73
|
+
: undefined }),
|
70
74
|
React.createElement(Grid, { item: true, xs: 0.5 },
|
71
75
|
React.createElement(ArrowForward, { sx: { fontSize: '12px' } })),
|
72
|
-
React.createElement(DisplayedProperty, { property: property, value: d.updatedValue
|
76
|
+
React.createElement(DisplayedProperty, { property: property, value: d.updatedValue, referencedObject: property.objectId
|
77
|
+
? referencedObjects?.find((o) => o.id === property.objectId)
|
78
|
+
: undefined }))));
|
73
79
|
}))),
|
74
80
|
['document', 'correspondence'].includes(r.type) && (React.createElement(Box, null,
|
75
81
|
React.createElement(Box, { display: "grid", gridTemplateColumns: 'fit-content(100%) fit-content(2%) fit-content(100%)', alignItems: "center", sx: { overflowWrap: 'break-word' } },
|
@@ -1,6 +1,9 @@
|
|
1
|
+
import { useApiServices } from '@evoke-platform/context';
|
1
2
|
import { Circle } from '@mui/icons-material';
|
3
|
+
import { uniq } from 'lodash';
|
2
4
|
import { DateTime } from 'luxon';
|
3
5
|
import React, { useEffect, useState } from 'react';
|
6
|
+
import { Snackbar } from '../../core';
|
4
7
|
import Typography from '../../core/Typography';
|
5
8
|
import Box from '../../layout/Box';
|
6
9
|
import HistoryFilter from './Filter';
|
@@ -20,9 +23,27 @@ export const HistoryLog = (props) => {
|
|
20
23
|
const { object, history, loading, title } = props;
|
21
24
|
const [historyMap, setHistoryMap] = useState({});
|
22
25
|
const [documentHistory, setDocumentHistory] = useState({});
|
26
|
+
const [referencedObjects, setReferencedObjects] = useState([]);
|
23
27
|
const [filteredHistory, setFilteredHistory] = useState({});
|
24
28
|
const [filter, setFilter] = useState([]);
|
25
29
|
const [order, setOrder] = useState('desc');
|
30
|
+
const [showSnackbar, setShowSnackbar] = useState(false);
|
31
|
+
const apiServices = useApiServices();
|
32
|
+
useEffect(() => {
|
33
|
+
const criteriaProperties = object.properties?.filter((property) => property.type === 'criteria');
|
34
|
+
if (criteriaProperties?.length) {
|
35
|
+
const uniqueObjectIds = uniq(criteriaProperties?.map((property) => property.objectId));
|
36
|
+
Promise.all(uniqueObjectIds.map((objectId) => apiServices.get(`/data/objects/${objectId}/effective`, {
|
37
|
+
params: {
|
38
|
+
filter: {
|
39
|
+
fields: ['id', 'name', 'properties'],
|
40
|
+
},
|
41
|
+
},
|
42
|
+
})))
|
43
|
+
.then((objs) => setReferencedObjects(objs))
|
44
|
+
.catch(() => setShowSnackbar(true));
|
45
|
+
}
|
46
|
+
}, [object, apiServices]);
|
26
47
|
const sortHistoryByTimestamp = (historicalData, order) => {
|
27
48
|
return historicalData.sort((a, b) => order === 'desc' ? b.timestamp.localeCompare(a.timestamp) : a.timestamp.localeCompare(b.timestamp));
|
28
49
|
};
|
@@ -78,13 +99,14 @@ export const HistoryLog = (props) => {
|
|
78
99
|
' ',
|
79
100
|
"\u00A0"),
|
80
101
|
React.createElement(Typography, { sx: { fontWeight: 600, fontSize: '16px', color: '#637381' } }, format(new Date(date + ' 00:00:000'), 'MMM dd, yyyy'))),
|
81
|
-
React.createElement(HistoricalData, { object: object, records: records, documentHistory: documentHistory })));
|
102
|
+
React.createElement(HistoricalData, { object: object, records: records, documentHistory: documentHistory, referencedObjects: referencedObjects })));
|
82
103
|
}
|
83
104
|
return null;
|
84
105
|
}),
|
85
106
|
!loading && filteredHistory && Object.values(filteredHistory).every((v) => !v.length) && (React.createElement(Box, { width: '100%', display: 'grid', justifyContent: 'center', marginTop: '60px' },
|
86
107
|
React.createElement(Typography, { fontSize: '20px', fontWeight: 700 }, "You Have No History"),
|
87
108
|
React.createElement(Typography, { fontSize: '14px', fontWeight: 400 }, "Try modifying the history type."))),
|
88
|
-
loading && React.createElement(HistoryLoading, null)
|
109
|
+
loading && React.createElement(HistoryLoading, null),
|
110
|
+
React.createElement(Snackbar, { open: showSnackbar, handleClose: () => setShowSnackbar(false), message: 'Error occurred when loading referenced objects', error: true })));
|
89
111
|
};
|
90
112
|
export default HistoryLog;
|
@@ -1,5 +1,5 @@
|
|
1
1
|
export { BuilderGrid } from './BuilderGrid';
|
2
|
-
export { CriteriaBuilder } from './CriteriaBuilder';
|
2
|
+
export { CriteriaBuilder, getReadableQuery } from './CriteriaBuilder';
|
3
3
|
export { DataGrid } from './DataGrid';
|
4
4
|
export { ErrorComponent } from './ErrorComponent';
|
5
5
|
export { Form } from './Form';
|
@@ -1,5 +1,5 @@
|
|
1
1
|
export { BuilderGrid } from './BuilderGrid';
|
2
|
-
export { CriteriaBuilder } from './CriteriaBuilder';
|
2
|
+
export { CriteriaBuilder, getReadableQuery } from './CriteriaBuilder';
|
3
3
|
export { DataGrid } from './DataGrid';
|
4
4
|
export { ErrorComponent } from './ErrorComponent';
|
5
5
|
export { Form } from './Form';
|
@@ -2,7 +2,7 @@ export { ClickAwayListener, createTheme, darken, lighten, styled, Toolbar } from
|
|
2
2
|
export { CalendarPicker, DateTimePicker, MonthPicker, PickersDay, StaticDateTimePicker, StaticTimePicker, TimePicker, YearPicker, } from '@mui/x-date-pickers';
|
3
3
|
export * from './colors';
|
4
4
|
export * from './components/core';
|
5
|
-
export { BuilderGrid, CriteriaBuilder, DataGrid, ErrorComponent, Form, FormField, HistoryLog, MenuBar, MultiSelect, RepeatableField, RichTextViewer, UserAvatar, } from './components/custom';
|
5
|
+
export { BuilderGrid, CriteriaBuilder, DataGrid, ErrorComponent, Form, FormField, getReadableQuery, HistoryLog, MenuBar, MultiSelect, RepeatableField, RichTextViewer, UserAvatar, } from './components/custom';
|
6
6
|
export type { FormRef } from './components/custom';
|
7
7
|
export { NumericFormat } from './components/custom/FormField/InputFieldComponent';
|
8
8
|
export { Box, Container, Grid, Stack } from './components/layout';
|
package/dist/published/index.js
CHANGED
@@ -2,7 +2,7 @@ export { ClickAwayListener, createTheme, darken, lighten, styled, Toolbar } from
|
|
2
2
|
export { CalendarPicker, DateTimePicker, MonthPicker, PickersDay, StaticDateTimePicker, StaticTimePicker, TimePicker, YearPicker, } from '@mui/x-date-pickers';
|
3
3
|
export * from './colors';
|
4
4
|
export * from './components/core';
|
5
|
-
export { BuilderGrid, CriteriaBuilder, DataGrid, ErrorComponent, Form, FormField, HistoryLog, MenuBar, MultiSelect, RepeatableField, RichTextViewer, UserAvatar, } from './components/custom';
|
5
|
+
export { BuilderGrid, CriteriaBuilder, DataGrid, ErrorComponent, Form, FormField, getReadableQuery, HistoryLog, MenuBar, MultiSelect, RepeatableField, RichTextViewer, UserAvatar, } from './components/custom';
|
6
6
|
export { NumericFormat } from './components/custom/FormField/InputFieldComponent';
|
7
7
|
export { Box, Container, Grid, Stack } from './components/layout';
|
8
8
|
export * from './theme';
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@evoke-platform/ui-components",
|
3
|
-
"version": "1.4.0-testing.
|
3
|
+
"version": "1.4.0-testing.20",
|
4
4
|
"description": "",
|
5
5
|
"main": "dist/published/index.js",
|
6
6
|
"module": "dist/published/index.js",
|
@@ -84,6 +84,7 @@
|
|
84
84
|
"webpack": "^5.74.0"
|
85
85
|
},
|
86
86
|
"peerDependencies": {
|
87
|
+
"@evoke-platform/context": "^1.1.0-testing.5",
|
87
88
|
"react": "^18.1.0",
|
88
89
|
"react-dom": "^18.1.0"
|
89
90
|
},
|
@@ -94,7 +95,6 @@
|
|
94
95
|
"@dnd-kit/sortable": "^7.0.1",
|
95
96
|
"@emotion/react": "^11.13.5",
|
96
97
|
"@emotion/styled": "^11.8.1",
|
97
|
-
"@evoke-platform/context": "^1.1.0-testing.0",
|
98
98
|
"@formio/react": "^5.2.4-rc.1",
|
99
99
|
"@js-joda/core": "^3.2.0",
|
100
100
|
"@js-joda/locale_en-us": "^3.2.2",
|