-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(asset): Added calibration forecast query builder
- Loading branch information
Robert Aradei
committed
Sep 26, 2024
1 parent
162d782
commit 41d7f38
Showing
15 changed files
with
884 additions
and
439 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,201 @@ | ||
import { QueryBuilderCustomOperation } from "smart-webcomponents-react"; | ||
|
||
export const queryBuilderMessages = { | ||
en: { | ||
propertyUnknownType: "'' property is with undefined 'type' member!", | ||
propertyInvalidValue: "Invalid '!", | ||
propertyInvalidValueType: "Invalid '!", | ||
elementNotInDOM: 'Element does not exist in DOM! Please, add the element to the DOM, before invoking a method.', | ||
moduleUndefined: 'Module is undefined.', | ||
missingReference: '.', | ||
htmlTemplateNotSuported: ": Browser doesn't support HTMLTemplate elements.", | ||
invalidTemplate: "' property accepts a string that must match the id of an HTMLTemplate element from the DOM.", | ||
add: 'Add', | ||
addCondition: 'Add Condition', | ||
addGroup: 'Add Group', | ||
and: 'And', | ||
notand: 'Not And', | ||
or: 'Or', | ||
notor: 'Not Or', | ||
'=': 'Equals', | ||
'<>': 'Does not equal', | ||
'>': 'Greater than', | ||
'>=': 'Greater than or equal to', | ||
'<': 'Less than', | ||
'<=': 'Less than or equal to', | ||
startswith: 'Starts with', | ||
endswith: 'Ends with', | ||
contains: 'Contains', | ||
notcontains: 'Does not contain', | ||
isblank: 'Is blank', | ||
isnotblank: 'Is not blank', | ||
wrongParentGroupIndex: "' method.", | ||
missingFields: | ||
': Fields are required for proper condition\'s adding. Set "fields" source and then conditions will be added as expected.', | ||
wrongElementNode: "' method.", | ||
invalidDataStructure: ': Used invalid data structure in updateCondition/updateGroup method.', | ||
dateTabLabel: 'DATE', | ||
timeTabLabel: 'TIME', | ||
queryLabel: '', | ||
}, | ||
}; | ||
|
||
export const QueryBuilderOperations = { | ||
EQUALS: { | ||
label: 'Equals', | ||
name: '=', | ||
expressionTemplate: '{0} = "{1}"', | ||
}, | ||
DOES_NOT_EQUAL: { | ||
label: 'Does not equal', | ||
name: '<>', | ||
expressionTemplate: '{0} != "{1}"', | ||
}, | ||
STARTS_WITH: { | ||
label: 'Starts with', | ||
name: 'startswith', | ||
expressionTemplate: '{0}.StartsWith("{1}")', | ||
}, | ||
ENDS_WITH: { | ||
label: 'Ends with', | ||
name: 'endswith', | ||
expressionTemplate: '{0}.EndsWith("{1}")', | ||
}, | ||
CONTAINS: { | ||
label: 'Contains', | ||
name: 'contains', | ||
expressionTemplate: '{0}.Contains("{1}")', | ||
}, | ||
DOES_NOT_CONTAIN: { | ||
label: 'Does not contain', | ||
name: 'notcontains', | ||
expressionTemplate: '!({0}.Contains("{1}"))', | ||
}, | ||
IS_BLANK: { | ||
label: 'Is blank', | ||
name: 'isblank', | ||
expressionTemplate: 'string.IsNullOrEmpty({0})', | ||
hideValue: true, | ||
}, | ||
IS_NOT_BLANK: { | ||
label: 'Is not blank', | ||
name: 'isnotblank', | ||
expressionTemplate: '!string.IsNullOrEmpty({0})', | ||
hideValue: true, | ||
}, | ||
GREATER_THAN: { | ||
label: 'Greater than', | ||
name: '>', | ||
expressionTemplate: '{0} > "{1}"', | ||
}, | ||
GREATER_THAN_OR_EQUAL_TO: { | ||
label: 'Greater than or equal to', | ||
name: '>=', | ||
expressionTemplate: '{0} >= "{1}"', | ||
}, | ||
LESS_THAN: { | ||
label: 'Less than', | ||
name: '<', | ||
expressionTemplate: '{0} < "{1}"', | ||
}, | ||
LESS_THAN_OR_EQUAL_TO: { | ||
label: 'Less than or equal to', | ||
name: '<=', | ||
expressionTemplate: '{0} <= "{1}"', | ||
}, | ||
// List expressions | ||
LIST_EQUALS: { | ||
label: 'Equals', | ||
name: 'listequals', | ||
expressionTemplate: '{0}.Contains("{1}")', | ||
}, | ||
LIST_DOES_NOT_EQUAL: { | ||
label: 'Does not equal', | ||
name: 'listnotequals', | ||
expressionTemplate: '!({0}.Contains("{1}"))', | ||
}, | ||
LIST_CONTAINS: { | ||
label: 'Contains', | ||
name: 'listcontains', | ||
expressionTemplate: '{0}.Any(it.Contains("{1}"))', | ||
}, | ||
LIST_DOES_NOT_CONTAIN: { | ||
label: 'Does not contain', | ||
name: 'listnotcontains', | ||
expressionTemplate: '{0}.Any(!it.Contains("{1}"))', | ||
}, | ||
// Properties expressions | ||
PROPERTY_EQUALS: { | ||
label: 'Equals', | ||
name: 'propertyequals', | ||
expressionTemplate: 'properties["{0}"] = "{1}"', | ||
}, | ||
PROPERTY_DOES_NOT_EQUAL: { | ||
label: 'Does not equal', | ||
name: 'propertynotequals', | ||
expressionTemplate: 'properties["{0}"] != "{1}"', | ||
}, | ||
PROPERTY_STARTS_WITH: { | ||
label: 'Starts with', | ||
name: 'propertystartswith', | ||
expressionTemplate: 'properties["{0}"].StartsWith("{1}")', | ||
}, | ||
PROPERTY_ENDS_WITH: { | ||
label: 'Ends with', | ||
name: 'propertyendswith', | ||
expressionTemplate: 'properties["{0}"].EndsWith("{1}")', | ||
}, | ||
PROPERTY_CONTAINS: { | ||
label: 'Contains', | ||
name: 'propertycontains', | ||
expressionTemplate: 'properties["{0}"].Contains("{1}")', | ||
}, | ||
PROPERTY_DOES_NOT_CONTAIN: { | ||
label: 'Does not contains', | ||
name: 'propertynotcontains', | ||
expressionTemplate: '!(properties["{0}"].Contains("{1}"))', | ||
}, | ||
PROPERTY_IS_BLANK: { | ||
label: 'Is blank', | ||
name: 'propertyisblank', | ||
expressionTemplate: 'string.IsNullOrEmpty(properties["{0}"])', | ||
hideValue: true, | ||
}, | ||
PROPERTY_IS_NOT_BLANK: { | ||
label: 'Is not blank', | ||
name: 'propertyisnotblank', | ||
expressionTemplate: '!string.IsNullOrEmpty(properties["{0}"])', | ||
hideValue: true, | ||
}, | ||
} | ||
|
||
export const customOperations: QueryBuilderCustomOperation[] = [ | ||
QueryBuilderOperations.EQUALS, | ||
QueryBuilderOperations.DOES_NOT_EQUAL, | ||
QueryBuilderOperations.STARTS_WITH, | ||
QueryBuilderOperations.ENDS_WITH, | ||
QueryBuilderOperations.CONTAINS, | ||
QueryBuilderOperations.DOES_NOT_CONTAIN, | ||
QueryBuilderOperations.IS_BLANK, | ||
QueryBuilderOperations.IS_NOT_BLANK, | ||
QueryBuilderOperations.GREATER_THAN, | ||
QueryBuilderOperations.GREATER_THAN_OR_EQUAL_TO, | ||
QueryBuilderOperations.LESS_THAN, | ||
QueryBuilderOperations.LESS_THAN_OR_EQUAL_TO, | ||
QueryBuilderOperations.LIST_EQUALS, | ||
QueryBuilderOperations.LIST_DOES_NOT_EQUAL, | ||
QueryBuilderOperations.LIST_CONTAINS, | ||
QueryBuilderOperations.LIST_DOES_NOT_CONTAIN, | ||
QueryBuilderOperations.PROPERTY_EQUALS, | ||
QueryBuilderOperations.PROPERTY_DOES_NOT_EQUAL, | ||
QueryBuilderOperations.PROPERTY_STARTS_WITH, | ||
QueryBuilderOperations.PROPERTY_ENDS_WITH, | ||
QueryBuilderOperations.PROPERTY_CONTAINS, | ||
QueryBuilderOperations.PROPERTY_DOES_NOT_CONTAIN, | ||
QueryBuilderOperations.PROPERTY_IS_BLANK, | ||
QueryBuilderOperations.PROPERTY_IS_NOT_BLANK, | ||
]; | ||
|
||
export enum ComputedDataFields { | ||
Location = 'Location.MinionId || Location.PhysicalLocation' | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
import { expressionBuilderCallback, expressionReaderCallback, setQueryComputedFields } from "./query-builder.utils" | ||
|
||
jest.mock('./query-builder.constants.ts', () => ({ | ||
...jest.requireActual('./query-builder.constants'), | ||
ComputedDataFields: { | ||
Object1: 'object1.prop1 || object1.prop2', | ||
Object2: 'object2.prop1 || object2.extra.prop2 || object2.prop3 || object2.prop4 || object2.prop5 || object2.prop6', | ||
Object3: 'object3.prop1 || object3.prop2 || object3.prop3' | ||
} | ||
})); | ||
|
||
describe('QueryBuilderUtils', () => { | ||
describe('setQueryComputedFields', () => { | ||
it('should not modify the query if there are no computed fields', () => { | ||
const query = 'field1 = "value1" AND field2 = "value2"'; | ||
const result = setQueryComputedFields(query); | ||
expect(result).toBe(query); | ||
}); | ||
|
||
it('should keep only first query of a computed field', () => { | ||
const query1 = 'object1.prop1 = "value" || object1.prop2 = "value"'; | ||
const query2 = 'object2.prop1 = "value" || object2.extra.prop2 = "value" || object2.prop3 = "value" || object2.prop4 = "value" || object2.prop5 = "value" || object2.prop6 = "value"'; | ||
const query3 = 'object3.prop1 = "value" || object3.prop2 = "value" || object3.prop3 = "value"'; | ||
|
||
expect(setQueryComputedFields(query1)).toBe('object1.prop1 = "value"'); | ||
expect(setQueryComputedFields(query2)).toBe('object2.prop1 = "value"'); | ||
expect(setQueryComputedFields(query3)).toBe('object3.prop1 = "value"'); | ||
|
||
}); | ||
|
||
it('should keep the right query in computed queries', () => { | ||
const query4 = 'object4.prop1 = "val" && (object1.prop1 = "value" || object1.prop2 = "value")'; | ||
const query5 = '(object1.prop1 = "value" || object1.prop2 = "value") && object5.prop1 = "val" || object3.prop1 = "value" || object3.prop2 = "value" || object3.prop3 = "value"'; | ||
|
||
expect(setQueryComputedFields(query4)).toBe('object4.prop1 = "val" && (object1.prop1 = "value")'); | ||
expect(setQueryComputedFields(query5)).toBe('(object1.prop1 = "value") && object5.prop1 = "val" || object3.prop1 = "value"'); | ||
}); | ||
|
||
it('should return undefined if the query is undefined', () => { | ||
const result = setQueryComputedFields(undefined); | ||
expect(result).toBeUndefined(); | ||
}); | ||
}) | ||
|
||
describe('expressionBuilderCallback', () => { | ||
const mockQueryBuilderCustomOperation = { | ||
expressionTemplate: '{0} = {1}' | ||
}; | ||
|
||
it('should build a valid expression for a single field', () => { | ||
const options = { | ||
'field1': [{ label: 'Option A', value: 'ValueA' }], | ||
}; | ||
|
||
const result = expressionBuilderCallback(options).call(mockQueryBuilderCustomOperation, 'field1', 'someOperation', 'Option A'); | ||
|
||
expect(result).toBe('field1 = ValueA'); | ||
}); | ||
|
||
it('should return original value if no matching label found', () => { | ||
const options = { | ||
'field1': [{ label: 'Option A', value: 'ValueA' }], | ||
}; | ||
|
||
const callback = expressionBuilderCallback(options).bind(mockQueryBuilderCustomOperation); | ||
const result = callback('field1', 'someOperation', 'Option B'); | ||
|
||
expect(result).toBe('field1 = Option B'); | ||
}); | ||
|
||
it('should return original expression if no options are provided', () => { | ||
const options = {}; | ||
|
||
const callback = expressionBuilderCallback(options).bind(mockQueryBuilderCustomOperation); | ||
const result = callback('field1', 'someOperation', 'Any Value'); | ||
|
||
expect(result).toBe('field1 = Any Value'); | ||
}); | ||
}) | ||
|
||
describe('expressionReaderCallback', () => { | ||
const options = { | ||
'optionsObject1': [{ label: 'Label A', value: 'ValueA' }], | ||
'optionsObject2': [{ label: 'Label B', value: 'ValueB' }], | ||
}; | ||
|
||
it('should map value to label for a given field', () => { | ||
const callback = expressionReaderCallback(options); | ||
const result = callback('someExpression', ['optionsObject1', 'ValueA']); | ||
|
||
expect(result).toEqual({ fieldName: 'optionsObject1', value: 'Label A' }); | ||
}); | ||
|
||
it('should replace field name with computed field name if applicable', () => { | ||
const callback = expressionReaderCallback(options); | ||
const result = callback('someExpression', ['object1.prop1', 'ValueA']); | ||
|
||
expect(result).toEqual({ fieldName: 'object1.prop1 || object1.prop2', value: 'ValueA' }); | ||
}); | ||
|
||
it('should return original field name and value if no matching label is found', () => { | ||
const callback = expressionReaderCallback(options); | ||
const result = callback('someExpression', ['field1', 'NonExistentValue']); | ||
|
||
expect(result).toEqual({ fieldName: 'field1', value: 'NonExistentValue' }); | ||
}); | ||
|
||
it('should return original field name and value if no options are provided for the field', () => { | ||
const emptyOptions = {}; | ||
const callback = expressionReaderCallback(emptyOptions); | ||
const result = callback('someExpression', ['field1', 'ValueA']); | ||
|
||
expect(result).toEqual({ fieldName: 'field1', value: 'ValueA' }); | ||
}); | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
import { QueryBuilderCustomOperation } from "smart-webcomponents-react"; | ||
import { QueryBuilderOption } from "./types"; | ||
import { ComputedDataFields } from "./query-builder.constants"; | ||
|
||
/** | ||
* Removes extra queries from computed fields so the query builder will read only one | ||
* Ex: Location.MinionId = "value" || Location.PhysicalLocation = "value" will become Location.MinionId = "value" | ||
* @param query actual query that has been sent | ||
* @returns query that the query builder can read | ||
*/ | ||
export function setQueryComputedFields(query?: string) { | ||
const propertiesToExclude = Object.values(ComputedDataFields) | ||
.flatMap(field => field.split(' || ').slice(1)) | ||
.join('|'); | ||
|
||
const operations = '(=|!=)'; // Update if additional operations need support | ||
const computedDataFieldsRegex = new RegExp(`\\s\\|\\| (${propertiesToExclude}) ${operations} *"[^"]*"`, "g"); | ||
|
||
return query?.replace(computedDataFieldsRegex, ''); | ||
} | ||
|
||
/** | ||
* The callback will replace the option's label with it's value | ||
* @param options Object with value, label object properties, that hold the dropdown values | ||
* @returns callback to be used by query builder when building the query | ||
*/ | ||
export function expressionBuilderCallback(options: Record<string, QueryBuilderOption[]>) { | ||
return function (this: QueryBuilderCustomOperation, fieldName: string, _operation: string, value: string) { | ||
const buildExpression = (field: string, value: string) => { | ||
const fieldOptions = options[fieldName]; | ||
if (fieldOptions?.length) { | ||
const labelValue = fieldOptions.find(option => option.label === value); | ||
|
||
if (labelValue) { | ||
value = labelValue.value; | ||
} | ||
} | ||
|
||
return this.expressionTemplate?.replace('{0}', field).replace('{1}', value); | ||
}; | ||
|
||
if (fieldName.includes('||')) { | ||
return `(${fieldName.split(' || ').map(prop => buildExpression(prop, value)).join(' || ')})`; | ||
} | ||
|
||
return buildExpression(fieldName, value); | ||
}; | ||
} | ||
|
||
/** | ||
* The callback will replace the option's value with it's label | ||
* @param options Object with value, label object properties, that hold the dropdown values | ||
* @returns callback to be used by query builder when reading the query | ||
*/ | ||
export function expressionReaderCallback(options: Record<string, QueryBuilderOption[]>) { | ||
return function (_expression: string, [fieldName, value]: string[]) { | ||
for (const computedFieldName of Object.values(ComputedDataFields)) { | ||
if (computedFieldName.includes(fieldName)) { | ||
fieldName = computedFieldName; | ||
break; | ||
} | ||
} | ||
|
||
const fieldOptions = options[fieldName]; | ||
|
||
if (fieldOptions?.length) { | ||
const valueLabel = fieldOptions.find(option => option.value === value); | ||
|
||
if (valueLabel) { | ||
value = valueLabel.label; | ||
} | ||
} | ||
|
||
return { fieldName, value }; | ||
} | ||
}; | ||
|
Oops, something went wrong.