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

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.
@@ -1,8 +1,7 @@
1
1
  import { ElementType } from 'react';
2
- import { ObjectProperty, TreeViewObject } from './types';
3
2
  import 'react-querybuilder/dist/query-builder.css';
4
3
  import { EvokeObject } from '../../../types';
5
- import { Operator, PresetValue } from './types';
4
+ import { ObjectProperty, Operator, PresetValue, TreeViewObject } from './types';
6
5
  import { ValueEditorProps } from './ValueEditor';
7
6
  export type CriteriaInputProps = {
8
7
  properties: ObjectProperty[];
@@ -1,7 +1,7 @@
1
1
  import { AddRounded, UnfoldMore } from '@mui/icons-material';
2
2
  import { Typography } from '@mui/material';
3
3
  import { QueryBuilderMaterial } from '@react-querybuilder/material';
4
- import { isEmpty, startCase } from 'lodash';
4
+ import { isArray, isEmpty, startCase } from 'lodash';
5
5
  import React, { useEffect, useMemo, useState } from 'react';
6
6
  import { QueryBuilder, RuleGroupBodyComponents, RuleGroupHeaderComponents, TestID, formatQuery, parseMongoDB, useRuleGroup, } from 'react-querybuilder';
7
7
  import 'react-querybuilder/dist/query-builder.css';
@@ -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))
@@ -159,13 +166,28 @@ const customSelector = (props) => {
159
166
  : option.label,
160
167
  }));
161
168
  }
169
+ else if (inputType === 'integer' || inputType === 'number') {
170
+ opts = options.filter((option) => ['=', '!=', '<', '<=', '>', '>=', 'null', 'notNull'].includes(option.name));
171
+ // checks if it is a single-select property
172
+ }
173
+ else if (inputType === 'string' && isArray(props.fieldData?.values)) {
174
+ opts = options.filter((option) => ['in', 'notIn', '=', '!=', 'notNull', 'null'].includes(option.name));
175
+ }
162
176
  break;
163
177
  case 'Fields':
178
+ placeholder = 'Select Property';
164
179
  width = '33%';
165
180
  val = options.find((option) => option.name === val)?.name;
166
181
  break;
167
182
  }
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) => {
183
+ const handleTreePropertySelect = async (propertyId) => {
184
+ const propertyInfo = await traversePropertyPath(propertyId, object, fetchObject);
185
+ context.setPropertyTreeMap((prev) => {
186
+ return { ...prev, [propertyId]: propertyInfo };
187
+ });
188
+ handleOnChange(propertyId);
189
+ };
190
+ 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
191
  if (typeof option === 'string') {
170
192
  return opts.find((o) => option === o.name)?.label || '';
171
193
  }
@@ -176,7 +198,7 @@ const customSelector = (props) => {
176
198
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
177
199
  onChange: (event, newValue) => {
178
200
  handleOnChange(newValue?.value.name);
179
- }, renderInput: (params) => React.createElement(TextField, { ...params, size: "small" }), sx: { width: width, background: '#fff' }, disableClearable: true, readOnly: readOnly }))));
201
+ }, renderInput: (params) => React.createElement(TextField, { ...params, placeholder: placeholder, size: "small" }), sx: { width: width, background: '#fff' }, disableClearable: true, readOnly: readOnly }))));
180
202
  };
