Skip to content

Commit

Permalink
Add additional conditional support to KeywordList filter (#2447)
Browse files Browse the repository at this point in the history
* Add additional condtionals as KeywordList filter search options

* Fix null conditionals in toListWorkflowFilters and account for is and is null values
  • Loading branch information
laurakwhit authored Dec 3, 2024
1 parent f010d7e commit 2b399a5
Show file tree
Hide file tree
Showing 9 changed files with 188 additions and 60 deletions.
96 changes: 62 additions & 34 deletions src/lib/components/search-attribute-filter/list-filter.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,51 +3,79 @@
import Button from '$lib/holocene/button.svelte';
import ChipInput from '$lib/holocene/input/chip-input.svelte';
import Input from '$lib/holocene/input/input.svelte';
import { translate } from '$lib/i18n/translate';
import { isInConditional } from '$lib/utilities/is';
import { formatListFilterValue } from '$lib/utilities/query/search-attribute-filter';
import ConditionalMenu from './conditional-menu.svelte';
import { FILTER_CONTEXT, type FilterContext } from './index.svelte';
const { filter, handleSubmit } = getContext<FilterContext>(FILTER_CONTEXT);
$: ({ value } = $filter);
$: list =
value.length > 0
? value
.slice(1, -1)
.split(', ')
.map((v) => v.slice(1, -1))
: [];
$: ({ value, conditional } = $filter);
$: _value = value;
$: chips = formatListFilterValue(_value);
$: options = [
{ value: 'in', label: 'In' },
{ value: '=', label: translate('common.equal-to') },
{ value: '!=', label: translate('common.not-equal-to') },
];
function onSubmit() {
$filter.conditional = 'IN';
list = list.map((item) => `"${item}"`);
$filter.value = `(${list.join(', ')})`;
$filter.value = `(${chips.map((item) => `"${item}"`).join(', ')})`;
handleSubmit();
}
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Enter' && _value !== '') {
e.preventDefault();
$filter.value = _value;
handleSubmit();
}
}
</script>

<form class="flex grow" on:submit|preventDefault={onSubmit}>
<ChipInput
label={$filter.attribute}
labelHidden
id="list-filter"
bind:chips={list}
class="w-full"
removeChipButtonLabel={(chip) =>
translate('workflows.remove-keyword-label', { keyword: chip })}
placeholder="{translate('common.type-or-paste-in')} {$filter.attribute}"
unroundLeft
unroundRight
external
/>
<div class="flex h-fit items-center">
<Button
borderRadiusModifier="square-left"
disabled={!list.length}
type="submit"
>
{translate('common.search')}
</Button>
<slot />
</div>
<ConditionalMenu inputId="list-filter" noBorderLeft {options}>
{#if isInConditional(conditional)}
<ChipInput
label={$filter.attribute}
labelHidden
id="list-filter"
bind:chips
class="w-full"
removeChipButtonLabel={(chip) =>
translate('workflows.remove-keyword-label', { keyword: chip })}
placeholder="{translate('common.enter')} {$filter.attribute}"
unroundLeft
unroundRight
external
/>
<div class="flex h-fit items-center">
<Button
borderRadiusModifier="square-left"
disabled={!chips.length}
type="submit"
>
{translate('common.search')}
</Button>
<slot />
</div>
{:else}
<Input
label={$filter.attribute}
labelHidden
id="list-filter"
type="search"
placeholder={`${translate('common.enter')} ${$filter.attribute}`}
icon="search"
class="w-full"
unroundLeft
bind:value={_value}
on:keydown={handleKeydown}
/>
<slot />
{/if}
</ConditionalMenu>
</form>
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@
import { translate } from '$lib/i18n/translate';
import type { SearchAttributeFilter } from '$lib/models/search-attribute-filters';
import type { SearchAttributeOption } from '$lib/stores/search-attributes';
import type { SearchAttributeType } from '$lib/types/workflows';
import {
SEARCH_ATTRIBUTE_TYPE,
type SearchAttributeType,
} from '$lib/types/workflows';
import { getFocusedElementId } from '$lib/utilities/query/search-attribute-filter';
import { emptyFilter } from '$lib/utilities/query/to-list-workflow-filters';
Expand All @@ -34,7 +37,8 @@
function handleNewQuery(value: string, type: SearchAttributeType) {
searchAttributeValue = '';
filter.set({ ...emptyFilter(), attribute: value, conditional: '=', type });
const conditional = type === SEARCH_ATTRIBUTE_TYPE.KEYWORDLIST ? 'in' : '=';
filter.set({ ...emptyFilter(), attribute: value, conditional, type });
$focusedElementId = getFocusedElementId($filter);
}
Expand Down
24 changes: 21 additions & 3 deletions src/lib/utilities/query/filter-workflow-query.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,13 +207,31 @@ describe('toListWorkflowQueryFromFilters', () => {
{
attribute: 'CustomKeywordListField',
type: 'KeywordList',
conditional: 'IN',
conditional: 'in',
operator: '',
parenthesis: '',
value: '("Hello", "World")',
},
{
attribute: 'CustomKeywordListField',
type: 'KeywordList',
conditional: 'is',
operator: '',
parenthesis: '',
value: null,
},
{
attribute: 'CustomKeywordListField',
type: 'KeywordList',
conditional: '=',
operator: '',
parenthesis: '',
value: 'Hello',
},
];
const query = toListWorkflowQueryFromFilters(filters);
expect(query).toBe('`CustomKeywordListField`IN("Hello", "World")');
const query = toListWorkflowQueryFromFilters(combineFilters(filters));
expect(query).toBe(
'`CustomKeywordListField`in("Hello", "World") AND `CustomKeywordListField` is null AND `CustomKeywordListField`="Hello"',
);
});
});
32 changes: 24 additions & 8 deletions src/lib/utilities/query/filter-workflow-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
type SearchAttributeType,
} from '$lib/types/workflows';

