diff --git a/web/client/components/data/query/QueryToolbar.jsx b/web/client/components/data/query/QueryToolbar.jsx index 6d441f521b..50d5b790bf 100644 --- a/web/client/components/data/query/QueryToolbar.jsx +++ b/web/client/components/data/query/QueryToolbar.jsx @@ -124,9 +124,8 @@ class QueryToolbar extends React.Component { let fieldsExceptions = this.props.filterFields.filter((field) => field.exception).length > 0; // option allowEmptyFilter available only for the base toolbar, not advanced TODO: externalize this behaviour const allowEmpty = (this.props.allowEmptyFilter && !this.props.advancedToolbar); - // this flag checks if there is any valid attribute fields (with value) - let hasValidAttributeFields = this.props.filterFields.filter((field) => field.value || field.value === 0).length > 0; + let hasValidAttributeFields = this.props.filterFields.filter((field) => checkOperatorValidity(field.value, field.operator)).length > 0; const isCurrentFilterEmpty = isFilterEmpty(this.props); const isAppliedFilterEmpty = isFilterEmpty(this.props.appliedFilter); diff --git a/web/client/utils/FilterUtils.js b/web/client/utils/FilterUtils.js index a1c22dfc8a..25626abe8f 100644 --- a/web/client/utils/FilterUtils.js +++ b/web/client/utils/FilterUtils.js @@ -29,7 +29,7 @@ export const cqlToOgc = (cqlFilter, fOpts) => { return toFilter(read(cqlFilter)); }; -import { get, isNil, isUndefined, isArray, find, findIndex, isString, flatten } from 'lodash'; +import { get, isNil, isArray, find, findIndex, isString, flatten } from 'lodash'; let FilterUtils; const wrapValueWithWildcard = (value, condition) => { @@ -43,7 +43,7 @@ export const wrapIfNoWildcards = (value) => { export const escapeCQLStrings = str => str && str.replace ? str.replace(/\'/g, "''") : str; export const checkOperatorValidity = (value, operator) => { - return (!isNil(value) && operator !== "isNull" || !isUndefined(value) && operator === "isNull"); + return ( operator === "isNull" || !isNil(value) ); }; /** * Test if crossLayer filter is valid. @@ -960,9 +960,7 @@ export const cqlStringField = function(attribute, operator, value) { const wrappedAttr = wrapAttributeWithDoubleQuotes(attribute); if (!isNil(value)) { const processedValue = processCqlWildcards(value, operator); - if (operator === "isNull") { - fieldFilter = "isNull(" + wrappedAttr + ")=true"; - } else if (["<>", "="].includes(operator)) { + if (["<>", "="].includes(operator)) { fieldFilter = wrappedAttr + operator + processedValue; } else if (operator === "ilike") { fieldFilter = "strToLowerCase(" + wrappedAttr + ") LIKE " + processedValue; @@ -1029,30 +1027,34 @@ export const processCQLFilterFields = function(group, objFilter) { if (fields) { fields.forEach((field) => { let fieldFilter; - - switch (field.type) { - case "date": - case "time": - case "date-time": - fieldFilter = FilterUtils.cqlDateField(field.attribute, field.operator, field.value); - break; - case "number": - fieldFilter = FilterUtils.cqlNumberField(field.attribute, field.operator, field.value); - break; - case "string": - fieldFilter = FilterUtils.cqlStringField(field.attribute, field.operator, field.value); - break; - case "boolean": - fieldFilter = FilterUtils.cqlBooleanField(field.attribute, field.operator, field.value); - break; - case "list": - fieldFilter = FilterUtils.cqlListField(field.attribute, field.operator, field.value); - break; - case "array": - fieldFilter = FilterUtils.cqlArrayField(field.attribute, field.operator, field.value); - break; - default: - break; + if (field.operator === "isNull") { + const wrappedAttr = wrapAttributeWithDoubleQuotes(field.attribute); + fieldFilter = "isNull(" + wrappedAttr + ")=true"; + } else { + switch (field.type) { + case "date": + case "time": + case "date-time": + fieldFilter = FilterUtils.cqlDateField(field.attribute, field.operator, field.value); + break; + case "number": + fieldFilter = FilterUtils.cqlNumberField(field.attribute, field.operator, field.value); + break; + case "string": + fieldFilter = FilterUtils.cqlStringField(field.attribute, field.operator, field.value); + break; + case "boolean": + fieldFilter = FilterUtils.cqlBooleanField(field.attribute, field.operator, field.value); + break; + case "list": + fieldFilter = FilterUtils.cqlListField(field.attribute, field.operator, field.value); + break; + case "array": + fieldFilter = FilterUtils.cqlArrayField(field.attribute, field.operator, field.value); + break; + default: + break; + } } if (fieldFilter) { filter.push(group.negateAll ? 'NOT (' + fieldFilter + ')' : fieldFilter); @@ -1139,7 +1141,7 @@ export const getWFSFilterData = (filterObj, options) => { }; export const isLikeOrIlike = (operator) => operator === "ilike" || operator === "like"; export const isFilterEmpty = ({ filterFields = [], spatialField = {}, crossLayerFilter = {}, filters = [] } = {}) => - !(filterFields.filter((field) => field.value || field.value === 0).length > 0) + !(filterFields.filter((field) => field.value || field.value === 0 || field.operator === "isNull").length > 0) && !spatialField.geometry && !(crossLayerFilter && crossLayerFilter.attribute && crossLayerFilter.operation) && !(filters && filters.length > 0); diff --git a/web/client/utils/__tests__/FilterUtils-test.js b/web/client/utils/__tests__/FilterUtils-test.js index d17ef83f74..9f9dd1df27 100644 --- a/web/client/utils/__tests__/FilterUtils-test.js +++ b/web/client/utils/__tests__/FilterUtils-test.js @@ -27,7 +27,8 @@ import { wrapIfNoWildcards, mergeFiltersToOGC, convertFiltersToOGC, - convertFiltersToCQL + convertFiltersToCQL, + isFilterEmpty } from '../FilterUtils'; @@ -577,49 +578,57 @@ describe('FilterUtils', () => { expect(filter).toEqual(expected); }); it('Test checkOperatorValidity', () => { - let filterObj = { - filterFields: [{ - attribute: "attributeNull", - groupId: 1, - exception: null, - operator: "=", - rowId: "1", - type: "string", - value: null - }, { - attribute: "attributeUndefined", - groupId: 1, - exception: null, - operator: "=", - rowId: "2", - type: "string", - value: undefined - }, { - attribute: "attributeNull2", - groupId: 1, - exception: null, - operator: "isNull", - rowId: "3", - type: "string", - value: undefined - }, { - attribute: "attributeUndefined2", - groupId: 1, - exception: null, - operator: "isNull", - rowId: "4", - type: "string", - value: null // valid value for isnull operator - }] - }; - - filterObj.filterFields.forEach((f, i) => { - let valid = checkOperatorValidity(f.value, f.operator); - if (i <= 2) { - expect(valid).toEqual(false); - } else { - expect(valid).toEqual(true); - } + const validFilterFields = [{ + attribute: "operatorIsEqaual", + groupId: 1, + exception: null, + operator: "=", + rowId: "1", + type: "string", + value: "value" + }, { + attribute: "attributeNull2", + groupId: 1, + exception: null, + operator: "isNull", + rowId: "3", + type: "string", + value: undefined + }, { + attribute: "attributeUndefined2", + groupId: 1, + exception: null, + operator: "isNull", + rowId: "4", + type: "string", + value: null // valid value for isnull operator + }]; + validFilterFields.forEach((f, i) => { + expect(checkOperatorValidity(f.value, f.operator)).toBe(true, `Failed on ${i}` ); + }); + const invalidFilterFields = [{ + attribute: "attributeNull", + groupId: 1, + exception: null, + operator: "=", + rowId: "1", + type: "string", + value: null + }, { + attribute: "attributeUndefined", + groupId: 1, + exception: null, + operator: "=", + rowId: "2", + type: "string", + value: undefined + }, { + attribute: "attributeUndefined", + groupId: 1, + exception: null + }]; + invalidFilterFields.forEach((f, i) => { + expect(checkOperatorValidity(f.value, f.operator)).toBe(false, `Failed on ${i}` ); }); }); it('getGetFeatureBase gets viewParams', () => { @@ -811,7 +820,24 @@ describe('FilterUtils', () => { value: "isNull" }] }; - let expected = 'attributeNullattributeValid'; + let expected = '' + + '' + + '' + + 'attributeNull' + + 'attributeUndefined' + + 'attributeValid' + + '' + + '' + + ''; let filter = toOGCFilter("ft_name_test", filterObj); expect(filter).toEqual(expected); }); @@ -1196,8 +1222,6 @@ describe('FilterUtils', () => { expect(cqlStringField("attribute_1", "=", "PRE'")).toBe("\"attribute_1\"='PRE'''"); // test <> expect(cqlStringField("attribute_1", "<>", "Alabama")).toBe("\"attribute_1\"<>'Alabama'"); - // test isNull - expect(cqlStringField("attribute_1", "isNull", "")).toBe("isNull(\"attribute_1\")=true"); // test ilike expect(cqlStringField("attribute_1", "ilike", "A")).toBe("strToLowerCase(\"attribute_1\") LIKE '%a%'"); // test LIKE @@ -1419,6 +1443,51 @@ describe('FilterUtils', () => { }; expect(toCQLFilter(filterObject)).toBe("(\"STATE_NAME\"='Alabama' OR (\"STATE_NAME\"='Arizona' OR \"STATE_NAME\"='Arkansas'))"); }); + it('isNull operator in CQL filter', () => { + const filterObj = { + "searchUrl": null, + "featureTypeConfigUrl": null, + "showGeneratedFilter": false, + "attributePanelExpanded": true, + "spatialPanelExpanded": true, + "crossLayerExpanded": true, + "showDetailsPanel": false, + "groupLevels": 5, + "useMapProjection": false, + "toolbarEnabled": true, + "groupFields": [ + { + "id": 1, + "logic": "NOR", + "index": 0 + } + ], + "maxFeaturesWPS": 5, + "filterFields": [ + { + "rowId": 1680880641587, + "groupId": 1, + "attribute": "STATE_NAME", + "operator": "isNull", + "value": null, + "type": "string", + "fieldOptions": { + "valuesCount": 0, + "currentPage": 1 + }, + "exception": null + } + ], + "spatialField": { + "method": null, + "operation": "INTERSECTS", + "geometry": null, + "attribute": "the_geom" + } + }; + expect(toCQLFilter(filterObj)).toBe('(NOT (isNull("STATE_NAME")=true))'); + + }); it('getCrossLayerCqlFilter', () => { const filter = getCrossLayerCqlFilter({ collectGeometries: { @@ -2042,6 +2111,26 @@ describe('FilterUtils', () => { }; filter = processCQLFilterFields(group, objFilter); expect(filter).toEqual(""); + // test one operator + expect(processCQLFilterFields(group, { + filterFields: [{ + groupId: 1, + attribute: "test", + type: "string", + operator: "=", + value: "test" + }] + })).toEqual(`"test"='test'`); + // test is null + expect(processCQLFilterFields(group, { + filterFields: [{ + groupId: 1, + attribute: "test", + type: "string", + operator: "isNull" + }] + })).toEqual(`isNull("test")=true`); + }); it('wrapIfNoWildcards', () => { @@ -2175,4 +2264,43 @@ describe('FilterUtils', () => { }); }); }); + it('isFilterEmpty', () => { + expect(isFilterEmpty({ + filterFields: [], + spatialField: {}, + crossLayerFilter: {}, + filters: [] + })).toBe(true); + expect(isFilterEmpty({ + filterFields: [{value: 1}], + spatialField: {}, + crossLayerFilter: {}, + filters: [] + })).toBe(false); + expect(isFilterEmpty({ + filterFields: [{value: 1}], + spatialField: {geometry: {type: 'Point', coordinates: [1, 2]}}, + crossLayerFilter: {}, + filters: [] + })).toBe(false); + expect(isFilterEmpty({ + filterFields: [], + spatialField: {}, + crossLayerFilter: {attribute: 'attr', operation: 'op'}, + filters: [] + })).toBe(false); + expect(isFilterEmpty({ + filterFields: [], + spatialField: {}, + crossLayerFilter: {}, + filters: [{format: 'logic', logic: 'AND', filters: []}] + })).toBe(false); + expect(isFilterEmpty({ + filterFields: [{operator: "isNull"}], + spatialField: {}, + crossLayerFilter: {}, + filters: [] + })).toBe(false); + + }); });