diff --git a/webui/react/src/components/FilterForm/components/type.ts b/webui/react/src/components/FilterForm/components/type.ts index 074523b5a73..f72690b49ec 100644 --- a/webui/react/src/components/FilterForm/components/type.ts +++ b/webui/react/src/components/FilterForm/components/type.ts @@ -25,9 +25,9 @@ export type FilterFormSet = { showArchived: boolean; }; -type FormFieldWithoutId = Omit; +export type FormFieldWithoutId = Omit; -type FormGroupWithoutId = { +export type FormGroupWithoutId = { readonly kind: typeof FormKind.Group; conjunction: Conjunction; children: (FormGroupWithoutId | FormFieldWithoutId)[]; diff --git a/webui/react/src/components/RunFilterInterstitialModalComponent.test.tsx b/webui/react/src/components/RunFilterInterstitialModalComponent.test.tsx index 3284273cdaf..7a05a4fdae1 100644 --- a/webui/react/src/components/RunFilterInterstitialModalComponent.test.tsx +++ b/webui/react/src/components/RunFilterInterstitialModalComponent.test.tsx @@ -109,14 +109,23 @@ describe('RunFilterInterstitialModalComponent', () => { expect(filterFormSetString).toBeDefined(); const filterFormSet = JSON.parse(filterFormSetString || ''); - // TODO: is there a better way to test this expectation? + // TODO: is there a better way to test these expectations? expect(filterFormSet.showArchived).toBeTruthy(); - const [filterGroup, idFilterGroup] = filterFormSet.filterGroup.children?.[0].children || []; - expect(filterGroup).toEqual(expectedFilterGroup); - - const idFilters = idFilterGroup.children; - expect(idFilters.every((f: FormField) => f.operator === '!=')).toBeTruthy(); - expect(idFilters.map((f: FormField) => f.value)).toEqual(expectedExclusions); + const [, , idFilter] = filterFormSet.filterGroup.children; + for (const child of expectedFilterGroup.children) { + expect(filterFormSet.filterGroup.children).toContainEqual(child); + } + + for (const exclusion of expectedExclusions) { + expect(idFilter?.children[0].children).toContainEqual({ + columnName: 'id', + kind: 'field', + location: 'LOCATION_TYPE_RUN', + operator: '!=', + type: 'COLUMN_TYPE_NUMBER', + value: exclusion, + }); + } }); it('calls server with filter describing visual selection', () => { @@ -139,7 +148,7 @@ describe('RunFilterInterstitialModalComponent', () => { const filterFormSet = JSON.parse(filterFormSetString || ''); expect(filterFormSet.showArchived).toBe(false); - const idFilters = filterFormSet.filterGroup.children?.[0].children || []; + const idFilters = filterFormSet.filterGroup.children || []; expect(idFilters.every((f: FormField) => f.operator === '=')).toBe(true); expect(idFilters.map((f: FormField) => f.value)).toEqual(expectedSelection); }); diff --git a/webui/react/src/components/RunFilterInterstitialModalComponent.tsx b/webui/react/src/components/RunFilterInterstitialModalComponent.tsx index 0a7b9f1a6ca..287b3f4a320 100644 --- a/webui/react/src/components/RunFilterInterstitialModalComponent.tsx +++ b/webui/react/src/components/RunFilterInterstitialModalComponent.tsx @@ -10,6 +10,7 @@ import { useAsync } from 'hooks/useAsync'; import { searchRuns } from 'services/api'; import { SelectionType } from 'types'; import { DetError } from 'utils/error'; +import { combine } from 'utils/filterFormSet'; import { getIdsFilter } from 'utils/flatRun'; import mergeAbortControllers from 'utils/mergeAbortControllers'; import { observable } from 'utils/observable'; @@ -77,7 +78,22 @@ export const RunFilterInterstitialModalComponent = forwardRef { if (!isOpen) return NotLoaded; const mergedCanceler = mergeAbortControllers(canceler, closeController.current); - const filter = getIdsFilter(filterFormSet, selection); + const filterWithSingleFilter = combine(filterFormSet.filterGroup, 'and', { + columnName: 'searcherType', + kind: 'field', + location: 'LOCATION_TYPE_RUN', + operator: '!=', + type: 'COLUMN_TYPE_TEXT', + value: 'single', + }); + const filter: FilterFormSetWithoutId = getIdsFilter( + { + ...filterFormSet, + filterGroup: filterWithSingleFilter, + }, + selection, + ); + try { const results = await searchRuns( { diff --git a/webui/react/src/utils/filterFormSet.ts b/webui/react/src/utils/filterFormSet.ts new file mode 100644 index 00000000000..74ad92ba07b --- /dev/null +++ b/webui/react/src/utils/filterFormSet.ts @@ -0,0 +1,28 @@ +import { + Conjunction, + FormFieldWithoutId, + FormGroupWithoutId, +} from 'components/FilterForm/components/type'; + +/** + * build a new filter group given an existing one and a child. will add the child + * as a child of the current formGroup if the conjunction matches, otherwise + * will create a new wrapper group having both as children + */ +export const combine = ( + filterGroup: FormGroupWithoutId, + conjunction: Conjunction, + child: FormGroupWithoutId | FormFieldWithoutId, +): FormGroupWithoutId => { + if (filterGroup.conjunction === conjunction) { + return { + ...filterGroup, + children: [...filterGroup.children, child], + }; + } + return { + children: [filterGroup, child], + conjunction, + kind: 'group', + }; +}; diff --git a/webui/react/src/utils/flatRun.ts b/webui/react/src/utils/flatRun.ts index 5a7112893b6..d16feb8e1fc 100644 --- a/webui/react/src/utils/flatRun.ts +++ b/webui/react/src/utils/flatRun.ts @@ -8,6 +8,8 @@ import { import { PermissionsHook } from 'hooks/usePermissions'; import { FlatRun, FlatRunAction, RunState, SelectionType } from 'types'; +import { combine } from './filterFormSet'; + type FlatRunChecker = (flatRun: Readonly) => boolean; type FlatRunPermissionSet = Pick< @@ -89,12 +91,11 @@ const idToFilter = (operator: Operator, id: number) => export const getIdsFilter = ( filterFormSet: FilterFormSetWithoutId, selection: SelectionType, -): FilterFormSetWithoutId | undefined => { +): FilterFormSetWithoutId => { const filterGroup: FilterFormSetWithoutId['filterGroup'] = selection.type === 'ALL_EXCEPT' - ? { + ? combine(filterFormSet.filterGroup, 'and', { children: [ - filterFormSet.filterGroup, { children: selection.exclusions.map(idToFilter.bind(this, '!=')), conjunction: 'and', @@ -103,30 +104,15 @@ export const getIdsFilter = ( ], conjunction: 'and', kind: 'group', - } + }) : { children: selection.selections.map(idToFilter.bind(this, '=')), conjunction: 'or', kind: 'group', }; - const filter: FilterFormSetWithoutId = { + return { ...filterFormSet, - filterGroup: { - children: [ - filterGroup, - { - columnName: 'searcherType', - kind: 'field', - location: 'LOCATION_TYPE_RUN', - operator: '!=', - type: 'COLUMN_TYPE_TEXT', - value: 'single', - } as const, - ], - conjunction: 'and', - kind: 'group', - }, + filterGroup, }; - return filter; };