@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.
- package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.js +79 -4
- package/dist/published/components/custom/CriteriaBuilder/PropertyTree.d.ts +2 -2
- package/dist/published/components/custom/CriteriaBuilder/PropertyTree.js +2 -2
- package/dist/published/components/custom/CriteriaBuilder/ValueEditor.js +16 -3
- package/dist/published/components/custom/CriteriaBuilder/utils.d.ts +1 -1
- package/dist/published/components/custom/CriteriaBuilder/utils.js +1 -1
- package/dist/published/stories/CriteriaBuilder.stories.d.ts +1 -0
- package/dist/published/stories/CriteriaBuilder.stories.js +139 -3
- package/package.json +1 -1
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
6
|
+
handleTreePropertySelect: (propertyId: string) => void;
|
7
7
|
value: string | undefined;
|
8
8
|
};
|
9
|
-
declare const PropertyTree: ({ fetchObject,
|
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,
|
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
|
-
|
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,
|
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 {
|
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 {
|
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) =>
|
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
|
-
|
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
|
-
'
|
845
|
+
'applicationType.status': 'DNP',
|
710
846
|
},
|
711
847
|
],
|
712
848
|
},
|