import { isNullConditional, isStartsWith } from '../is';
import { isInConditional, isNullConditional, isStartsWith } from '../is';
import { isDuration, isDurationString, toDate, tomorrow } from '../to-duration';

export type QueryKey =
Expand Down Expand Up @@ -40,14 +40,22 @@ const isValid = (value: unknown, conditional: string): boolean => {
return true;
};

const formatValue = (
value: string,
type: SearchAttributeType,
): string | boolean => {
const formatValue = ({
value,
type,
conditional,
}: {
value: string;
type: SearchAttributeType;
conditional: string;
}): string | boolean => {
if (type === SEARCH_ATTRIBUTE_TYPE.BOOL) {
return value.toLowerCase() === 'true' ? true : false;
}
if (type === SEARCH_ATTRIBUTE_TYPE.KEYWORDLIST) {
if (
type === SEARCH_ATTRIBUTE_TYPE.KEYWORDLIST &&
isInConditional(conditional)
) {
return value;
}
return `"${value}"`;
Expand Down Expand Up @@ -90,10 +98,18 @@ const toFilterQueryStatement = (
}

if (isStartsWith(conditional)) {
return `\`${queryKey}\` ${conditional} ${formatValue(value, type)}`;
return `\`${queryKey}\` ${conditional} ${formatValue({
value,
type,
conditional,
})}`;
}

return `\`${queryKey}\`${conditional}${formatValue(value, type)}`;
return `\`${queryKey}\`${conditional}${formatValue({
value,
type,
conditional,
})}`;
};

const toQueryStatementsFromFilters = (
Expand Down
24 changes: 24 additions & 0 deletions src/lib/utilities/query/search-attribute-filter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { describe, expect, it } from 'vitest';
import type { SearchAttributes } from '$lib/types/workflows';

import {
formatListFilterValue,
isBooleanFilter,
isDateTimeFilter,
isDurationFilter,
Expand Down Expand Up @@ -150,3 +151,26 @@ describe('isDateTimeFilter', () => {
).toBe(false);
});
});

describe('formatListFilterValue', () => {
it('should return an empty array if there is no value', () => {
expect(formatListFilterValue('')).toStrictEqual([]);
});

it('should return an array of strings if the value starts with "(" and ends with ")"', () => {
expect(formatListFilterValue('("one")')).toStrictEqual(['one']);
expect(formatListFilterValue('("one", "two")')).toStrictEqual([
'one',
'two',
]);
expect(formatListFilterValue('("one","two","three")')).toStrictEqual([
'one',
'two',
'three',
]);
});

it('should return an array with the value', () => {
expect(formatListFilterValue('example')).toStrictEqual(['example']);
});
});
11 changes: 11 additions & 0 deletions src/lib/utilities/query/search-attribute-filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,14 @@ export function getFocusedElementId(filter: SearchAttributeFilter) {

return '';
}

export function formatListFilterValue(value: string): string[] {
if (value.startsWith('(') && value.endsWith(')')) {
return value
.slice(1, -1)
.split(',')
.map((v) => v.trim().slice(1, -1));
}
if (value) return [value];
return [];
}
44 changes: 35 additions & 9 deletions src/lib/utilities/query/to-list-workflow-filters.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const workflowQueryWithSpaces =
const prefixQuery = '`WorkflowType` STARTS_WITH "hello"';
const isEmptyQuery = '`WorkflowType` is null';
const isNotEmptyQuery = '`StartTime` IS NOT NULL';
const keywordListQuery = '`CustomKeywordListField`IN("Hello", "World")';
const keywordListQuery = '`CustomKeywordListField`in("Hello", "World")';

const attributes = {
CloseTime: 'Datetime',
Expand Down Expand Up @@ -276,7 +276,7 @@ describe('toListWorkflowFilters', () => {
{
attribute: 'CustomKeywordListField',
type: 'KeywordList',
conditional: 'IN',
conditional: 'in',
operator: '',
parenthesis: '',
value: '("Hello", "World")',
Expand All @@ -285,7 +285,7 @@ describe('toListWorkflowFilters', () => {
expect(result).toEqual(expectedFilters);
});

it('should parse a query with a KeywordList type and other types ', () => {
it('should parse a query with a KeywordList type and other types', () => {
const result = toListWorkflowFilters(
keywordListQuery + ' AND ' + workflowQuery4 + ' AND ' + keywordListQuery,
attributes,
Expand All @@ -294,7 +294,7 @@ describe('toListWorkflowFilters', () => {
{
attribute: 'CustomKeywordListField',
type: 'KeywordList',
conditional: 'IN',
conditional: 'in',
operator: 'AND',
parenthesis: '',
value: '("Hello", "World")',
Expand Down Expand Up @@ -342,7 +342,7 @@ describe('toListWorkflowFilters', () => {
{
attribute: 'CustomKeywordListField',
type: 'KeywordList',
conditional: 'IN',
conditional: 'in',
operator: '',
parenthesis: '',
value: '("Hello", "World")',
Expand Down Expand Up @@ -1176,7 +1176,7 @@ describe('combineFilters', () => {
conditional: 'is',
operator: '',
parenthesis: '',
value: 'null',
value: null,
},
];
expect(result).toEqual(expectedFilters);
Expand All @@ -1191,7 +1191,7 @@ describe('combineFilters', () => {
conditional: 'IS NOT',
operator: '',
parenthesis: '',
value: 'NULL',
value: null,
},
];
expect(result).toEqual(expectedFilters);
Expand All @@ -1209,15 +1209,41 @@ describe('combineFilters', () => {
conditional: 'is',
operator: 'AND',
parenthesis: '',
value: 'null',
value: null,
},
{
attribute: 'StartTime',
type: 'Datetime',
conditional: 'IS NOT',
operator: '',
parenthesis: '',
value: 'NULL',
value: null,
},
];
expect(result).toEqual(expectedFilters);
});

it('should parse a query with "is" and "is not" as a value', () => {
const result = toListWorkflowFilters(
'`WorkflowId`="is" AND `WorkflowType`="is not"',
attributes,
);
const expectedFilters = [
{
attribute: 'WorkflowId',
type: 'Keyword',
conditional: '=',
operator: 'AND',
parenthesis: '',
value: 'is',
},
{
attribute: 'WorkflowType',
type: 'Keyword',
conditional: '=',
operator: '',
parenthesis: '',
value: 'is not',
},
];
expect(result).toEqual(expectedFilters);
Expand Down
Loading

0 comments on commit 2b399a5

Please sign in to comment.