-
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 (#69)
Co-authored-by: Robert Aradei <sorin.robert.aradei@ni.com>
- Loading branch information
1 parent
dc17252
commit 99f2fff
Showing
16 changed files
with
853 additions
and
442 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,197 @@ | ||
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, | ||
]; |
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,100 @@ | ||
import { expressionBuilderCallback, expressionReaderCallback, transformComputedFieldsQuery } from "./query-builder.utils" | ||
|
||
describe('QueryBuilderUtils', () => { | ||
describe('transformComputedFieldsQuery', () => { | ||
const computedDataFields = { | ||
Object1: '(object1.prop1 = {value} || object1.prop2 = {value})', | ||
Object2: '(object2.prop1 = {value} || object2.extra.prop2 = {value} || object2.prop3 = {value} || object2.prop4 = {value})', | ||
Object3: '(object3.prop1 = {value} || object3.prop2 = {value} || object3.prop3 = {value})' | ||
}; | ||
|
||
it('should transform a query with computed fields', () => { | ||
const query = 'Object1 = "value1" AND Object2 = "value2"'; | ||
const result = transformComputedFieldsQuery(query, computedDataFields); | ||
expect(result).toBe('(object1.prop1 = value1 || object1.prop2 = value1) AND (object2.prop1 = value2 || object2.extra.prop2 = value2 || object2.prop3 = value2 || object2.prop4 = value2)'); | ||
}); | ||
|
||
it('should return the original query if no computed fields are present', () => { | ||
const query = 'field1 = "value1" AND field2 = "value2"'; | ||
const result = transformComputedFieldsQuery(query, computedDataFields); | ||
expect(result).toBe(query); | ||
}); | ||
|
||
it('should handle multiple computed fields correctly', () => { | ||
const query = 'Object1 = "value1" AND Object3 = "value3"'; | ||
const result = transformComputedFieldsQuery(query, computedDataFields); | ||
expect(result).toBe('(object1.prop1 = value1 || object1.prop2 = value1) AND (object3.prop1 = value3 || object3.prop2 = value3 || object3.prop3 = value3)'); | ||
}); | ||
|
||
it('should handle an empty query', () => { | ||
const query = ''; | ||
const result = transformComputedFieldsQuery(query, computedDataFields); | ||
expect(result).toBe(query); | ||
}); | ||
}); | ||
|
||
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 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,64 @@ | ||
import { QueryBuilderCustomOperation } from "smart-webcomponents-react"; | ||
import { QueryBuilderOption } from "./types"; | ||
|
||
/** | ||
* The function will replace the computed fields with their transformation | ||
* Example: object = "value" => object1.prop1 = "value" || object1.prop2 = "value" | ||
* @param query Query builder provided string | ||
* @param computedDataFields Object with computed fields and their transformations | ||
* @returns Updated query with computed fields transformed | ||
*/ | ||
export function transformComputedFieldsQuery(query: string, computedDataFields: Record<string, string>) { | ||
for (const [field, transformation] of Object.entries(computedDataFields)) { | ||
const regex = new RegExp(`\\b${field}\\s*=\\s*"([^"]*)"`, 'g'); | ||
query = query.replace(regex, (_match, value) => transformation.replace(/{value}/g, value)); | ||
} | ||
|
||
return query; | ||
} | ||
|
||
/** | ||
* 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); | ||
}; | ||
|
||
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[]) { | ||
const fieldOptions = options[fieldName]; | ||
|
||
if (fieldOptions?.length) { | ||
const valueLabel = fieldOptions.find(option => option.value === value); | ||
|
||
if (valueLabel) { | ||
value = valueLabel.label; | ||
} | ||
} | ||
|
||
return { fieldName, value }; | ||
} | ||
}; | ||
|
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
Oops, something went wrong.