@evoke-platform/ui-components 1.0.0-dev.244 → 1.0.0-dev.245

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.
@@ -10,6 +10,7 @@ import { Autocomplete, Button, IconButton, TextField } from '../../core';
10
10
  import { Box } from '../../layout';
11
11
  import { difference } from '../util';
12
12
  import PropertyTree from './PropertyTree';
13
+ import { traversePropertyPath } from './utils';
13
14
  import ValueEditor from './ValueEditor';
14
15
  const ALL_OPERATORS = [
15
16
  { name: '=', label: 'Is' },
@@ -107,7 +108,11 @@ const customSelector = (props) => {
107
108
  let width = '90px';
108
109
  let val = value;
109
110
  let opts = options;
110
- const inputType = props.fieldData?.inputType;
111
+ let inputType = props.fieldData?.inputType;
112
+ // for tree view / related object properties, the properties are stored in the propertyTreeMap upon selection
113
+ if (!!context.treeViewOpts && !!context.propertyTreeMap[rule.field]) {
114
+ inputType = context.propertyTreeMap[rule.field].type;
115
+ }
111
116
  let readOnly = false;
112
117
  const isTreeViewEnabled = context.treeViewOpts && title === 'Fields';
113
118
  const fetchObject = context.treeViewOpts?.fetchObject;
@@ -120,9 +125,11 @@ const customSelector = (props) => {
120
125
  opts = opts.filter((o) => readOnly || !keys.includes(o.name));
121
126
  }
122
127
  }
128
+ let placeholder = '';
123
129
  switch (title) {
124
130
  case 'Operators':
125
131
  width = '25%';
132
+ placeholder = 'Select Operator';
126
133
  if (inputType === 'array') {
127
134
  opts = options
128
135
  .filter((option) => ['null', 'notNull', 'in', 'notIn'].includes(option.name))
@@ -161,11 +168,19 @@ const customSelector = (props) => {
161
168
  }
162
169
  break;
163
170
  case 'Fields':
171
+ placeholder = 'Select Property';
164
172
  width = '33%';
165
173
  val = options.find((option) => option.name === val)?.name;
166
174
  break;
167
175
  }
168
- return (React.createElement(React.Fragment, null, isTreeViewEnabled ? (React.createElement(PropertyTree, { value: val ?? value, rootObject: object, fetchObject: fetchObject, handleOnChange: handleOnChange })) : (React.createElement(Autocomplete, { options: opts, value: val ?? null, getOptionLabel: (option) => {
176
+ const handleTreePropertySelect = async (propertyId) => {
177
+ const propertyInfo = await traversePropertyPath(propertyId, object, fetchObject);
178
+ context.setPropertyTreeMap((prev) => {
179
+ return { ...prev, [propertyId]: propertyInfo };
180
+ });
181
+ handleOnChange(propertyId);
182
+ };
183
+ return (React.createElement(React.Fragment, null, isTreeViewEnabled ? (React.createElement(PropertyTree, { value: val ?? value, rootObject: object, fetchObject: fetchObject, handleTreePropertySelect: handleTreePropertySelect })) : (React.createElement(Autocomplete, { options: opts, value: val ?? null, getOptionLabel: (option) => {
169
184
  if (typeof option === 'string') {
170
185
  return opts.find((o) => option === o.name)?.label || '';
171
186
  }
@@ -176,7 +191,7 @@ const customSelector = (props) => {
176
191
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
177
192
  onChange: (event, newValue) => {
178
193
  handleOnChange(newValue?.value.name);
179
- }, renderInput: (params) => React.createElement(TextField, { ...params, size: "small" }), sx: { width: width, background: '#fff' }, disableClearable: true, readOnly: readOnly }))));
194
+ }, renderInput: (params) => React.createElement(TextField, { ...params, placeholder: placeholder, size: "small" }), sx: { width: width, background: '#fff' }, disableClearable: true, readOnly: readOnly }))));
180
195
  };
181
196
  const customCombinator = (props) => {
182
197
  const { value, handleOnChange, context, level, path } = props;
@@ -243,10 +258,12 @@ export const valueEditor = (props) => {
243
258
  const CriteriaBuilder = (props) => {
244
259
  const { properties, criteria, setCriteria, originalCriteria, enablePresetValues, presetValues, operators, dynamicContentInput, disabled, disabledCriteria, hideBorder, presetGroupLabel, treeViewOpts, } = props;
245
260
  const [query, setQuery] = useState(undefined);
261
+ const [propertyTreeMap, setPropertyTreeMap] = useState({});
246
262
  useEffect(() => {
247
263
  if (criteria || originalCriteria) {
248
264
  const criteriaToParse = criteria || originalCriteria || {};
249
265
  const updatedQuery = parseMongoDB(criteriaToParse);
266
+ !isEmpty(treeViewOpts) && updatePropertyTreeMap(updatedQuery);
250
267
  setQuery({
251
268
  ...updatedQuery,
252
269
  rules: processRules(updatedQuery.rules),
@@ -276,10 +293,40 @@ const CriteriaBuilder = (props) => {
276
293
  }
277
294
  });
278
295
  }
296
+ // this retrieves the properties from a treeview for each property in the query
297
+ // they are then used in the custom query builder components to determine the input type etc
298
+ const updatePropertyTreeMap = (q) => {
299
+ const ids = [];
300
+ const traverseRulesForIds = (rules) => {
301
+ rules.forEach((rule) => {
302
+ if ('rules' in rule) {
303
+ traverseRulesForIds(rule.rules);
304
+ }
305
+ else {
306
+ ids.push(rule.field);
307
+ }
308
+ });
309
+ };
310
+ traverseRulesForIds(q.rules);
311
+ const tempPropertyMap = { ...propertyTreeMap };
312
+ ids.forEach(async (id) => {
313
+ if (!propertyTreeMap[id] && treeViewOpts?.object && treeViewOpts?.fetchObject) {
314
+ const prop = await traversePropertyPath(id, treeViewOpts?.object, treeViewOpts?.fetchObject);
315
+ if (prop)
316
+ tempPropertyMap[id] = prop;
317
+ }
318
+ setPropertyTreeMap(tempPropertyMap);
319
+ });
320
+ };
279
321
  const handleQueryChange = (q) => {
280
322
  setQuery(q);
281
323
  const newCriteria = JSON.parse(formatQuery(q, 'mongodb'));
282
- if (isEmpty(difference(newCriteria, { $and: [{ $expr: true }] }))) {
324
+ //when q has no rules, it formats and parses to { $and: [{ $expr: true }] }
325
+ const allRulesDeleted = isEmpty(difference(newCriteria, { $and: [{ $expr: true }] }));
326
+ // since the Add Condition / Add Group buttons add rules with all the fields empty,
327
+ // we need to check if the first rule was added because q will still parse to { $and: [{ $expr: true }] }
328
+ const firstRuleAdded = isEmpty(criteria) && q.rules.length > 0;
329
+ if (allRulesDeleted && !firstRuleAdded) {
283
330
  setCriteria(undefined);
284
331
  }
285
332
  else {
@@ -346,6 +393,32 @@ const CriteriaBuilder = (props) => {
346
393
  React.createElement(QueryBuilderMaterial, null,
347
394
  React.createElement(QueryBuilder, { query: !criteria && !originalCriteria ? { combinator: 'and', rules: [] } : query, fields: fields, onQueryChange: (q) => {
348
395
  handleQueryChange(q);
396
+ }, onAddRule: (rule) => {
397
+ // overrides new rule and sets up an empty rule with all three fields empty
398
+ return {
399
+ ...rule,
400
+ field: '',
401
+ operator: '',
402
+ };
403
+ }, onAddGroup: (group) => {
404
+ // overrides new group and sets up a new group with an empty rule with all three fields showing
405
+ const emptyRules = group.rules.map((rule) => ({
406
+ ...rule,
407
+ field: '',
408
+ operator: '',
409
+ }));
410
+ return {
411
+ ...group,
412
+ rules: emptyRules,
413
+ combinator: 'and',
414
+ };
415
+ }, translations: {
416
+ fields: {
417
+ placeholderLabel: '',
418
+ },
419
+ operators: {
420
+ placeholderLabel: '',
421
+ },
349
422
  }, showCombinatorsBetweenRules: true, listsAsArrays: true, disabled: disabled, addRuleToNewGroups: true, controlElements: {
350
423
  combinatorSelector: customCombinator,
351
424
  fieldSelector: customSelector,
@@ -363,6 +436,8 @@ const CriteriaBuilder = (props) => {
363
436
  presetGroupLabel,
364
437
  disabledCriteria,
365
438
  treeViewOpts,
439
+ propertyTreeMap,
440
+ setPropertyTreeMap,
366
441
  }, controlClassnames: {
367
442
  ruleGroup: 'container',
368
443
  }, operators: operators
@@ -3,8 +3,8 @@ import { EvokeObject } from '../../../types';
3
3
  type PropertyTreeProps = {
4
4
  fetchObject: (id: string) => Promise<EvokeObject | undefined>;
5
5
  rootObject: EvokeObject;
6
- handleOnChange: (property: string) => void;
6
+ handleTreePropertySelect: (propertyId: string) => void;
7
7
  value: string | undefined;
8
8
  };
9
- declare const PropertyTree: ({ fetchObject, handleOnChange, rootObject, value }: PropertyTreeProps) => JSX.Element;
9
+ declare const PropertyTree: ({ fetchObject, handleTreePropertySelect, rootObject, value }: PropertyTreeProps) => JSX.Element;
10
10
  export default PropertyTree;
@@ -3,7 +3,7 @@ import React, { useEffect, useState } from 'react';
3
3
  import { Autocomplete, MenuItem, TextField, Tooltip, TreeView } from '../../core';
4
4
  import PropertyTreeItem from './PropertyTreeItem';
5
5
  import { fetchDisplayNamePath, findPropertyById, setIdPaths, truncateNamePath, updateTreeNode } from './utils';
6
- const PropertyTree = ({ fetchObject, handleOnChange, rootObject, value }) => {
6
+ const PropertyTree = ({ fetchObject, handleTreePropertySelect, rootObject, value }) => {
7
7
  const [expandedNodes, setExpandedNodes] = useState([]);
8
8
  const [objectPropertyNamePathMap, setObjectPropertyNamePathMap] = useState({});
9
9
  const [objectProperties, setObjectProperties] = useState(rootObject.properties ?? []);
@@ -99,7 +99,7 @@ const PropertyTree = ({ fetchObject, handleOnChange, rootObject, value }) => {
99
99
  const property = findPropertyById(objectProperties, propertyId);
100
100
  // this prevents the selection of a parent node
101
101
  if (property && !property.children) {
102
- handleOnChange(property.id);
102
+ handleTreePropertySelect(property.id);
103
103
  }
104
104
  };
105
105
  return (React.createElement(Autocomplete, { "aria-label": "Property Selector", value: value, fullWidth: true, sx: {
@@ -17,7 +17,20 @@ const GroupHeader = styled('div')(({ theme }) => ({
17
17
  }));
18
18
  const GroupItems = styled('ul')({ padding: 0 });
19
19
  const ValueEditor = (props) => {
20
- const { handleOnChange, value, values, operator, inputType, context, level, rule } = props;
20
+ const { handleOnChange, value, operator, context, level, rule } = props;
21
+ let inputType = props.inputType;
22
+ let values = props.values;
23
+ const property = context.propertyTreeMap[rule.field];
24
+ // for tree view / related object properties, the properties are stored in the propertyTreeMap upon selection
25
+ if (!!context.treeViewOpts && !!property) {
26
+ inputType = property.type;
27
+ if (property.enum) {
28
+ values = property.enum.map((item) => ({
29
+ name: item,
30
+ label: item,
31
+ }));
32
+ }
33
+ }
21
34
  const inputRef = useRef(null);
22
35
  const [openPresetValues, setOpenPresetValues] = useState(false);
23
36
  const disabled = ['null', 'notNull'].includes(operator);
@@ -79,7 +92,7 @@ const ValueEditor = (props) => {
79
92
  handleOnChange(uniqueSelections.length ? uniqueSelections : '');
80
93
  },
81
94
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
82
- isOptionEqualToValue: (option, value) => option === value, renderInput: (params) => (React.createElement(TextField, { label: params.label, ...params, size: "small" })), groupBy: (option) => isPresetValue(option.value?.name) ? context.presetGroupLabel || 'Preset Values' : 'Options', renderGroup: groupRenderGroup, sx: { width: '33%', background: '#fff' }, readOnly: readOnly }));
95
+ isOptionEqualToValue: (option, value) => option === value, renderInput: (params) => (React.createElement(TextField, { label: params.label, ...params, size: "small", sx: { backgroundColor: '#fff' } })), groupBy: (option) => isPresetValue(option.value?.name) ? context.presetGroupLabel || 'Preset Values' : 'Options', renderGroup: groupRenderGroup, sx: { width: '33%', background: '#fff' }, readOnly: readOnly }));
83
96
  }
84
97
  else {
85
98
  return (React.createElement(TextField, { inputRef: inputRef, value: ['null', 'notNull'].includes(operator) ? '' : value, disabled: ['null', 'notNull'].includes(operator), onChange: (e) => {
@@ -114,7 +127,7 @@ const ValueEditor = (props) => {
114
127
  value = newValue?.name ?? newValue?.value?.name ?? '';
115
128
  }
116
129
  handleOnChange(value);
117
- }, renderInput: (params) => (React.createElement(TextField, { inputRef: inputRef, label: params?.label, ...params, size: "small" })),
130
+ }, renderInput: (params) => (React.createElement(TextField, { inputRef: inputRef, label: params?.label, ...params, size: "small", sx: { backgroundColor: '#fff' } })),
118
131
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
119
132
  getOptionLabel: (option) => {
120
133
  if (typeof option === 'string') {
@@ -39,7 +39,7 @@ export declare const setIdPaths: (properties: ObjectProperty[], parentPath: stri
39
39
  *
40
40
  * @param {string} propertyPath - The dot-separated path of the property to traverse.
41
41
  * @param {Obj} rootObject - The root object from which to start the traversal.
42
- * @param {(objectId: string) => Promise<Obj | undefined>} fetchObject - A function to fetch an object by its ID.
42
+ * @param {FetchObjectFunction} fetchObject - A function to fetch an object by its ID.
43
43
  * @returns {Promise<ObjectProperty | null>} A promise that resolves to an ObjectProperty if found, or null otherwise.
44
44
  */
45
45
  export declare const traversePropertyPath: (propertyPath: string, rootObject: Obj, fetchObject: (objectId: string) => Promise<Obj | undefined>) => Promise<ObjectProperty | null>;
@@ -72,7 +72,7 @@ export const setIdPaths = (properties, parentPath) => {
72
72
  *
73
73
  * @param {string} propertyPath - The dot-separated path of the property to traverse.
74
74
  * @param {Obj} rootObject - The root object from which to start the traversal.
75
- * @param {(objectId: string) => Promise<Obj | undefined>} fetchObject - A function to fetch an object by its ID.
75
+ * @param {FetchObjectFunction} fetchObject - A function to fetch an object by its ID.
76
76
  * @returns {Promise<ObjectProperty | null>} A promise that resolves to an ObjectProperty if found, or null otherwise.
77
77
  */
78
78
  export const traversePropertyPath = async (propertyPath, rootObject, fetchObject) => {
@@ -3,6 +3,7 @@ import { ComponentMeta, ComponentStory } from '@storybook/react';
3
3
  import { CriteriaInputProps } from '../components/custom/CriteriaBuilder/CriteriaBuilder';
4
4
  declare const _default: ComponentMeta<(props: CriteriaInputProps) => JSX.Element>;
5
5
  export default _default;
6
+ export declare const CriteriaBuilderEmpty: ComponentStory<(props: CriteriaInputProps) => JSX.Element>;
6
7
  export declare const CriteriaBuilder: ComponentStory<(props: CriteriaInputProps) => JSX.Element>;
7
8
  export declare const CriteriaBuilderPresetUserID: ComponentStory<(props: CriteriaInputProps) => JSX.Element>;
8
9
  export declare const CriteriaBuilderGroupedPresetValues: ComponentStory<(props: CriteriaInputProps) => JSX.Element>;
@@ -4,7 +4,101 @@ export default {
4
4
  title: 'Input/CriteriaBuilder',
5
5
  component: BuildCriteria,
6
6
  };
7
- const CriteriaBuilderTemplate = (args) => (React.createElement(BuildCriteria, { ...args }));
7
+ const CriteriaBuilderTemplate = (args) => {
8
+ const [criteria, setCriteria] = React.useState(args.criteria);
9
+ args.setCriteria = setCriteria;
10
+ args.criteria = criteria;
11
+ console.log('reset criteria= ', criteria);
12
+ return React.createElement(BuildCriteria, { ...args });
13
+ };
14
+ export const CriteriaBuilderEmpty = CriteriaBuilderTemplate.bind({});
15
+ CriteriaBuilderEmpty.args = {
16
+ properties: [
17
+ {
18
+ id: 'name',
19
+ name: 'name',
20
+ type: 'string',
21
+ required: true,
22
+ },
23
+ {
24
+ id: 'licenseNumber',
25
+ name: 'license number',
26
+ type: 'string',
27
+ required: true,
28
+ },
29
+ {
30
+ id: 'issueDate',
31
+ name: 'issue date',
32
+ type: 'date',
33
+ required: false,
34
+ objectId: '',
35
+ },
36
+ {
37
+ id: 'status',
38
+ name: 'status',
39
+ type: 'string',
40
+ enum: ['active', 'expired', 'pending approval'],
41
+ required: false,
42
+ objectId: '',
43
+ },
44
+ {
45
+ id: 'licenseeName',
46
+ name: 'Licensee Name',
47
+ type: 'string',
48
+ required: false,
49
+ formula: '{{person.firstName}} {{person.middleName}} {{person.lastName}}',
50
+ },
51
+ {
52
+ id: 'calculated',
53
+ name: 'calculated',
54
+ type: 'string',
55
+ required: false,
56
+ objectId: '',
57
+ formula: '{{status}} - {{licensee.firstName}}',
58
+ },
59
+ {
60
+ id: 'application',
61
+ name: 'applicant',
62
+ type: 'object',
63
+ required: false,
64
+ objectId: 'application',
65
+ },
66
+ {
67
+ id: 'applicantName',
68
+ name: 'Application Info',
69
+ type: 'string',
70
+ required: false,
71
+ formula: '{{application.name}} - {{status}}',
72
+ },
73
+ {
74
+ id: 'licensedFor',
75
+ name: 'licensed for',
76
+ type: 'array',
77
+ enum: [
78
+ 'amusements',
79
+ 'food and beverage',
80
+ 'entertainment',
81
+ 'transportation',
82
+ 'hot dogs',
83
+ 'swimming pools',
84
+ 'ferris wheels',
85
+ 'bungee jumping',
86
+ ],
87
+ required: true,
88
+ searchable: false,
89
+ },
90
+ {
91
+ id: 'inState',
92
+ name: 'In State',
93
+ type: 'string',
94
+ enum: ['Yes', 'No'],
95
+ required: false,
96
+ searchable: false,
97
+ },
98
+ ],
99
+ criteria: {},
100
+ setCriteria: (criteria) => console.log('criteria= ', criteria),
101
+ };
8
102
  export const CriteriaBuilder = CriteriaBuilderTemplate.bind({});
9
103
  CriteriaBuilder.args = {
10
104
  properties: [
@@ -418,6 +512,13 @@ CriteriaBuilderRelatedObject.args = {
418
512
  type: 'date',
419
513
  required: false,
420
514
  },
515
+ {
516
+ id: 'approvalStatus',
517
+ name: 'Approval Status',
518
+ type: 'string',
519
+ enum: ['Approved', 'Denied', 'InProgress', 'Received'],
520
+ required: false,
521
+ },
421
522
  {
422
523
  id: 'licenseNumber',
423
524
  name: 'LicenseNumber',
@@ -464,6 +565,13 @@ CriteriaBuilderRelatedObject.args = {
464
565
  type: 'string',
465
566
  required: true,
466
567
  },
568
+ {
569
+ id: 'status',
570
+ name: 'Status',
571
+ type: 'string',
572
+ enum: ['RN', 'NP', 'DNP', 'MA'],
573
+ required: false,
574
+ },
467
575
  {
468
576
  id: 'description',
469
577
  name: 'Description',
@@ -703,10 +811,38 @@ CriteriaBuilderRelatedObject.args = {
703
811
  criteria: {
704
812
  $and: [
705
813
  {
706
- name: 'bert',
814
+ 'applicant.vehicleCount': {
815
+ $gt: 2,
816
+ },
817
+ },
818
+ {
819
+ 'applicant.license.expirationDate': {
820
+ $gt: '2025-01-16',
821
+ },
822
+ },
823
+ {
824
+ 'applicant.license.person.phone': {
825
+ $regex: '410',
826
+ },
827
+ },
828
+ {
829
+ $or: [
830
+ {
831
+ 'applicant.license.approvalStatus': 'InProgress',
832
+ },
833
+ {
834
+ 'applicant.license.approvalStatus': 'Approved',
835
+ },
836
+ {
837
+ 'applicant.license.issueDate': '2025-01-09',
838
+ },
839
+ {
840
+ 'applicant.avgVehicleRating': 1,
841
+ },
842
+ ],
707
843
  },
708
844
  {
709
- 'applicant.lastName': '',
845
+ 'applicationType.status': 'DNP',
710
846
  },
711
847
  ],
712
848
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evoke-platform/ui-components",
3
- "version": "1.0.0-dev.244",
3
+ "version": "1.0.0-dev.245",
4
4
  "description": "",
5
5
  "main": "dist/published/index.js",
6
6
  "module": "dist/published/index.js",