Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for null & not null filter options to enum columns #92

Merged
merged 3 commits into from
Jun 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
16 changes: 11 additions & 5 deletions src/AutoUI/Filters/PersistentFilters.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -70,7 +69,9 @@ export const listFilterQuery = (filters: JSONSchema[]) => {
}),
);
});
return qs.stringify(queryStringFilters);
return qs.stringify(queryStringFilters, {
strictNullHandling: true,
});
};

export const loadRulesFromUrl = (
Expand All @@ -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];
Expand Down
24 changes: 16 additions & 8 deletions src/DataTypes/enum.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -11,6 +12,9 @@ export type OperatorSlug =
| keyof ReturnType<typeof operators>
| typeof FULL_TEXT_SLUG;

const notNullObj = { not: { const: null } };
const isNotNullObj = (value: unknown) => isEqual(value, notNullObj);

export const createFilter: CreateFilter<OperatorSlug> = (
field,
operator,
Expand All @@ -37,9 +41,11 @@ export const createFilter: CreateFilter<OperatorSlug> = (
return {
type: 'object',
properties: {
[field]: {
const: value,
},
[field]: isNotNullObj(value)
? value
: {
const: value,
},
},
required: [field],
};
Expand All @@ -49,11 +55,13 @@ export const createFilter: CreateFilter<OperatorSlug> = (
return {
type: 'object',
properties: {
[field]: {
not: {
const: value,
},
},
[field]: isNotNullObj(value)
? { const: null }
: {
not: {
const: value,
},
},
},
};
}
Expand Down
11 changes: 8 additions & 3 deletions src/components/Filters/FilterDescription.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we could do v

Suggested change
return (schema.enumNames as string[])[index];
return String(schema.enumNames[index]);

}

if (typeof value === 'object') {
if (Object.keys(value).length > 1) {
Expand Down Expand Up @@ -54,7 +59,7 @@ export const FilterDescription = ({
{
name: parsedFilterDescription.field,
operator: 'contains',
value: transformToRidableValue(parsedFilterDescription),
value: transformToReadableValue(parsedFilterDescription),
},
]
: undefined;
Expand All @@ -69,7 +74,7 @@ export const FilterDescription = ({
if (!parsedFilterDescription) {
return;
}
const value = transformToRidableValue(parsedFilterDescription);
const value = transformToReadableValue(parsedFilterDescription);
return {
name:
parsedFilterDescription?.schema?.title ??
Expand Down
14 changes: 13 additions & 1 deletion src/components/Filters/FiltersDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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,
Expand All @@ -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),
};
});
};
Expand Down
90 changes: 89 additions & 1 deletion src/oData/converter.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { JSONSchema } from 'rendition';
import { JSONSchema7 as JSONSchema } from 'json-schema';
import { convertToPineClientFilter } from './jsonToOData';

type FilterTest = {
Expand Down Expand Up @@ -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', () => {
Expand Down
4 changes: 2 additions & 2 deletions src/oData/jsonToOData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
Loading