@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.
Files changed (30) hide show
  1. package/dist/published/components/core/Alert/Alert.js +1 -1
  2. package/dist/published/components/core/Autocomplete/Autocomplete.js +3 -3
  3. package/dist/published/components/core/DatePicker/DatePicker.js +1 -1
  4. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.js +63 -64
  5. package/dist/published/components/custom/CriteriaBuilder/PropertyTree.js +3 -3
  6. package/dist/published/components/custom/CriteriaBuilder/ValueEditor.js +25 -17
  7. package/dist/published/components/custom/CriteriaBuilder/index.d.ts +2 -1
  8. package/dist/published/components/custom/CriteriaBuilder/index.js +2 -1
  9. package/dist/published/components/custom/CriteriaBuilder/utils.d.ts +13 -0
  10. package/dist/published/components/custom/CriteriaBuilder/utils.js +60 -3
  11. package/dist/published/components/custom/Form/Common/Form.js +19 -7
  12. package/dist/published/components/custom/Form/Common/FormComponentWrapper.js +2 -1
  13. package/dist/published/components/custom/Form/FormComponents/CriteriaComponent/Criteria.js +52 -1
  14. package/dist/published/components/custom/Form/FormComponents/ObjectComponent/ObjectComponent.js +9 -5
  15. package/dist/published/components/custom/Form/FormComponents/ObjectComponent/ObjectPropertyInput.js +3 -2
  16. package/dist/published/components/custom/Form/FormComponents/RepeatableFieldComponent/RepeatableField.js +1 -1
  17. package/dist/published/components/custom/Form/tests/Form.test.js +46 -0
  18. package/dist/published/components/custom/Form/tests/test-data.js +99 -0
  19. package/dist/published/components/custom/Form/utils.js +81 -39
  20. package/dist/published/components/custom/FormField/AddressFieldComponent/addressFieldComponent.js +1 -1
  21. package/dist/published/components/custom/HistoryLog/DisplayedProperty.d.ts +2 -1
  22. package/dist/published/components/custom/HistoryLog/DisplayedProperty.js +5 -2
  23. package/dist/published/components/custom/HistoryLog/HistoryData.d.ts +1 -0
  24. package/dist/published/components/custom/HistoryLog/HistoryData.js +9 -3
  25. package/dist/published/components/custom/HistoryLog/index.js +24 -2
  26. package/dist/published/components/custom/index.d.ts +1 -1
  27. package/dist/published/components/custom/index.js +1 -1
  28. package/dist/published/index.d.ts +1 -1
  29. package/dist/published/index.js +1 -1
  30. 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: (props.popupIcon ?? (props.readOnly || props.disabled)) ? null : React.createElement(ExpandMore, null) }),
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: (props.popupIcon ?? (props.readOnly || props.disabled)) ? null : React.createElement(ExpandMore, null) }),
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 = '33%';
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 ((propertyType === 'array' ||
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
- // this retrieves the properties from a treeview for each property in the query
329
- // they are then used in the custom query builder components to determine the input type etc
330
- const updatePropertyTreeMap = (q) => {
331
- const ids = [];
332
- const traverseRulesForIds = (rules) => {
333
- rules.forEach((rule) => {
334
- if ('rules' in rule) {
335
- traverseRulesForIds(rule.rules);
336
- }
337
- else {
338
- ids.push(rule.field);
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
- traverseRulesForIds(q.rules);
343
- const tempPropertyMap = { ...propertyTreeMap };
344
- ids.forEach(async (id) => {
345
- if (!propertyTreeMap?.[id] && treeViewOpts?.object && treeViewOpts?.fetchObject) {
346
- const prop = await traversePropertyPath(id, treeViewOpts?.object, treeViewOpts?.fetchObject);
347
- if (prop)
348
- tempPropertyMap[id] = prop;
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
- setPropertyTreeMap(tempPropertyMap);
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
- maxWidth: '71vw',
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: '33%',
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 isPresetValueSelected = presetValues && typeof value === 'string' && isPresetValue(value);
46
- const presetDisplayValue = presetValues?.find((option) => option.value.name === value)?.label ?? '';
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(Chip, { label: presetDisplayValue, sx: {
278
- borderRadius: '8px',
279
- fontSize: '14px',
273
+ isPresetValueSelected ? (React.createElement(Box, { ref: inputRef, sx: {
280
274
  width: '33%',
281
- height: '40px',
282
- padding: '0 5px',
275
+ display: 'flex',
283
276
  justifyContent: 'space-between',
284
- }, deleteIcon: React.createElement(ClearRounded, { fontSize: "small" }), onDelete: clearValue })) : (getEditor()),
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
1
  import CriteriaBuilder from './CriteriaBuilder';
2
- export { CriteriaBuilder };
2
+ import { getReadableQuery } from './utils';
3
+ export { CriteriaBuilder, getReadableQuery };
3
4
  export default CriteriaBuilder;
@@ -1,3 +1,4 @@
1
1
  import CriteriaBuilder from './CriteriaBuilder';
2
- export { CriteriaBuilder };
2
+ import { getReadableQuery } from './utils';
3
+ export { CriteriaBuilder, getReadableQuery };
3
4
  export default CriteriaBuilder;
@@ -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: (value.$in || []).join(','),
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: (value.$nin || []).join(','),
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(Typography, { variant: "caption", sx: { ...descriptionStyles, ...(displayOption === 'radioButton' && { display: 'flex' }) } }, description),
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
- setProperties(properties);
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);
@@ -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.attachReact(this.element);
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.attachReact(this.element);
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(this.dataValue) ? null : this.dataValue,
227
+ [this.component.key]: isEmpty(updatedValue) || isNil(updatedValue) || updatedValue.length === 0 ? null : updatedValue,
224
228
  },
225
229
  defaultValueCriteria: this.updatedDefaultValueCriteria,
226
230
  };
@@ -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 ?? selectedInstance?.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: typeof entry.visibility !== 'string' && entry.visibility?.conditions?.length
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: typeof entry.visibility !== 'string' && entry.visibility?.conditions?.length
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: typeof entry.visibility !== 'string' && entry.visibility?.conditions?.length
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.id === associatedObject.propertyId) {
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
+ }
@@ -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
  };
@@ -6,6 +6,7 @@ type HistoryDataProps = {
6
6
  records: History[];
7
7
  documentHistory?: Record<string, History[]>;
8
8
  object: Obj;
9
+ referencedObjects?: Obj[];
9
10
  };
10
11
  declare const HistoricalData: (props: HistoryDataProps) => React.JSX.Element;
11
12
  export default HistoricalData;
@@ -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';
@@ -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.2",
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",