diff --git a/package.json b/package.json index 7f6b2e5..b37b19a 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "README.md" ], "dependencies": { - "@balena/ui-shared-components": "^5.8.1", + "@balena/ui-shared-components": "^5.8.2", "ajv": "^8.12.0", "ajv-formats": "^3.0.1", "ajv-keywords": "^5.1.0", diff --git a/src/AutoUI/Filters/PersistentFilters.tsx b/src/AutoUI/Filters/PersistentFilters.tsx index 63719df..f552e83 100644 --- a/src/AutoUI/Filters/PersistentFilters.tsx +++ b/src/AutoUI/Filters/PersistentFilters.tsx @@ -1,5 +1,4 @@ import * as React from 'react'; -import filter from 'lodash/filter'; import qs from 'qs'; import { JSONSchema } from 'rendition'; import { History } from 'history'; @@ -70,7 +69,9 @@ export const listFilterQuery = (filters: JSONSchema[]) => { }), ); }); - return qs.stringify(queryStringFilters); + return qs.stringify(queryStringFilters, { + strictNullHandling: true, + }); }; export const loadRulesFromUrl = ( @@ -82,10 +83,15 @@ export const loadRulesFromUrl = ( if (!searchLocation || !properties) { return []; } - const parsed = qs.parse(searchLocation, { ignoreQueryPrefix: true }) || {}; - const rules = filter(parsed, isQueryStringFilterRuleset) + const parsed = + qs.parse(searchLocation, { + ignoreQueryPrefix: true, + strictNullHandling: true, + }) || {}; + + const rules = (Array.isArray(parsed) ? parsed : Object.values(parsed)) + .filter(isQueryStringFilterRuleset) .map( - // @ts-expect-error (rules: ListQueryStringFilterObject[]) => { if (!Array.isArray(rules)) { rules = [rules]; diff --git a/src/DataTypes/enum.tsx b/src/DataTypes/enum.tsx index 9d0ebb1..571d4a5 100644 --- a/src/DataTypes/enum.tsx +++ b/src/DataTypes/enum.tsx @@ -1,3 +1,4 @@ +import isEqual from 'lodash/isEqual'; import { FULL_TEXT_SLUG } from '../components/Filters/SchemaSieve'; import { CreateFilter, getDataTypeSchema, regexEscape } from './utils'; import { JSONSchema7 as JSONSchema } from 'json-schema'; @@ -11,6 +12,9 @@ export type OperatorSlug = | keyof ReturnType | typeof FULL_TEXT_SLUG; +const notNullObj = { not: { const: null } }; +const isNotNullObj = (value: unknown) => isEqual(value, notNullObj); + export const createFilter: CreateFilter = ( field, operator, @@ -37,9 +41,11 @@ export const createFilter: CreateFilter = ( return { type: 'object', properties: { - [field]: { - const: value, - }, + [field]: isNotNullObj(value) + ? value + : { + const: value, + }, }, required: [field], }; @@ -49,11 +55,13 @@ export const createFilter: CreateFilter = ( return { type: 'object', properties: { - [field]: { - not: { - const: value, - }, - }, + [field]: isNotNullObj(value) + ? { const: null } + : { + not: { + const: value, + }, + }, }, }; } diff --git a/src/components/Filters/FilterDescription.tsx b/src/components/Filters/FilterDescription.tsx index 1cfb5a2..7e1a40a 100644 --- a/src/components/Filters/FilterDescription.tsx +++ b/src/components/Filters/FilterDescription.tsx @@ -9,14 +9,19 @@ import { import { isDateTimeFormat } from '../../DataTypes'; import { format as dateFormat } from 'date-fns'; import { isJSONSchema } from '../../AutoUI/schemaOps'; +import isEqual from 'lodash/isEqual'; -const transformToRidableValue = ( +const transformToReadableValue = ( parsedFilterDescription: SieveFilterDescription, ): string => { const { schema, value } = parsedFilterDescription; if (schema && isDateTimeFormat(schema.format)) { return dateFormat(value, 'PPPppp'); } + if (schema?.enum && 'enumNames' in schema) { + const index = schema.enum.findIndex((a) => isEqual(a, value)); + return (schema.enumNames as string[])[index]; + } if (typeof value === 'object') { if (Object.keys(value).length > 1) { @@ -54,7 +59,7 @@ export const FilterDescription = ({ { name: parsedFilterDescription.field, operator: 'contains', - value: transformToRidableValue(parsedFilterDescription), + value: transformToReadableValue(parsedFilterDescription), }, ] : undefined; @@ -69,7 +74,7 @@ export const FilterDescription = ({ if (!parsedFilterDescription) { return; } - const value = transformToRidableValue(parsedFilterDescription); + const value = transformToReadableValue(parsedFilterDescription); return { name: parsedFilterDescription?.schema?.title ?? diff --git a/src/components/Filters/FiltersDialog.tsx b/src/components/Filters/FiltersDialog.tsx index 3694bb8..8090bf5 100644 --- a/src/components/Filters/FiltersDialog.tsx +++ b/src/components/Filters/FiltersDialog.tsx @@ -23,6 +23,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faTimes } from '@fortawesome/free-solid-svg-icons/faTimes'; import { faPlus } from '@fortawesome/free-solid-svg-icons/faPlus'; import { getRefSchema } from '../../AutoUI/schemaOps'; +import { findInObject } from '../../AutoUI/utils'; const { Box, Button, IconButton, Typography, DialogContent } = Material; @@ -113,6 +114,16 @@ const initialFormData = [ }, ]; +const getDefaultValue = ( + data: FormData | undefined, + propertySchema: JSONSchema | undefined, +) => { + const schemaEnum = findInObject(propertySchema, 'enum'); + const schemaOneOf = findInObject(propertySchema, 'oneOf'); + + return data?.value ?? schemaEnum?.[0] ?? schemaOneOf?.[0]?.const ?? undefined; +}; + const normalizeFormData = ( data: FormData[] | FormData | undefined, schema: JSONSchema, @@ -136,10 +147,11 @@ const normalizeFormData = ( ? Object.keys(model.operators).find((o) => o === d?.operator) ?? Object.keys(model.operators)[0] : undefined; + return { field: d?.field ?? field, operator, - value: d?.value, + value: getDefaultValue(d, propertySchema), }; }); }; diff --git a/src/oData/converter.spec.ts b/src/oData/converter.spec.ts index fa182a1..31a3183 100644 --- a/src/oData/converter.spec.ts +++ b/src/oData/converter.spec.ts @@ -1,4 +1,4 @@ -import { JSONSchema } from 'rendition'; +import { JSONSchema7 as JSONSchema } from 'json-schema'; import { convertToPineClientFilter } from './jsonToOData'; type FilterTest = { @@ -318,6 +318,94 @@ const filterTests: FilterTest[] = [ }, }, }, + { + testCase: 'should convert enum "is" "null" filter to pine $filter', // is default + filters: [ + { + $id: 'EEVF5Y2fWZd84xWq', + title: 'is', + description: + '{"title":"Release policy","field":"should_be_running__release","operator":{"slug":"is","label":"is"},"value":null}', + type: 'object', + properties: { + should_be_running__release: { + const: null, + }, + }, + required: ['should_be_running__release'], + }, + ], + expected: { + should_be_running__release: null, + }, + }, + { + testCase: 'should convert enum "is" "not null" filter to pine $filter', // is pinned + filters: [ + { + $id: 'FEVF5Y2fWZd84xWq', + title: 'is', + description: + '{"title":"Release policy","field":"should_be_running__release","operator":{"slug":"is","label":"is"},"value":{"not":null}}', + type: 'object', + properties: { + should_be_running__release: { + not: { const: null }, + }, + }, + required: ['should_be_running__release'], + }, + ], + expected: { + $not: { + should_be_running__release: null, + }, + }, + }, + { + testCase: 'should convert enum "is_not" "null" filter to pine $filter', // is not pinned + filters: [ + { + $id: 'GEVF5Y2fWZd84xWq', + title: 'is_not', + description: + '{"title":"Release policy","field":"should_be_running__release","operator":{"slug":"is_not","label":"is not"},"value":null}', + type: 'object', + properties: { + should_be_running__release: { + const: null, + }, + }, + required: ['should_be_running__release'], + }, + ], + expected: { + should_be_running__release: null, + }, + }, + { + testCase: 'should convert enum "is_not" "not null" filter to pine $filter', // is not default + filters: [ + { + $id: 'HEVF5Y2fWZd84xWq', + title: 'is_not', + description: + '{"title":"Release policy","field":"should_be_running__release","operator":{"slug":"is_not","label":"is not"},"value":{"not":null}}', + type: 'object', + properties: { + should_be_running__release: { + not: { const: null }, + }, + }, + required: ['should_be_running__release'], + }, + ], + expected: { + $not: { + should_be_running__release: null, + }, + }, + }, ]; describe('JSONSchema to Pine client converter', () => { diff --git a/src/oData/jsonToOData.ts b/src/oData/jsonToOData.ts index be88e37..4fe2979 100644 --- a/src/oData/jsonToOData.ts +++ b/src/oData/jsonToOData.ts @@ -46,10 +46,10 @@ const handlePrimitiveFilter = ( formatExclusiveMinimum?: string; }, ): PineFilterObject => { - if (value.const != null) { + if (value.const !== undefined) { return wrapValue(parentKeys, value.const); } - if (value.enum != null) { + if (value.enum !== undefined) { return wrapValue(parentKeys, { $in: value.enum }); } const regexp =