181
203
  const customCombinator = (props) => {
182
204
  const { value, handleOnChange, context, level, path } = props;
@@ -243,10 +265,12 @@ export const valueEditor = (props) => {
243
265
  const CriteriaBuilder = (props) => {
244
266
  const { properties, criteria, setCriteria, originalCriteria, enablePresetValues, presetValues, operators, dynamicContentInput, disabled, disabledCriteria, hideBorder, presetGroupLabel, treeViewOpts, } = props;
245
267
  const [query, setQuery] = useState(undefined);
268
+ const [propertyTreeMap, setPropertyTreeMap] = useState({});
246
269
  useEffect(() => {
247
270
  if (criteria || originalCriteria) {
248
271
  const criteriaToParse = criteria || originalCriteria || {};
249
272
  const updatedQuery = parseMongoDB(criteriaToParse);
273
+ !isEmpty(treeViewOpts) && updatePropertyTreeMap(updatedQuery);
250
274
  setQuery({
251
275
  ...updatedQuery,
252
276
  rules: processRules(updatedQuery.rules),
@@ -276,10 +300,40 @@ const CriteriaBuilder = (props) => {
276
300
  }
277
301
  });
278
302
  }
303
+ // this retrieves the properties from a treeview for each property in the query
304
+ // they are then used in the custom query builder components to determine the input type etc
305
+ const updatePropertyTreeMap = (q) => {
306
+ const ids = [];
307
+ const traverseRulesForIds = (rules) => {
308
+ rules.forEach((rule) => {
309
+ if ('rules' in rule) {
310
+ traverseRulesForIds(rule.rules);
311
+ }
312
+ else {
313
+ ids.push(rule.field);
314
+ }
315
+ });
316
+ };
317
+ traverseRulesForIds(q.rules);
318
+ const tempPropertyMap = { ...propertyTreeMap };
319
+ ids.forEach(async (id) => {
320
+ if (!propertyTreeMap[id] && treeViewOpts?.object && treeViewOpts?.fetchObject) {
321
+ const prop = await traversePropertyPath(id, treeViewOpts?.object, treeViewOpts?.fetchObject);
322
+ if (prop)
323
+ tempPropertyMap[id] = prop;
324
+ }
325
+ setPropertyTreeMap(tempPropertyMap);
326
+ });
327
+ };
279
328
  const handleQueryChange = (q) => {
280
329
  setQuery(q);
281
330
  const newCriteria = JSON.parse(formatQuery(q, 'mongodb'));
282
- if (isEmpty(difference(newCriteria, { $and: [{ $expr: true }] }))) {
331
+ //when q has no rules, it formats and parses to { $and: [{ $expr: true }] }
332
+ const allRulesDeleted = isEmpty(difference(newCriteria, { $and: [{ $expr: true }] }));
333
+ // since the Add Condition / Add Group buttons add rules with all the fields empty,
334
+ // we need to check if the first rule was added because q will still parse to { $and: [{ $expr: true }] }
335
+ const firstRuleAdded = isEmpty(criteria) && q.rules.length > 0;
336
+ if (allRulesDeleted && !firstRuleAdded) {
283
337
  setCriteria(undefined);
284
338
  }
285
339
  else {
@@ -346,6 +400,32 @@ const CriteriaBuilder = (props) => {
346
400
  React.createElement(QueryBuilderMaterial, null,
347
401
  React.createElement(QueryBuilder, { query: !criteria && !originalCriteria ? { combinator: 'and', rules: [] } : query, fields: fields, onQueryChange: (q) => {
348
402
  handleQueryChange(q);
403
+ }, onAddRule: (rule) => {
404
+ // overrides new rule and sets up an empty rule with all three fields empty
405
+ return {
406
+ ...rule,
407
+ field: '',
408
+ operator: '',
409
+ };
410
+ }, onAddGroup: (group) => {
411
+ // overrides new group and sets up a new group with an empty rule with all three fields showing
412
+ const emptyRules = group.rules.map((rule) => ({
413
+ ...rule,
414
+ field: '',
415
+ operator: '',
416
+ }));
417
+ return {
418
+ ...group,
419
+ rules: emptyRules,
420
+ combinator: 'and',
421
+ };
422
+ }, translations: {
423
+ fields: {
424
+ placeholderLabel: '',
425
+ },
426
+ operators: {
427
+ placeholderLabel: '',
428
+ },
349
429
  }, showCombinatorsBetweenRules: true, listsAsArrays: true, disabled: disabled, addRuleToNewGroups: true, controlElements: {
350
430
  combinatorSelector: customCombinator,
351
431
  fieldSelector: customSelector,
@@ -363,6 +443,8 @@ const CriteriaBuilder = (props) => {
363
443
  presetGroupLabel,
364
444
  disabledCriteria,
365
445
  treeViewOpts,
446
+ propertyTreeMap,
447
+ setPropertyTreeMap,
366
448
  }, controlClassnames: {
367
449
  ruleGroup: 'container',
368
450
  }, 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.246",
4
4
  "description": "",
5
5
  "main": "dist/published/index.js",
6
6
  "module": "dist/published/index.js",