From 8e42d33b7ad513ed7729cb23fae23ea66326a3b1 Mon Sep 17 00:00:00 2001 From: XingY Date: Sat, 13 Dec 2025 17:24:14 -0800 Subject: [PATCH 01/12] Workflow Automation: Task action to filter samples for selected task --- packages/components/src/index.ts | 17 +- .../entities/FindDerivativesButton.test.tsx | 6 +- .../entities/FindDerivativesButton.tsx | 43 +-- .../internal/components/entities/models.ts | 5 +- .../search/QueryFilterPanel.spec.tsx | 224 --------------- .../search/QueryFilterPanel.test.tsx | 271 ++++++++++++++++++ .../components/search/QueryFilterPanel.tsx | 27 +- .../src/internal/components/search/models.ts | 2 +- .../internal/components/search/utils.test.ts | 32 +-- .../src/internal/components/search/utils.ts | 4 +- .../public/QueryModel/FilterStatus.test.tsx | 33 +++ .../src/public/QueryModel/FilterStatus.tsx | 6 +- .../src/public/QueryModel/GridFilterModal.tsx | 8 +- .../src/public/QueryModel/grid/Value.tsx | 5 +- .../QueryModel/grid/actions/Filter.test.ts | 125 +++++++- .../public/QueryModel/grid/actions/Filter.ts | 79 ++++- .../src/public/QueryModel/grid/utils.test.ts | 49 +++- .../src/public/QueryModel/grid/utils.ts | 6 +- 18 files changed, 647 insertions(+), 295 deletions(-) delete mode 100644 packages/components/src/internal/components/search/QueryFilterPanel.spec.tsx create mode 100644 packages/components/src/internal/components/search/QueryFilterPanel.test.tsx diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index 862b90c7a3..8e95f6158c 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -346,6 +346,7 @@ import { FindDerivativesButton, FindDerivativesMenuItem, getSampleFinderLocalStorageKey, + getSearchFilterObj, getSearchFilterObjs, SAMPLE_FINDER_SESSION_PREFIX, searchFiltersToJson, @@ -623,6 +624,9 @@ import { GridPanel, GridPanelWithModel } from './public/QueryModel/GridPanel'; import { TabbedGridPanel } from './public/QueryModel/TabbedGridPanel'; import { DetailPanel, DetailPanelWithModel } from './public/QueryModel/DetailPanel'; import { makeTestActions, makeTestQueryModel } from './public/QueryModel/testUtils'; +import { FilterStatus } from './public/QueryModel/FilterStatus'; +import { FilterAction, getActionValuesForFilterProps, removeFilterValueForFilterProps } from './public/QueryModel/grid/actions/Filter'; + import { BACKGROUND_IMPORT_MIN_FILE_SIZE, BACKGROUND_IMPORT_MIN_ROW_SIZE, @@ -1228,6 +1232,10 @@ export { DetailPanel, DetailPanelHeader, DetailPanelWithModel, + FilterStatus, + FilterAction, + getActionValuesForFilterProps, + removeFilterValueForFilterProps, DisableableAnchor, DisableableButton, DisableableMenuItem, @@ -1392,6 +1400,7 @@ export { getSampleTypeDetails, getSampleTypesFromTransactionIds, getSchemaQuery, + getSearchFilterObj, getSearchFilterObjs, getSearchScopeFromContainerFilter, getSelected, @@ -1863,7 +1872,7 @@ export type { StorageActionStatusCounts, } from './internal/components/samples/models'; export type { SearchHit, SearchOptions } from './internal/components/search/actions'; -export type { FieldFilter } from './internal/components/search/models'; +export type { EntityFieldFilter } from './internal/components/search/models'; export type { SecurityAPIWrapper } from './internal/components/security/APIWrapper'; export type { IDataViewInfo } from './internal/DataViewInfo'; export type { BSStyle } from './internal/dropdowns'; @@ -1910,3 +1919,9 @@ export type { QueryModelMap, RequiresModelAndActions, } from './public/QueryModel/withQueryModels'; + +export type { + Action, + ActionValue +} from './public/QueryModel/grid/actions/Action' + diff --git a/packages/components/src/internal/components/entities/FindDerivativesButton.test.tsx b/packages/components/src/internal/components/entities/FindDerivativesButton.test.tsx index 97f895a8cb..a5f9c48fff 100644 --- a/packages/components/src/internal/components/entities/FindDerivativesButton.test.tsx +++ b/packages/components/src/internal/components/entities/FindDerivativesButton.test.tsx @@ -10,7 +10,7 @@ import { SchemaQuery } from '../../../public/SchemaQuery'; import { TestTypeDataType, TestTypeDataTypeWithEntityFilter } from '../../../test/data/constants'; -import { FieldFilter } from '../search/models'; +import { EntityFieldFilter } from '../search/models'; import { SCHEMAS } from '../../schemas'; @@ -232,14 +232,14 @@ const anyValueFilter = { fieldCaption: 'textField', filter: Filter.create('textField', null, Filter.Types.HAS_ANY_VALUE), jsonType: 'string', -} as FieldFilter; +} as EntityFieldFilter; const stringBetweenFilter = { fieldKey: 'strField', fieldCaption: 'strField', filter: Filter.create('strField', ['1', '5'], Filter.Types.BETWEEN), jsonType: 'string', -} as FieldFilter; +} as EntityFieldFilter; const card = { entityDataType: TestTypeDataType, diff --git a/packages/components/src/internal/components/entities/FindDerivativesButton.tsx b/packages/components/src/internal/components/entities/FindDerivativesButton.tsx index ae95958dff..68ba55dec3 100644 --- a/packages/components/src/internal/components/entities/FindDerivativesButton.tsx +++ b/packages/components/src/internal/components/entities/FindDerivativesButton.tsx @@ -10,7 +10,7 @@ import { QueryColumn } from '../../../public/QueryColumn'; import { QueryInfo } from '../../../public/QueryInfo'; import { isValidFilterField } from '../search/utils'; import { QueryModel } from '../../../public/QueryModel/QueryModel'; -import { FieldFilter } from '../search/models'; +import { EntityFieldFilter } from '../search/models'; import { useAppContext } from '../../AppContext'; import { ResponsiveMenuButton } from '../buttons/ResponsiveMenuButton'; @@ -44,7 +44,7 @@ export function isValidFilterFieldSampleFinder( } // exported for unit test coverage -export const getFieldFilter = (model: QueryModel, filter: Filter.IFilter): FieldFilter => { +export const getFieldFilter = (model: QueryModel, filter: Filter.IFilter): EntityFieldFilter => { const colName = filter.getColumnName(); const column = model.getColumn(colName); @@ -53,7 +53,7 @@ export const getFieldFilter = (model: QueryModel, filter: Filter.IFilter): Field fieldCaption: column?.caption ?? colName, filter, jsonType: column?.isLookup() ? column.displayFieldJsonType : (column?.jsonType ?? 'string'), - } as FieldFilter; + } as EntityFieldFilter; }; // exported for unit test coverage @@ -126,27 +126,30 @@ function filterToJson(filter: Filter.IFilter): string { return encodeURIComponent(filter.getURLParameterName()) + '=' + encodeURIComponent(filter.getURLParameterValue()); } +export function getSearchFilterObj(filterProp: FilterProps): any { + const filterPropObj = { ...filterProp }; + delete filterPropObj['entityDataType']; + // don't persist the entire entitydatatype + filterPropObj['sampleFinderCardType'] = filterProp.entityDataType.sampleFinderCardType; + + const filterArrayObjs = []; + [...filterPropObj.filterArray].forEach(field => { + filterArrayObjs.push({ + fieldKey: field.fieldKey, + fieldCaption: field.fieldCaption, + filter: filterToJson(field.filter), + jsonType: field.jsonType, + }); + }); + filterPropObj.filterArray = filterArrayObjs; + return filterPropObj; +} + export function getSearchFilterObjs(filterProps: FilterProps[]): any[] { const filterPropsObj = []; filterProps.forEach(filterProp => { - const filterPropObj = { ...filterProp }; - delete filterPropObj['entityDataType']; - // don't persist the entire entitydatatype - filterPropObj['sampleFinderCardType'] = filterProp.entityDataType.sampleFinderCardType; - - const filterArrayObjs = []; - [...filterPropObj.filterArray].forEach(field => { - filterArrayObjs.push({ - fieldKey: field.fieldKey, - fieldCaption: field.fieldCaption, - filter: filterToJson(field.filter), - jsonType: field.jsonType, - }); - }); - filterPropObj.filterArray = filterArrayObjs; - - filterPropsObj.push(filterPropObj); + filterPropsObj.push(getSearchFilterObj(filterProp)); }); return filterPropsObj; diff --git a/packages/components/src/internal/components/entities/models.ts b/packages/components/src/internal/components/entities/models.ts index 7167658821..458f3df26e 100644 --- a/packages/components/src/internal/components/entities/models.ts +++ b/packages/components/src/internal/components/entities/models.ts @@ -29,7 +29,7 @@ import { SCHEMAS } from '../../schemas'; import { EntityCreationType } from '../samples/models'; import { QueryInfo } from '../../../public/QueryInfo'; import { ViewInfo } from '../../ViewInfo'; -import { FieldFilter } from '../search/models'; +import { EntityFieldFilter } from '../search/models'; export interface EntityInputProps { role: string; @@ -514,6 +514,7 @@ export interface EntityDataType { typeNounAsParentSingular: string; typeNounSingular: string; uniqueFieldKey: string; + supportAllValueInQuery?: boolean; } interface OperationContainerInfo { @@ -649,7 +650,7 @@ export interface FilterProps { disabled?: boolean; entityDataType: EntityDataType; // the filters to be used in conjunction with the schemaQuery - filterArray?: FieldFilter[]; + filterArray?: EntityFieldFilter[]; index?: number; schemaQuery?: SchemaQuery; selectColumnFieldKey?: string; diff --git a/packages/components/src/internal/components/search/QueryFilterPanel.spec.tsx b/packages/components/src/internal/components/search/QueryFilterPanel.spec.tsx deleted file mode 100644 index 7769a2e291..0000000000 --- a/packages/components/src/internal/components/search/QueryFilterPanel.spec.tsx +++ /dev/null @@ -1,224 +0,0 @@ -import React from 'react'; -import { mount, ReactWrapper } from 'enzyme'; -import { Filter } from '@labkey/api'; - -import { ExtendedMap } from '../../../public/ExtendedMap'; - -import { QueryInfo } from '../../../public/QueryInfo'; -import { ChoicesListItem } from '../base/ChoicesListItem'; -import sampleSetAllFieldTypesQueryInfo from '../../../test/data/sampleSetAllFieldTypes-getQueryDetails.json'; - -import { waitForLifecycle } from '../../test/enzymeTestHelpers'; - -import { getTestAPIWrapper } from '../../APIWrapper'; - -import { AssayResultDataType } from '../entities/constants'; - -import { QueryColumn } from '../../../public/QueryColumn'; - -import { QueryFilterPanel } from './QueryFilterPanel'; -import { FieldFilter } from './models'; - -describe('QueryFilterPanel', () => { - const DEFAULT_PROPS = { - api: getTestAPIWrapper(jest.fn, {}), - filters: {}, - queryInfo: QueryInfo.fromJsonForTests(sampleSetAllFieldTypesQueryInfo, true), - onFilterUpdate: jest.fn, - }; - - function validate( - wrapper: ReactWrapper, - fieldItems: number, - showFilterExpression = false, - showChooseValues = false - ): void { - expect(wrapper.find('.filter-modal__col_fields').hostNodes()).toHaveLength(1); - expect(wrapper.find('.filter-modal__col_filter_exp').hostNodes()).toHaveLength(1); - expect(wrapper.find(ChoicesListItem)).toHaveLength(fieldItems); - - if (showFilterExpression) { - expect(wrapper.find('li[role="presentation"]').at(0).prop('className')).toBe('active'); - } - - if (showChooseValues) { - expect(wrapper.find('li[role="presentation"]').at(1).prop('className')).toBe('active'); - } - } - - test('default props', () => { - const wrapper = mount(); - validate(wrapper, 10); - expect(wrapper.find('.filter-modal__container')).toHaveLength(0); - wrapper.unmount(); - }); - - test('skipDefaultViewCheck', async () => { - const wrapper = mount(); - await waitForLifecycle(wrapper); - validate(wrapper, 28); - wrapper.unmount(); - }); - - test('asRow', () => { - const wrapper = mount(); - validate(wrapper, 10); - expect(wrapper.find('.field-modal__container').hostNodes()).toHaveLength(1); - wrapper.unmount(); - }); - - test('no queryName emptyMsg', () => { - const wrapper = mount(); - validate(wrapper, 0); - expect(wrapper.find('.field-modal__empty-msg').hostNodes()).toHaveLength(1); - expect(wrapper.find('.field-modal__empty-msg').hostNodes().text()).toBe('Select a query'); - wrapper.unmount(); - }); - - test('fullWidth', async () => { - const wrapper = mount(); - validate(wrapper, 10); - expect(wrapper.find('.col-sm-3').exists()).toBe(true); - expect(wrapper.find('.col-sm-6').exists()).toBe(true); - wrapper.setProps({ fullWidth: true }); - await waitForLifecycle(wrapper); - expect(wrapper.find('.col-sm-4').exists()).toBe(true); - expect(wrapper.find('.col-sm-8').exists()).toBe(true); - wrapper.unmount(); - }); - - test('viewName', () => { - const wrapper = mount(); - validate(wrapper, 2); - wrapper.unmount(); - }); - - test('validFilterField', () => { - const wrapper = mount( - field.jsonType === 'string'} /> - ); - validate(wrapper, 6); - wrapper.unmount(); - }); - - test('one field not filterable', () => { - const props = { ...DEFAULT_PROPS }; - let queryInfo = props.queryInfo; - let col: QueryColumn = queryInfo.getDisplayColumns().find(field => field.jsonType === 'string'); - col = col.mutate({ filterable: false }); - const columns = new ExtendedMap(queryInfo.columns); - columns.set(col.fieldKey.toLowerCase(), col); - queryInfo = queryInfo.mutate({ columns }); - props.queryInfo = queryInfo; - const wrapper = mount( - field.jsonType === 'string'} /> - ); - validate(wrapper, 5); - }); - - test('with text activeField', () => { - const wrapper = mount(); - validate(wrapper, 10, false, true); - expect(wrapper.find('.list-group-item.active').text()).toBe('Text'); - expect(wrapper.find('.field-modal__col-sub-title').first().text()).toBe('Find values for Text'); - expect(wrapper.find('.field-modal__empty-msg').hostNodes()).toHaveLength(0); - expect(wrapper.find('a[role="tab"]')).toHaveLength(2); - expect(wrapper.find('.field-modal__field_dot')).toHaveLength(0); - wrapper.unmount(); - }); - - test('with non-text activeField', () => { - const wrapper = mount(); - validate(wrapper, 10, true, false); - expect(wrapper.find('.list-group-item.active').text()).toBe('Integer'); - expect(wrapper.find('.field-modal__col-sub-title').text()).toBe('Find values for Integer'); - expect(wrapper.find('.field-modal__empty-msg').hostNodes()).toHaveLength(0); - expect(wrapper.find('a[role="tab"]')).toHaveLength(1); - wrapper.unmount(); - }); - - test('text activeField with non-equal filter', () => { - const wrapper = mount( - - ); - validate(wrapper, 10, true, false); - expect(wrapper.find('.list-group-item.active').text()).toBe('Text'); - expect(wrapper.find('.field-modal__col-sub-title').first().text()).toBe('Find values for Text'); - expect(wrapper.find('.field-modal__empty-msg').hostNodes()).toHaveLength(0); - expect(wrapper.find('a[role="tab"]')).toHaveLength(2); - expect(wrapper.find('.field-modal__field_dot')).toHaveLength(1); - wrapper.unmount(); - }); - - test('hasNoValueInQuery checkbox, not checked', () => { - const hasNotInQueryFilterLabel = 'Sample Without assay data'; - const wrapper = mount( - - ); - validate(wrapper, 10); - expect(wrapper.find('.filter-modal__fields-col-nodata-msg').hostNodes().text()).toBe(hasNotInQueryFilterLabel); - expect(wrapper.find('.field-modal__col-content-disabled').hostNodes()).toHaveLength(0); - - wrapper.unmount(); - }); - - test('hasNoValueInQuery checkbox, checked', () => { - const wrapper = mount( - - ); - validate(wrapper, 10); - expect(wrapper.find('.filter-modal__fields-col-nodata-msg').hostNodes().text()).toBe( - 'Without data from this type' - ); - expect(wrapper.find('.field-modal__col-content-disabled').hostNodes()).toHaveLength(1); - - wrapper.unmount(); - }); - - test('hasNoValueInQuery checkbox, checked, has active field and filters', () => { - const wrapper = mount( - - ); - expect(wrapper.find('.filter-modal__fields-col-nodata-msg').hostNodes().text()).toBe( - 'Without data from this type' - ); - expect(wrapper.find('.field-modal__col-content-disabled').hostNodes()).toHaveLength(2); - - wrapper.unmount(); - }); -}); diff --git a/packages/components/src/internal/components/search/QueryFilterPanel.test.tsx b/packages/components/src/internal/components/search/QueryFilterPanel.test.tsx new file mode 100644 index 0000000000..15c1941a55 --- /dev/null +++ b/packages/components/src/internal/components/search/QueryFilterPanel.test.tsx @@ -0,0 +1,271 @@ +import React from 'react'; +import { render, waitFor } from '@testing-library/react'; +import { Filter } from '@labkey/api'; + +import { ExtendedMap } from '../../../public/ExtendedMap'; + +import { QueryInfo } from '../../../public/QueryInfo'; +import sampleSetAllFieldTypesQueryInfo from '../../../test/data/sampleSetAllFieldTypes-getQueryDetails.json'; + +import { getTestAPIWrapper } from '../../APIWrapper'; + +import { AssayResultDataType, SamplePropertyDataType } from '../entities/constants'; + +import { QueryColumn } from '../../../public/QueryColumn'; + +import { QueryFilterPanel } from './QueryFilterPanel'; +import { EntityFieldFilter } from './models'; +import { EntityDataType } from '../entities/models'; + +describe('QueryFilterPanel', () => { + const DEFAULT_PROPS = { + api: getTestAPIWrapper(jest.fn, {}), + filters: {}, + queryInfo: QueryInfo.fromJsonForTests(sampleSetAllFieldTypesQueryInfo, true), + onFilterUpdate: jest.fn, + }; + + const TestAllowAllEntityType = { + ...SamplePropertyDataType, + supportAllValueInQuery: true + } as EntityDataType; + + function validate( + fieldItems: number, + showFilterExpression = false, + showChooseValues = false + ): void { + expect(document.querySelectorAll('.filter-modal__col_fields')).toHaveLength(1); + expect(document.querySelectorAll('.filter-modal__col_filter_exp')).toHaveLength(1); + // ChoicesListItem renders as button.list-group-item + expect(document.querySelectorAll('button.list-group-item')).toHaveLength(fieldItems); + + const tabs = document.querySelectorAll('li[role="presentation"]'); + if (showFilterExpression) { + expect((tabs[0] as HTMLElement).className).toBe('active'); + } + if (showChooseValues) { + expect((tabs[1] as HTMLElement).className).toBe('active'); + } + } + + test('default props', () => { + const { unmount } = render(); + validate(10); + expect(document.querySelectorAll('.filter-modal__container')).toHaveLength(0); + unmount(); + }); + + test('skipDefaultViewCheck', async () => { + render(); + await waitFor(() => expect(document.querySelectorAll('button.list-group-item')).toHaveLength(28)); + validate(28); + }); + + test('asRow', () => { + const { unmount } = render(); + validate(10); + expect(document.querySelectorAll('.field-modal__container')).toHaveLength(1); + unmount(); + }); + + test('no queryName emptyMsg', () => { + const { unmount } = render( + + ); + validate(0); + expect(document.querySelectorAll('.field-modal__empty-msg')).toHaveLength(1); + expect(document.querySelector('.field-modal__empty-msg')!.textContent).toBe('Select a query'); + unmount(); + }); + + test('fullWidth', async () => { + const { rerender, unmount } = render(); + validate(10); + expect(document.querySelector('.col-sm-3')).not.toBeNull(); + expect(document.querySelector('.col-sm-6')).not.toBeNull(); + rerender(); + await waitFor(() => expect(document.querySelector('.col-sm-4')).not.toBeNull()); + expect(document.querySelector('.col-sm-8')).not.toBeNull(); + unmount(); + }); + + test('viewName', () => { + const { unmount } = render(); + validate(2); + unmount(); + }); + + test('validFilterField', () => { + const { unmount } = render( + field.jsonType === 'string'} /> + ); + validate(6); + unmount(); + }); + + test('one field not filterable', () => { + const props = { ...DEFAULT_PROPS }; + let queryInfo = props.queryInfo; + let col: QueryColumn = queryInfo.getDisplayColumns().find(field => field.jsonType === 'string'); + col = col.mutate({ filterable: false }); + const columns = new ExtendedMap(queryInfo.columns); + columns.set(col.fieldKey.toLowerCase(), col); + queryInfo = queryInfo.mutate({ columns }); + props.queryInfo = queryInfo; + const { unmount } = render( + field.jsonType === 'string'} /> + ); + validate(5); + unmount(); + }); + + test('with text activeField', () => { + const { unmount } = render(); + validate(10, false, true); + const active = document.querySelector('.list-group-item.active'); + expect(active && active.textContent).toBe('Text'); + expect(document.querySelectorAll('.field-modal__col-sub-title')[0].textContent).toBe('Find values for Text'); + expect(document.querySelectorAll('.field-modal__empty-msg')).toHaveLength(0); + expect(document.querySelectorAll('a[role="tab"]')).toHaveLength(2); + expect(document.querySelectorAll('.field-modal__field_dot')).toHaveLength(0); + unmount(); + }); + + test('with non-text activeField', () => { + const { unmount } = render(); + validate(10, true, false); + const active = document.querySelector('.list-group-item.active'); + expect(active && active.textContent).toBe('Integer'); + expect(document.querySelector('.field-modal__col-sub-title')!.textContent).toBe('Find values for Integer'); + expect(document.querySelectorAll('.field-modal__empty-msg')).toHaveLength(0); + expect(document.querySelectorAll('a[role="tab"]')).toHaveLength(1); + unmount(); + }); + + test('text activeField with non-equal filter', () => { + const { unmount } = render( + + ); + validate(10, true, false); + const active = document.querySelector('.list-group-item.active'); + expect(active && active.textContent).toBe('Text'); + expect(document.querySelectorAll('.field-modal__col-sub-title')[0].textContent).toBe('Find values for Text'); + expect(document.querySelectorAll('.field-modal__empty-msg')).toHaveLength(0); + expect(document.querySelectorAll('a[role="tab"]')).toHaveLength(2); + expect(document.querySelectorAll('.field-modal__field_dot')).toHaveLength(1); + unmount(); + }); + + test('hasNoValueInQuery checkbox, not checked', () => { + const hasNotInQueryFilterLabel = 'Sample Without assay data'; + const { unmount } = render( + + ); + validate(10); + expect(document.querySelector('.filter-modal__fields-col-nodata-msg')!.textContent).toBe( + hasNotInQueryFilterLabel + ); + expect(document.querySelectorAll('.field-modal__col-content-disabled')).toHaveLength(0); + + unmount(); + }); + + test('hasNoValueInQuery checkbox, checked', () => { + const { unmount } = render( + + ); + validate(10); + expect(document.querySelector('.filter-modal__fields-col-nodata-msg')!.textContent).toBe( + 'Without data from this type' + ); + expect(document.querySelectorAll('.field-modal__col-content-disabled')).toHaveLength(1); + + unmount(); + }); + + test('hasNoValueInQuery checkbox, checked, has active field and filters', () => { + const { unmount } = render( + + ); + expect(document.querySelector('.filter-modal__fields-col-nodata-msg')!.textContent).toBe( + 'Without data from this type' + ); + expect(document.querySelectorAll('.field-modal__col-content-disabled')).toHaveLength(2); + + unmount(); + }); + + test('supportAllValueInQuery checkbox, not checked', () => { + const { unmount } = render( + + ); + validate(10); + expect(document.querySelector('.filter-modal__fields-col-any-msg')!.textContent).toBe( + 'All Samples' + ); + // query by name field-value-nodata-check + expect(document.querySelector('input[name="field-value-allvalues-check"]').getAttribute('checked')).toBe(null) + expect(document.querySelectorAll('.field-modal__col-content-disabled')).toHaveLength(0); + + unmount(); + }); + + test('supportAllValueInQuery checkbox, checked', () => { + const { unmount } = render( + + ); + validate(10); + expect(document.querySelector('.filter-modal__fields-col-any-msg')!.textContent).toBe( + 'All Samples' + ); + expect(document.querySelector('input[name="field-value-allvalues-check"]').getAttribute('checked')).toBe(''); + expect(document.querySelectorAll('.field-modal__col-content-disabled')).toHaveLength(0); + + unmount(); + }); +}); diff --git a/packages/components/src/internal/components/search/QueryFilterPanel.tsx b/packages/components/src/internal/components/search/QueryFilterPanel.tsx index 598b5dac55..655b0a152a 100644 --- a/packages/components/src/internal/components/search/QueryFilterPanel.tsx +++ b/packages/components/src/internal/components/search/QueryFilterPanel.tsx @@ -21,8 +21,9 @@ import { Tab, Tabs } from '../../Tabs'; import { FilterFacetedSelector } from './FilterFacetedSelector'; import { FilterExpressionView } from './FilterExpressionView'; -import { FieldFilter } from './models'; +import { EntityFieldFilter } from './models'; import { isChooseValuesFilter } from './utils'; +import { SAMPLE_PROPERTY_ALL_SAMPLE_TYPE } from './constants'; enum FieldFilterTabs { ChooseValues = 'ChooseValues', @@ -41,14 +42,16 @@ interface Props { entityDataType?: EntityDataType; fieldKey?: string; fields?: QueryColumn[]; - filters: Record; + filters: Record; fullWidth?: boolean; hasNotInQueryFilter?: boolean; hasNotInQueryFilterLabel?: string; isAncestor?: boolean; metricFeatureArea?: string; onFilterUpdate: (field: QueryColumn, newFilters: Filter.IFilter[], index: number) => void; + hasAllValuesInQuery?: boolean; onHasNoValueInQueryChange?: (check: boolean) => void; + onAllValuesInQueryChange?: (check: boolean) => void; queryInfo: QueryInfo; selectDistinctOptions?: Partial; skipDefaultViewCheck?: boolean; @@ -60,6 +63,7 @@ export const QueryFilterPanel: FC = memo(props => { const { allowRelativeDateFilter, hasNotInQueryFilter, + hasAllValuesInQuery, asRow, api = getDefaultAPIWrapper(), queryInfo, @@ -74,6 +78,7 @@ export const QueryFilterPanel: FC = memo(props => { fullWidth, selectDistinctOptions, onHasNoValueInQueryChange, + onAllValuesInQueryChange, hasNotInQueryFilterLabel, altQueryName, fields, @@ -188,10 +193,10 @@ export const QueryFilterPanel: FC = memo(props => { return activeField?.getDisplayFieldKey(); }, [activeField]); - const currentFieldFilters = useMemo((): FieldFilter[] => { + const currentFieldFilters = useMemo((): EntityFieldFilter[] => { if (!filters || !activeField) return null; - const activeQueryFilters: FieldFilter[] = filters[filterQueryKey]; + const activeQueryFilters: EntityFieldFilter[] = filters[filterQueryKey]; return activeQueryFilters?.filter(filter => filter.fieldKey === activeFieldKey); }, [activeField, filterQueryKey, filters, activeFieldKey]); @@ -242,6 +247,20 @@ export const QueryFilterPanel: FC = memo(props => { {queryName && (
{!queryFields && } + {(entityDataType?.supportAllValueInQuery && altQueryName !== SAMPLE_PROPERTY_ALL_SAMPLE_TYPE.query) && ( +
+ onAllValuesInQueryChange(event.target.checked)} + type="checkbox" + /> +
+ All {entityDataType.nounAsParentPlural ?? entityDataType.nounPlural} +
+
+ )} {entityDataType?.supportHasNoValueInQuery && (
{ LABKEY.container = { @@ -50,14 +50,14 @@ const anyValueFilter = { fieldCaption: 'textField', filter: Filter.create('textField', null, Filter.Types.HAS_ANY_VALUE), jsonType: 'string', -} as FieldFilter; +} as EntityFieldFilter; const matchesAllFilter = { fieldKey: 'textField', fieldCaption: 'textField', filter: Filter.create('textField', ['a', 'b'], ANCESTOR_MATCHES_ALL_OF_FILTER_TYPE), jsonType: 'string', -} as FieldFilter; +} as EntityFieldFilter; const matchesAllBadFilter = { fieldKey: 'textField', @@ -68,14 +68,14 @@ const matchesAllBadFilter = { ANCESTOR_MATCHES_ALL_OF_FILTER_TYPE ), jsonType: 'string', -} as FieldFilter; +} as EntityFieldFilter; const matchesAllEmptyFilter = { fieldKey: 'textField', fieldCaption: 'textField', filter: Filter.create('textField', [], ANCESTOR_MATCHES_ALL_OF_FILTER_TYPE), jsonType: 'string', -} as FieldFilter; +} as EntityFieldFilter; const containsOneOfBadFilter = { fieldKey: 'textField', @@ -86,7 +86,7 @@ const containsOneOfBadFilter = { Filter.Types.CONTAINS_ONE_OF ), jsonType: 'string', -} as FieldFilter; +} as EntityFieldFilter; const containsNoneOfBadFilter = { fieldKey: 'otherTextField', @@ -97,7 +97,7 @@ const containsNoneOfBadFilter = { Filter.Types.CONTAINS_NONE_OF ), jsonType: 'string', -} as FieldFilter; +} as EntityFieldFilter; const equalsNoneOfBadFilter = { fieldKey: 'textField', @@ -108,63 +108,63 @@ const equalsNoneOfBadFilter = { Filter.Types.EQUALS_NONE_OF ), jsonType: 'string', -} as FieldFilter; +} as EntityFieldFilter; const intEqFilter = { fieldKey: 'intField', fieldCaption: 'intField', filter: Filter.create('intField', 1), jsonType: 'int', -} as FieldFilter; +} as EntityFieldFilter; const stringBetweenFilter = { fieldKey: 'strField', fieldCaption: 'strField', filter: Filter.create('strField', ['1', '5'], Filter.Types.BETWEEN), jsonType: 'string', -} as FieldFilter; +} as EntityFieldFilter; const stringEqualFilter = { fieldKey: 'strField', fieldCaption: 'strField', filter: Filter.create('strField', '2', Filter.Types.EQ), jsonType: 'string', -} as FieldFilter; +} as EntityFieldFilter; const emptyStringBetweenFilter = { fieldKey: 'strField', fieldCaption: 'strField', filter: Filter.create('strField', ['', '5'], Filter.Types.BETWEEN), jsonType: 'string', -} as FieldFilter; +} as EntityFieldFilter; const emptyStringLessThanFilter = { fieldKey: 'strField', fieldCaption: 'strField', filter: Filter.create('strField', '', Filter.Types.LT), jsonType: 'string', -} as FieldFilter; +} as EntityFieldFilter; const floatBetweenFilter = { fieldKey: 'floatField2', fieldCaption: 'floatField2', filter: Filter.create('floatField2', '1,5', Filter.Types.BETWEEN), jsonType: 'float', -} as FieldFilter; +} as EntityFieldFilter; const badIntFilter = { fieldKey: 'intField', fieldCaption: 'intField', filter: Filter.create('intField', null), jsonType: 'int', -} as FieldFilter; +} as EntityFieldFilter; const badBetweenFilter = { fieldKey: 'doubleField', fieldCaption: 'doubleField', filter: Filter.create('doubleField', '1', Filter.Types.BETWEEN), jsonType: 'float', -} as FieldFilter; +} as EntityFieldFilter; describe('getFilterValuesAsArray', () => { test('array value', () => { diff --git a/packages/components/src/internal/components/search/utils.ts b/packages/components/src/internal/components/search/utils.ts index cf5e9cab4b..43798c0e3f 100644 --- a/packages/components/src/internal/components/search/utils.ts +++ b/packages/components/src/internal/components/search/utils.ts @@ -21,7 +21,7 @@ import { REGISTRY_KEY } from '../../app/constants'; import { makeCommaSeparatedString } from '../../util/utils'; import { SearchCategory, SearchScope } from './constants'; -import { FieldFilter, FieldFilterOption, FilterSelection, SearchResultCardData } from './models'; +import { EntityFieldFilter, FieldFilterOption, FilterSelection, SearchResultCardData } from './models'; export const SAMPLE_FILTER_METRIC_AREA = 'sampleFinder'; @@ -131,7 +131,7 @@ export function getFilterValuesAsArray(filter: Filter.IFilter, blankValue?: stri } export function getFieldFiltersValidationResult( - dataTypeFilters: Record, + dataTypeFilters: Record, queryLabels?: Record, maxMultiValuedValues: number = MAX_MULTI_VALUE_FILTER_VALUES ): string { diff --git a/packages/components/src/public/QueryModel/FilterStatus.test.tsx b/packages/components/src/public/QueryModel/FilterStatus.test.tsx index 04277e728e..ffccbf784f 100644 --- a/packages/components/src/public/QueryModel/FilterStatus.test.tsx +++ b/packages/components/src/public/QueryModel/FilterStatus.test.tsx @@ -33,6 +33,14 @@ describe('FilterStatus', () => { value: 'test2', valueObject: Filter.create('A', undefined, Filter.Types.NONBLANK), }; + const filterAction1Locked = { + ...filterAction1, + isReadOnly: 'locked filter' + }; + const filterAction2Locked = { + ...filterAction2, + isReadOnly: 'locked filter' + }; const searchAction = { action: new SearchAction(), value: 'foo', @@ -94,4 +102,29 @@ describe('FilterStatus', () => { await userEvent.click(document.querySelector('.remove-all-filters')); expect(ON_REMOVE_ALL).toHaveBeenCalledTimes(1); }); + + test('multiple locked filter actionValue', async () => { + render(); + validate(2, 2); + expect(document.querySelectorAll('.fa-table')).toHaveLength(0); + expect(document.querySelectorAll('.fa-search')).toHaveLength(0); + expect(document.querySelectorAll('.fa-filter')).toHaveLength(2); + expect(document.querySelectorAll('.filter-status-value')[0].textContent).toBe('test1'); + expect(document.querySelectorAll('.filter-status-value')[1].textContent).toBe('test2'); + expect(document.querySelectorAll('.fa-close')).toHaveLength(0); + expect(document.querySelectorAll('.remove-all-filters')).toHaveLength(1); + }); + + test('multiple locked filter actionValue, cannot remove', async () => { + render(); + expect(document.querySelectorAll('.fa-table')).toHaveLength(0); + expect(document.querySelectorAll('.fa-search')).toHaveLength(0); + expect(document.querySelectorAll('.fa-filter')).toHaveLength(0); + expect(document.querySelectorAll('.fa-lock')).toHaveLength(2); + expect(document.querySelectorAll('.filter-status-value')[0].textContent).toBe('test1'); + expect(document.querySelectorAll('.filter-status-value')[1].textContent).toBe('test2'); + expect(document.querySelectorAll('.fa-close')).toHaveLength(0); + expect(document.querySelectorAll('.remove-all-filters')).toHaveLength(0); + }); + }); diff --git a/packages/components/src/public/QueryModel/FilterStatus.tsx b/packages/components/src/public/QueryModel/FilterStatus.tsx index f2e6b81cc0..57aad1ae1d 100644 --- a/packages/components/src/public/QueryModel/FilterStatus.tsx +++ b/packages/components/src/public/QueryModel/FilterStatus.tsx @@ -9,11 +9,12 @@ interface Props { onClick: (actionValue: ActionValue, event: any) => void; onRemove: (actionValueIndex: number, event: any) => void; onRemoveAll?: () => void; + lockReadOnlyForDelete?: boolean; } export const FilterStatus: FC = memo(props => { - const { actionValues, onClick, onRemove, onRemoveAll } = props; - const showRemoveAll = filterActionValuesByType(actionValues, 'filter').length > 1; + const { actionValues, onClick, onRemove, onRemoveAll, lockReadOnlyForDelete } = props; + const showRemoveAll = filterActionValuesByType(actionValues, 'filter', lockReadOnlyForDelete).length > 1; return (
@@ -50,6 +51,7 @@ export const FilterStatus: FC = memo(props => { actionValue={actionValue} onClick={_onClick} onRemove={_onRemove} + lockReadOnlyForDelete={lockReadOnlyForDelete} /> ); })} diff --git a/packages/components/src/public/QueryModel/GridFilterModal.tsx b/packages/components/src/public/QueryModel/GridFilterModal.tsx index 8443f75da5..39a8fdedd2 100644 --- a/packages/components/src/public/QueryModel/GridFilterModal.tsx +++ b/packages/components/src/public/QueryModel/GridFilterModal.tsx @@ -2,7 +2,7 @@ import React, { FC, memo, useCallback, useMemo, useState } from 'react'; import { Filter, Query } from '@labkey/api'; import { Modal } from '../../internal/Modal'; -import { FieldFilter } from '../../internal/components/search/models'; +import { EntityFieldFilter } from '../../internal/components/search/models'; import { QueryColumn } from '../QueryColumn'; import { Alert } from '../../internal/components/base/Alert'; @@ -38,12 +38,12 @@ export const GridFilterModal: FC = memo(props => { } = props; const { queryInfo } = model; const [filterError, setFilterError] = useState(undefined); - const [filters, setFilters] = useState( + const [filters, setFilters] = useState( initFilters.map(filter => { return { fieldKey: filter.getColumnName(), filter, - } as FieldFilter; + } as EntityFieldFilter; }) ); @@ -85,7 +85,7 @@ export const GridFilterModal: FC = memo(props => { fieldCaption: field.caption, filter: newFilter, jsonType: field.getDisplayFieldJsonType(), - } as FieldFilter); + } as EntityFieldFilter); }); } diff --git a/packages/components/src/public/QueryModel/grid/Value.tsx b/packages/components/src/public/QueryModel/grid/Value.tsx index 17ae0105fc..1185e361ee 100644 --- a/packages/components/src/public/QueryModel/grid/Value.tsx +++ b/packages/components/src/public/QueryModel/grid/Value.tsx @@ -23,6 +23,7 @@ interface ValueProps { index: number; onClick?: (actionValue: ActionValue, event: any) => void; onRemove?: (actionValueIndex: number, event: any) => void; + lockReadOnlyForDelete?: boolean; } interface ValueState { @@ -80,7 +81,7 @@ export class Value extends React.Component { }; render(): ReactNode { - const { actionValue } = this.props; + const { actionValue, lockReadOnlyForDelete } = this.props; const { action, value, displayValue, isReadOnly, isRemovable } = actionValue; const showRemoveIcon = this.state.isActive && isRemovable !== false && actionValue.action.keyword !== 'view'; @@ -103,7 +104,7 @@ export class Value extends React.Component { onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave} > - + {(!lockReadOnlyForDelete || !isReadOnly) && } {isReadOnly ? : null} {displayValue ?? value}
diff --git a/packages/components/src/public/QueryModel/grid/actions/Filter.test.ts b/packages/components/src/public/QueryModel/grid/actions/Filter.test.ts index 3172249a41..2ce4a5a92f 100644 --- a/packages/components/src/public/QueryModel/grid/actions/Filter.test.ts +++ b/packages/components/src/public/QueryModel/grid/actions/Filter.test.ts @@ -19,8 +19,10 @@ import { QueryColumn } from '../../../QueryColumn'; import { TIME_RANGE_URI } from '../../../../internal/components/domainproperties/constants'; -import { FilterAction } from './Filter'; +import { FilterAction, removeFilterValueForFilterProps } from './Filter'; import { ActionValue } from './Action'; +import { EntityFieldFilter } from '../../../../internal/components/search/models'; +import { FilterProps } from '../../../../internal/components/entities/models'; describe('FilterAction::actionValueFromFilter', () => { const action = new FilterAction(); @@ -94,3 +96,124 @@ describe('FilterAction::actionValueFromFilter', () => { expect(value.value).toBe('"$BoolField./,$&" = true'); }); }); + + +describe('actionValueFromEntityFieldFilter', () => { + const action = new FilterAction(); + + it('returns correct ActionValue for date field with formatted value', () => { + const entityFieldFilter: EntityFieldFilter = { + fieldCaption: 'DateField', + filter: Filter.create('DateField', '10-01-2023', Filter.Types.EQUAL), + jsonType: 'date', + } as EntityFieldFilter; + const value = action.actionValueFromEntityFieldFilter(entityFieldFilter); + expect(value.displayValue).toBe('DateField = 2023-10-01'); + expect(value.value).toBe('"DateField" = 2023-10-01'); + }); + + it('returns correct ActionValue for time field with formatted value', () => { + const entityFieldFilter: EntityFieldFilter = { + fieldCaption: 'TimeField', + filter: Filter.create('TimeField', '12:30:00.123', Filter.Types.EQUAL), + jsonType: 'time', + } as EntityFieldFilter; + const value = action.actionValueFromEntityFieldFilter(entityFieldFilter); + expect(value.displayValue).toBe('TimeField = 12:30'); + expect(value.value).toBe('"TimeField" = 12:30'); + }); + + it('returns correct ActionValue for string field', () => { + const entityFieldFilter: EntityFieldFilter = { + fieldCaption: 'StringField', + filter: Filter.create('StringField', 'testValue', Filter.Types.EQUAL), + } as EntityFieldFilter; + const value = action.actionValueFromEntityFieldFilter(entityFieldFilter); + expect(value.displayValue).toBe('StringField = testValue'); + expect(value.value).toBe('"StringField" = testValue'); + }); + + it('handles missing fieldCaption gracefully', () => { + const entityFieldFilter: EntityFieldFilter = { + filter: Filter.create('UnnamedField', 'value', Filter.Types.EQUAL), + } as EntityFieldFilter; + const value = action.actionValueFromEntityFieldFilter(entityFieldFilter); + expect(value.displayValue).toBe('UnnamedField = value'); + expect(value.value).toBe('"UnnamedField" = value'); + }); + + it('handles null filter value', () => { + const entityFieldFilter: EntityFieldFilter = { + fieldCaption: 'NullField', + filter: Filter.create('NullField', null, Filter.Types.ISBLANK), + } as EntityFieldFilter; + const value = action.actionValueFromEntityFieldFilter(entityFieldFilter); + expect(value.displayValue).toBe('NullField Is Blank'); + expect(value.value).toBe('"NullField" Is Blank null'); + }); +}); + +describe('removeFilterValueForFilterProps', () => { + const filter1 = Filter.create('PropA', 'a'); + const filter2 = Filter.create('PropB', 'b'); + const filter3 = Filter.create('PropB', 'c'); + + const entityFilterProps: FilterProps = { + schemaQuery: { schemaName: 'schema', queryName: 'query' }, + filterArray: [{ filter: filter2 }, { filter: filter1 }, { filter: filter3 }], + } as FilterProps; + const actionValues: ActionValue[] = [ + { + action: new FilterAction(), + displayValue: 'PropA = a', + value: '"PropA" = a', + valueObject: filter1, + }, + { + action: new FilterAction(), + displayValue: 'PropB = b', + value: '"PropB" = b', + valueObject: filter2, + }, + { + action: new FilterAction(), + displayValue: 'PropC = c', + value: '"PropC" = c', + valueObject: filter3, + }, + ]; + + it('removes the filter value when it exists in the filter array', () => { + const updatedFilters = removeFilterValueForFilterProps(entityFilterProps, actionValues, 0); + expect(updatedFilters.length).toBe(2); + expect(updatedFilters[0].filter).toBe(filter2); + expect(updatedFilters[1].filter).toBe(filter3); + }); + + it('does not modify the filter array when the value does not exist', () => { + const nonMatchingFilter = Filter.create('OtherField', 'otherValue', Filter.Types.EQUAL); + const nonMatchingActionValues: ActionValue[] = [ + { + action: new FilterAction(), + displayValue: 'OtherField = otherValue', + value: '"OtherField" = otherValue', + valueObject: nonMatchingFilter, + }, + ]; + const updatedFilters = removeFilterValueForFilterProps(entityFilterProps, nonMatchingActionValues, 0); + expect(updatedFilters.length).toBe(3); + expect(updatedFilters[0].filter).toBe(filter2); + }); + + it('handles an empty filter array gracefully', () => { + const emptyFilterProps: FilterProps = { schemaQuery: { schemaName: 'schema', queryName: 'query' }, filterArray: [] } as FilterProps; + const updatedFilters = removeFilterValueForFilterProps(emptyFilterProps, actionValues, 0); + expect(updatedFilters.length).toBe(0); + }); + + it('does not throw an error when valueIndex is out of bounds', () => { + const updatedFilters = removeFilterValueForFilterProps(entityFilterProps, actionValues, 3); + expect(updatedFilters.length).toBe(3); + expect(updatedFilters[0].filter).toBe(filter2); + }); +}); diff --git a/packages/components/src/public/QueryModel/grid/actions/Filter.ts b/packages/components/src/public/QueryModel/grid/actions/Filter.ts index 2dfcca1c10..4bd9a28bdf 100644 --- a/packages/components/src/public/QueryModel/grid/actions/Filter.ts +++ b/packages/components/src/public/QueryModel/grid/actions/Filter.ts @@ -27,6 +27,9 @@ import { QueryColumn } from '../../../QueryColumn'; import { ANCESTOR_MATCHES_ALL_OF_FILTER_TYPE } from '../../../../internal/query/filter'; import { Action, ActionValue } from './Action'; +import { FilterProps } from '../../../../internal/components/entities/models'; +import { EntityFieldFilter } from '../../../../internal/components/search/models'; +import { filtersEqual } from '../../utils'; /** * The following section prepares the SYMBOL_MAP and SUFFIX_MAP to allow any Filter Action instances @@ -146,6 +149,42 @@ function resolveSymbol(filterType: Filter.IFilterType): string { return getURLSuffix(filterType); } +export function getActionValuesForFilterProps(entityFilterProps: FilterProps, queryTypeLabel: string, isReadOnly?: string): ActionValue[] { + const action = new FilterAction(); + const actionValues: ActionValue[] = []; + if (!entityFilterProps || !entityFilterProps.schemaQuery) + return null + if (!entityFilterProps.filterArray || entityFilterProps.filterArray.length === 0) { + const filterValue = `${queryTypeLabel} = ${entityFilterProps.dataTypeDisplayName}`; + actionValues.push({ + action, + displayValue: filterValue, + isReadOnly, + value: filterValue, + valueObject: null, + }); + + } else { + entityFilterProps.filterArray.forEach(filter => { + actionValues.push(action.actionValueFromEntityFieldFilter(filter, isReadOnly)); + }) + } + + return actionValues; +} + +export function removeFilterValueForFilterProps(entityFilterProps: FilterProps, actionValues: ActionValue[], valueIndex: number): EntityFieldFilter[] { + const value = actionValues[valueIndex]?.valueObject; + if (!value) + return entityFilterProps.filterArray; + const viewFilterIndex = entityFilterProps.filterArray.findIndex(f => filtersEqual(f.filter, value)) ?? -1; + const updatedFilterArray = [...entityFilterProps.filterArray]; + if (viewFilterIndex > -1) { + updatedFilterArray.splice(viewFilterIndex, 1) + } + return updatedFilterArray; +} + export class FilterAction implements Action { iconCls = 'filter'; keyword = 'filter'; @@ -208,19 +247,10 @@ export class FilterAction implements Action { return { displayValue: displayParts.join(' '), inputValue }; } - actionValueFromFilter(filter: Filter.IFilter, column?: QueryColumn, isReadOnly?: string): ActionValue { - const label = column?.shortCaption; + _actionValueFromFilter(filter: Filter.IFilter, value: any, label: string, isReadOnly?: string): ActionValue { const columnName = decodePart(filter.getColumnName()); const filterType = filter.getFilterType(); const operator = resolveSymbol(filter.getFilterType()); - let value = filter.getValue(); - - // Issue 45140: match date display format in grid filter status pill display - if (column?.isTimeColumn) { - value = getColFormattedTimeFilterValue(column, value); - } else if (column?.getDisplayFieldJsonType() === 'date') { - value = getColFormattedDateFilterValue(column, value); - } const { displayValue, inputValue } = this.getDisplayValue(label ?? columnName, filterType, value); @@ -232,4 +262,33 @@ export class FilterAction implements Action { valueObject: filter, }; } + + actionValueFromFilter(filter: Filter.IFilter, column?: QueryColumn, isReadOnly?: string): ActionValue { + const label = column?.shortCaption; + let value = filter.getValue(); + + // Issue 45140: match date display format in grid filter status pill display + if (column?.isTimeColumn) { + value = getColFormattedTimeFilterValue(column, value); + } else if (column?.getDisplayFieldJsonType() === 'date') { + value = getColFormattedDateFilterValue(column, value); + } + + return this._actionValueFromFilter(filter, value, label, isReadOnly); + } + + actionValueFromEntityFieldFilter(entityFieldFilter: EntityFieldFilter, isReadOnly?: string): ActionValue { + const label = entityFieldFilter.fieldCaption; + const filter = entityFieldFilter.filter; + let value = filter.getValue(); + + // use folder level display format since queryColumn is not available + if (entityFieldFilter?.jsonType === 'time') { + value = getColFormattedTimeFilterValue(null, value); + } else if (entityFieldFilter?.jsonType === 'date') { + value = getColFormattedDateFilterValue(null, value); + } + + return this._actionValueFromFilter(filter, value, label, isReadOnly); + } } diff --git a/packages/components/src/public/QueryModel/grid/utils.test.ts b/packages/components/src/public/QueryModel/grid/utils.test.ts index c1a9fc88c5..9fb45e2b27 100644 --- a/packages/components/src/public/QueryModel/grid/utils.test.ts +++ b/packages/components/src/public/QueryModel/grid/utils.test.ts @@ -3,10 +3,11 @@ import { Filter } from '@labkey/api'; import { QueryInfo } from '../../QueryInfo'; -import { getSearchValueAction } from './utils'; +import { filterActionValuesByType, getSearchValueAction } from './utils'; import { ChangeType } from './model'; import { FilterAction } from './actions/Filter'; import { SearchAction } from './actions/Search'; +import { ActionValue } from './actions/Action'; const filterAction = { action: new FilterAction( @@ -39,3 +40,49 @@ describe('replaceSearchValue', () => { expect(change.type).toBe(ChangeType.remove); }); }); + +describe('filterActionValuesByType', () => { + const mockActionValues: ActionValue[] = [ + { + action: { keyword: 'search' }, + }, + { + action: { keyword: 'filter' }, + }, + { + action: { keyword: 'filter' }, + isReadOnly: 'locked', + }, + ]; + + it('returns only action values matching the keyword', () => { + const result = filterActionValuesByType(mockActionValues, 'search', false); + expect(result.length).toBe(1); + expect(result.every(av => av.action.keyword === 'search')).toBe(true); + }); + + it('excludes read-only action values when lockReadOnlyForDelete is true', () => { + const result = filterActionValuesByType(mockActionValues, 'search', true); + expect(result.length).toBe(1); + expect(result[0].isReadOnly).toBeUndefined(); + }); + + it('returns an empty array when no action values match the keyword', () => { + const result = filterActionValuesByType(mockActionValues, 'nonexistent', false); + expect(result.length).toBe(0); + }); + + it('returns an empty array when all matching action values are read-only and lockReadOnlyForDelete is true', () => { + const result = filterActionValuesByType(mockActionValues, 'filter', true); + expect(result.length).toBe(1); + expect(result[0].isReadOnly).toBeUndefined(); + }); + + it('returns all matching action values when lockReadOnlyForDelete is false', () => { + const result = filterActionValuesByType(mockActionValues, 'filter', false); + expect(result.length).toBe(2); + expect(result[0].isReadOnly).toBeUndefined(); + expect(result[1].isReadOnly).toBe('locked'); + }); +}); + diff --git a/packages/components/src/public/QueryModel/grid/utils.ts b/packages/components/src/public/QueryModel/grid/utils.ts index 252915e0e8..f9982cd9a3 100644 --- a/packages/components/src/public/QueryModel/grid/utils.ts +++ b/packages/components/src/public/QueryModel/grid/utils.ts @@ -65,6 +65,8 @@ export function getSearchValueAction(actionValues: ActionValue[], value: string) return change; } -export function filterActionValuesByType(actionValues: ActionValue[], keyword: string): ActionValue[] { - return actionValues.filter(actionValue => actionValue.action.keyword === keyword); +export function filterActionValuesByType(actionValues: ActionValue[], keyword: string, lockReadOnlyForDelete: boolean): ActionValue[] { + return actionValues + .filter(actionValue => actionValue.action.keyword === keyword) + .filter(actionValue => !actionValue.isReadOnly || !lockReadOnlyForDelete); } From 81d7e45460756d30b1ae472652d76df6c01542d2 Mon Sep 17 00:00:00 2001 From: XingY Date: Sat, 13 Dec 2025 17:27:10 -0800 Subject: [PATCH 02/12] Workflow Automation: Task action to filter samples for selected task --- packages/components/package-lock.json | 4 +-- packages/components/package.json | 2 +- .../components/releaseNotes/components.md | 5 +++ packages/components/src/index.ts | 22 ++++++------ .../internal/components/entities/models.ts | 14 ++++---- .../search/QueryFilterPanel.test.tsx | 36 +++++++------------ .../components/search/QueryFilterPanel.tsx | 31 ++++++++-------- .../public/QueryModel/FilterStatus.test.tsx | 13 ++++--- .../src/public/QueryModel/FilterStatus.tsx | 8 ++--- .../src/public/QueryModel/GridFilterModal.tsx | 2 +- .../src/public/QueryModel/grid/Value.tsx | 2 +- .../QueryModel/grid/actions/Filter.test.ts | 6 ++-- .../public/QueryModel/grid/actions/Filter.ts | 23 +++++++----- .../src/public/QueryModel/grid/utils.test.ts | 1 - .../src/public/QueryModel/grid/utils.ts | 6 +++- 15 files changed, 92 insertions(+), 83 deletions(-) diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index 442dd5c57b..c6d821574a 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "7.3.1", + "version": "7.4.0-fb-jobTaskFilter.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "7.3.1", + "version": "7.4.0-fb-jobTaskFilter.1", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", diff --git a/packages/components/package.json b/packages/components/package.json index 94c8910ad6..5a61e6bfbc 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "7.3.1", + "version": "7.4.0-fb-jobTaskFilter.1", "description": "Components, models, actions, and utility functions for LabKey applications and pages", "sideEffects": false, "files": [ diff --git a/packages/components/releaseNotes/components.md b/packages/components/releaseNotes/components.md index eebfc5ab91..b7f12ff698 100644 --- a/packages/components/releaseNotes/components.md +++ b/packages/components/releaseNotes/components.md @@ -1,6 +1,11 @@ # @labkey/components Components, models, actions, and utility functions for LabKey applications and pages +### version 7.X +*Released*: x December 2025 +- Workflow Automation: Task action to filter samples for selected task + - TODO + ### version 7.3.1 *Released*: 13 December 2025 - Remove LSID column from provisioned sample tables diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index 8e95f6158c..ec79b1bf91 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -625,7 +625,11 @@ import { TabbedGridPanel } from './public/QueryModel/TabbedGridPanel'; import { DetailPanel, DetailPanelWithModel } from './public/QueryModel/DetailPanel'; import { makeTestActions, makeTestQueryModel } from './public/QueryModel/testUtils'; import { FilterStatus } from './public/QueryModel/FilterStatus'; -import { FilterAction, getActionValuesForFilterProps, removeFilterValueForFilterProps } from './public/QueryModel/grid/actions/Filter'; +import { + FilterAction, + getActionValuesForFilterProps, + removeFilterValueForFilterProps, +} from './public/QueryModel/grid/actions/Filter'; import { BACKGROUND_IMPORT_MIN_FILE_SIZE, @@ -1232,10 +1236,6 @@ export { DetailPanel, DetailPanelHeader, DetailPanelWithModel, - FilterStatus, - FilterAction, - getActionValuesForFilterProps, - removeFilterValueForFilterProps, DisableableAnchor, DisableableButton, DisableableMenuItem, @@ -1294,7 +1294,9 @@ export { FileColumnRenderer, FileInput, FileTree, + FilterAction, FilterCriteriaRenderer, + FilterStatus, FIND_BY_IDS_QUERY_PARAM, FindByIdsModal, FindDerivativesButton, @@ -1322,6 +1324,7 @@ export { generateId, generateNameWithTimestamp, getActionErrorMessage, + getActionValuesForFilterProps, getAltUnitKeys, getAssayDefinitions, getAuditQueries, @@ -1604,6 +1607,7 @@ export { removeColumn, removeColumns, RemoveEntityButton, + removeFilterValueForFilterProps, removeParameters, renderWithAppContext, replaceParameters, @@ -1904,9 +1908,11 @@ export type { QueryParams } from './internal/util/URL'; export type { FileSizeLimitProps } from './public/files/models'; export type { ImportTemplate } from './public/QueryInfo'; export type { EditableDetailPanelProps } from './public/QueryModel/EditableDetailPanel'; +export type { Action, ActionValue } from './public/QueryModel/grid/actions/Action'; export type { QueryConfig } from './public/QueryModel/QueryModel'; export type { QueryModelLoader } from './public/QueryModel/QueryModelLoader'; export type { TabbedGridPanelProps } from './public/QueryModel/TabbedGridPanel'; + // Due to babel-loader & typescript babel plugins we need to export/import types separately. The babel plugins require // the typescript compiler option "isolatedModules", which do not export types from modules, so types must be exported // separately. @@ -1919,9 +1925,3 @@ export type { QueryModelMap, RequiresModelAndActions, } from './public/QueryModel/withQueryModels'; - -export type { - Action, - ActionValue -} from './public/QueryModel/grid/actions/Action' - diff --git a/packages/components/src/internal/components/entities/models.ts b/packages/components/src/internal/components/entities/models.ts index 458f3df26e..fbf53a6913 100644 --- a/packages/components/src/internal/components/entities/models.ts +++ b/packages/components/src/internal/components/entities/models.ts @@ -41,7 +41,7 @@ export interface IDerivePayload { materialDefault?: any; materialInputs?: EntityInputProps[]; materialOutputCount?: number; - materialOutputs?: Array<{ [key: string]: any }>; + materialOutputs?: Record[]; targetType: string; } @@ -434,14 +434,14 @@ export interface IEntityTypeDetails extends IEntityDetails { importAliasValues?: string[]; } -export type SampleFinderCardType = 'sampleproperty' | 'sampleparent' | 'dataclassparent' | 'assaydata'; +export type SampleFinderCardType = 'assaydata' | 'dataclassparent' | 'sampleparent' | 'sampleproperty'; export type FolderConfigurableDataType = - | 'SampleType' + | 'AssayDesign' + | 'Container' | 'DashboardSampleType' | 'DataClass' - | 'AssayDesign' - | 'StorageLocation' - | 'Container'; + | 'SampleType' + | 'StorageLocation'; /** * Avoid inline comment or above line comments for properties due to es-lint's limitation on moving comments: @@ -507,6 +507,7 @@ export interface EntityDataType { operationConfirmationActionName: string; operationConfirmationControllerName: string; sampleFinderCardType?: SampleFinderCardType; + supportAllValueInQuery?: boolean; supportHasNoValueInQuery?: boolean; supportsCrossTypeImport?: boolean; typeIcon?: string; @@ -514,7 +515,6 @@ export interface EntityDataType { typeNounAsParentSingular: string; typeNounSingular: string; uniqueFieldKey: string; - supportAllValueInQuery?: boolean; } interface OperationContainerInfo { diff --git a/packages/components/src/internal/components/search/QueryFilterPanel.test.tsx b/packages/components/src/internal/components/search/QueryFilterPanel.test.tsx index 15c1941a55..296219adb0 100644 --- a/packages/components/src/internal/components/search/QueryFilterPanel.test.tsx +++ b/packages/components/src/internal/components/search/QueryFilterPanel.test.tsx @@ -27,14 +27,10 @@ describe('QueryFilterPanel', () => { const TestAllowAllEntityType = { ...SamplePropertyDataType, - supportAllValueInQuery: true + supportAllValueInQuery: true, } as EntityDataType; - function validate( - fieldItems: number, - showFilterExpression = false, - showChooseValues = false - ): void { + function validate(fieldItems: number, showFilterExpression = false, showChooseValues = false): void { expect(document.querySelectorAll('.filter-modal__col_fields')).toHaveLength(1); expect(document.querySelectorAll('.filter-modal__col_filter_exp')).toHaveLength(1); // ChoicesListItem renders as button.list-group-item @@ -71,7 +67,7 @@ describe('QueryFilterPanel', () => { test('no queryName emptyMsg', () => { const { unmount } = render( - + ); validate(0); expect(document.querySelectorAll('.field-modal__empty-msg')).toHaveLength(1); @@ -173,8 +169,8 @@ describe('QueryFilterPanel', () => { const { unmount } = render( ); @@ -191,8 +187,8 @@ describe('QueryFilterPanel', () => { const { unmount } = render( ); @@ -209,9 +205,8 @@ describe('QueryFilterPanel', () => { const { unmount } = render( { } as EntityFieldFilter, ], }} + hasNotInQueryFilter={true} /> ); expect(document.querySelector('.filter-modal__fields-col-nodata-msg')!.textContent).toBe( @@ -233,18 +229,12 @@ describe('QueryFilterPanel', () => { test('supportAllValueInQuery checkbox, not checked', () => { const { unmount } = render( - + ); validate(10); - expect(document.querySelector('.filter-modal__fields-col-any-msg')!.textContent).toBe( - 'All Samples' - ); + expect(document.querySelector('.filter-modal__fields-col-any-msg')!.textContent).toBe('All Samples'); // query by name field-value-nodata-check - expect(document.querySelector('input[name="field-value-allvalues-check"]').getAttribute('checked')).toBe(null) + expect(document.querySelector('input[name="field-value-allvalues-check"]').getAttribute('checked')).toBe(null); expect(document.querySelectorAll('.field-modal__col-content-disabled')).toHaveLength(0); unmount(); @@ -254,15 +244,13 @@ describe('QueryFilterPanel', () => { const { unmount } = render( ); validate(10); - expect(document.querySelector('.filter-modal__fields-col-any-msg')!.textContent).toBe( - 'All Samples' - ); + expect(document.querySelector('.filter-modal__fields-col-any-msg')!.textContent).toBe('All Samples'); expect(document.querySelector('input[name="field-value-allvalues-check"]').getAttribute('checked')).toBe(''); expect(document.querySelectorAll('.field-modal__col-content-disabled')).toHaveLength(0); diff --git a/packages/components/src/internal/components/search/QueryFilterPanel.tsx b/packages/components/src/internal/components/search/QueryFilterPanel.tsx index 655b0a152a..4d35ec3f98 100644 --- a/packages/components/src/internal/components/search/QueryFilterPanel.tsx +++ b/packages/components/src/internal/components/search/QueryFilterPanel.tsx @@ -44,14 +44,14 @@ interface Props { fields?: QueryColumn[]; filters: Record; fullWidth?: boolean; + hasAllValuesInQuery?: boolean; hasNotInQueryFilter?: boolean; hasNotInQueryFilterLabel?: string; isAncestor?: boolean; metricFeatureArea?: string; + onAllValuesInQueryChange?: (check: boolean) => void; onFilterUpdate: (field: QueryColumn, newFilters: Filter.IFilter[], index: number) => void; - hasAllValuesInQuery?: boolean; onHasNoValueInQueryChange?: (check: boolean) => void; - onAllValuesInQueryChange?: (check: boolean) => void; queryInfo: QueryInfo; selectDistinctOptions?: Partial; skipDefaultViewCheck?: boolean; @@ -247,20 +247,21 @@ export const QueryFilterPanel: FC = memo(props => { {queryName && (
{!queryFields && } - {(entityDataType?.supportAllValueInQuery && altQueryName !== SAMPLE_PROPERTY_ALL_SAMPLE_TYPE.query) && ( -
- onAllValuesInQueryChange(event.target.checked)} - type="checkbox" - /> -
- All {entityDataType.nounAsParentPlural ?? entityDataType.nounPlural} + {entityDataType?.supportAllValueInQuery && + altQueryName !== SAMPLE_PROPERTY_ALL_SAMPLE_TYPE.query && ( +
+ onAllValuesInQueryChange(event.target.checked)} + type="checkbox" + /> +
+ All {entityDataType.nounAsParentPlural ?? entityDataType.nounPlural} +
-
- )} + )} {entityDataType?.supportHasNoValueInQuery && (
{ }; const filterAction1Locked = { ...filterAction1, - isReadOnly: 'locked filter' + isReadOnly: 'locked filter', }; const filterAction2Locked = { ...filterAction2, - isReadOnly: 'locked filter' + isReadOnly: 'locked filter', }; const searchAction = { action: new SearchAction(), @@ -116,7 +116,13 @@ describe('FilterStatus', () => { }); test('multiple locked filter actionValue, cannot remove', async () => { - render(); + render( + + ); expect(document.querySelectorAll('.fa-table')).toHaveLength(0); expect(document.querySelectorAll('.fa-search')).toHaveLength(0); expect(document.querySelectorAll('.fa-filter')).toHaveLength(0); @@ -126,5 +132,4 @@ describe('FilterStatus', () => { expect(document.querySelectorAll('.fa-close')).toHaveLength(0); expect(document.querySelectorAll('.remove-all-filters')).toHaveLength(0); }); - }); diff --git a/packages/components/src/public/QueryModel/FilterStatus.tsx b/packages/components/src/public/QueryModel/FilterStatus.tsx index 57aad1ae1d..b4cec32b46 100644 --- a/packages/components/src/public/QueryModel/FilterStatus.tsx +++ b/packages/components/src/public/QueryModel/FilterStatus.tsx @@ -6,10 +6,10 @@ import { filterActionValuesByType } from './grid/utils'; interface Props { actionValues: ActionValue[]; + lockReadOnlyForDelete?: boolean; onClick: (actionValue: ActionValue, event: any) => void; onRemove: (actionValueIndex: number, event: any) => void; onRemoveAll?: () => void; - lockReadOnlyForDelete?: boolean; } export const FilterStatus: FC = memo(props => { @@ -46,12 +46,12 @@ export const FilterStatus: FC = memo(props => { return ( ); })} diff --git a/packages/components/src/public/QueryModel/GridFilterModal.tsx b/packages/components/src/public/QueryModel/GridFilterModal.tsx index 39a8fdedd2..dcc20b4781 100644 --- a/packages/components/src/public/QueryModel/GridFilterModal.tsx +++ b/packages/components/src/public/QueryModel/GridFilterModal.tsx @@ -105,8 +105,8 @@ export const GridFilterModal: FC = memo(props => { > {filterError} void; onRemove?: (actionValueIndex: number, event: any) => void; - lockReadOnlyForDelete?: boolean; } interface ValueState { diff --git a/packages/components/src/public/QueryModel/grid/actions/Filter.test.ts b/packages/components/src/public/QueryModel/grid/actions/Filter.test.ts index 2ce4a5a92f..2e259e6bd9 100644 --- a/packages/components/src/public/QueryModel/grid/actions/Filter.test.ts +++ b/packages/components/src/public/QueryModel/grid/actions/Filter.test.ts @@ -97,7 +97,6 @@ describe('FilterAction::actionValueFromFilter', () => { }); }); - describe('actionValueFromEntityFieldFilter', () => { const action = new FilterAction(); @@ -206,7 +205,10 @@ describe('removeFilterValueForFilterProps', () => { }); it('handles an empty filter array gracefully', () => { - const emptyFilterProps: FilterProps = { schemaQuery: { schemaName: 'schema', queryName: 'query' }, filterArray: [] } as FilterProps; + const emptyFilterProps: FilterProps = { + schemaQuery: { schemaName: 'schema', queryName: 'query' }, + filterArray: [], + } as FilterProps; const updatedFilters = removeFilterValueForFilterProps(emptyFilterProps, actionValues, 0); expect(updatedFilters.length).toBe(0); }); diff --git a/packages/components/src/public/QueryModel/grid/actions/Filter.ts b/packages/components/src/public/QueryModel/grid/actions/Filter.ts index 4bd9a28bdf..98d5a45c9a 100644 --- a/packages/components/src/public/QueryModel/grid/actions/Filter.ts +++ b/packages/components/src/public/QueryModel/grid/actions/Filter.ts @@ -149,11 +149,14 @@ function resolveSymbol(filterType: Filter.IFilterType): string { return getURLSuffix(filterType); } -export function getActionValuesForFilterProps(entityFilterProps: FilterProps, queryTypeLabel: string, isReadOnly?: string): ActionValue[] { +export function getActionValuesForFilterProps( + entityFilterProps: FilterProps, + queryTypeLabel: string, + isReadOnly?: string +): ActionValue[] { const action = new FilterAction(); const actionValues: ActionValue[] = []; - if (!entityFilterProps || !entityFilterProps.schemaQuery) - return null + if (!entityFilterProps || !entityFilterProps.schemaQuery) return null; if (!entityFilterProps.filterArray || entityFilterProps.filterArray.length === 0) { const filterValue = `${queryTypeLabel} = ${entityFilterProps.dataTypeDisplayName}`; actionValues.push({ @@ -163,24 +166,26 @@ export function getActionValuesForFilterProps(entityFilterProps: FilterProps, qu value: filterValue, valueObject: null, }); - } else { entityFilterProps.filterArray.forEach(filter => { actionValues.push(action.actionValueFromEntityFieldFilter(filter, isReadOnly)); - }) + }); } return actionValues; } -export function removeFilterValueForFilterProps(entityFilterProps: FilterProps, actionValues: ActionValue[], valueIndex: number): EntityFieldFilter[] { +export function removeFilterValueForFilterProps( + entityFilterProps: FilterProps, + actionValues: ActionValue[], + valueIndex: number +): EntityFieldFilter[] { const value = actionValues[valueIndex]?.valueObject; - if (!value) - return entityFilterProps.filterArray; + if (!value) return entityFilterProps.filterArray; const viewFilterIndex = entityFilterProps.filterArray.findIndex(f => filtersEqual(f.filter, value)) ?? -1; const updatedFilterArray = [...entityFilterProps.filterArray]; if (viewFilterIndex > -1) { - updatedFilterArray.splice(viewFilterIndex, 1) + updatedFilterArray.splice(viewFilterIndex, 1); } return updatedFilterArray; } diff --git a/packages/components/src/public/QueryModel/grid/utils.test.ts b/packages/components/src/public/QueryModel/grid/utils.test.ts index 9fb45e2b27..4790db2b4f 100644 --- a/packages/components/src/public/QueryModel/grid/utils.test.ts +++ b/packages/components/src/public/QueryModel/grid/utils.test.ts @@ -85,4 +85,3 @@ describe('filterActionValuesByType', () => { expect(result[1].isReadOnly).toBe('locked'); }); }); - diff --git a/packages/components/src/public/QueryModel/grid/utils.ts b/packages/components/src/public/QueryModel/grid/utils.ts index f9982cd9a3..72e5e809d4 100644 --- a/packages/components/src/public/QueryModel/grid/utils.ts +++ b/packages/components/src/public/QueryModel/grid/utils.ts @@ -65,7 +65,11 @@ export function getSearchValueAction(actionValues: ActionValue[], value: string) return change; } -export function filterActionValuesByType(actionValues: ActionValue[], keyword: string, lockReadOnlyForDelete: boolean): ActionValue[] { +export function filterActionValuesByType( + actionValues: ActionValue[], + keyword: string, + lockReadOnlyForDelete: boolean +): ActionValue[] { return actionValues .filter(actionValue => actionValue.action.keyword === keyword) .filter(actionValue => !actionValue.isReadOnly || !lockReadOnlyForDelete); From 77abac90e2beb4bbf363ada0b2452bb37b99261a Mon Sep 17 00:00:00 2001 From: XingY Date: Mon, 15 Dec 2025 19:31:59 -0800 Subject: [PATCH 03/12] Workflow Automation: Task action to filter samples for selected task --- packages/components/src/theme/filter.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/src/theme/filter.scss b/packages/components/src/theme/filter.scss index a5d910884e..f86bb2d655 100644 --- a/packages/components/src/theme/filter.scss +++ b/packages/components/src/theme/filter.scss @@ -262,7 +262,7 @@ cursor:pointer; } -.filter-modal__fields-col-nodata-msg { +.filter-modal__fields-col-nodata-msg, .filter-modal__fields-col-any-msg { display: inline-block; margin-left: 20px; padding-right: 5px; From 4f9bac0702d5b2a53cb63c2f98fb2c9dccfdf788 Mon Sep 17 00:00:00 2001 From: XingY Date: Mon, 15 Dec 2025 19:42:32 -0800 Subject: [PATCH 04/12] merge from develop --- packages/components/package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index 79ed0edb40..812b69b352 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "7.3.2", + "version": "7.4.0-fb-jobTaskFilter.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "7.3.2", + "version": "7.4.0-fb-jobTaskFilter.2", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", From 5e9ac08b1a1fa4082d258b0aa45a0f66b58e2734 Mon Sep 17 00:00:00 2001 From: XingY Date: Tue, 16 Dec 2025 18:26:09 -0800 Subject: [PATCH 05/12] show task samples in modal, move on to next active task on complete --- packages/components/src/public/QueryModel/GridPanel.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/components/src/public/QueryModel/GridPanel.tsx b/packages/components/src/public/QueryModel/GridPanel.tsx index 1586085ea5..9feeb48874 100644 --- a/packages/components/src/public/QueryModel/GridPanel.tsx +++ b/packages/components/src/public/QueryModel/GridPanel.tsx @@ -690,9 +690,12 @@ export class GridPanel extends PureComponent, State> { }; showFilterModal = (actionValue?: ActionValue): void => { - const { model } = this.props; + const { model, allowFiltering, showFiltersButton, allowViewCustomization } = this.props; const displayColumns = model.displayColumns; + if (!allowFiltering && !showFiltersButton && !allowViewCustomization) + return; + // if the user clicked to edit an existing filter, use that filter's column name when opening the modal // else open modal with the first field selected const columnName = actionValue?.valueObject?.getColumnName(); From 4c155252d1a16707a8def7980e8ceaac907f692c Mon Sep 17 00:00:00 2001 From: XingY Date: Tue, 16 Dec 2025 18:35:06 -0800 Subject: [PATCH 06/12] lint --- packages/components/package-lock.json | 4 ++-- packages/components/package.json | 2 +- packages/components/src/public/QueryModel/GridPanel.tsx | 3 +-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index 812b69b352..0f08625c9d 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "7.4.0-fb-jobTaskFilter.2", + "version": "7.4.0-fb-jobTaskFilter.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "7.4.0-fb-jobTaskFilter.2", + "version": "7.4.0-fb-jobTaskFilter.3", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", diff --git a/packages/components/package.json b/packages/components/package.json index eea1866a49..d1b869392b 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "7.4.0-fb-jobTaskFilter.2", + "version": "7.4.0-fb-jobTaskFilter.3", "description": "Components, models, actions, and utility functions for LabKey applications and pages", "sideEffects": false, "files": [ diff --git a/packages/components/src/public/QueryModel/GridPanel.tsx b/packages/components/src/public/QueryModel/GridPanel.tsx index 9feeb48874..39b1149b78 100644 --- a/packages/components/src/public/QueryModel/GridPanel.tsx +++ b/packages/components/src/public/QueryModel/GridPanel.tsx @@ -693,8 +693,7 @@ export class GridPanel extends PureComponent, State> { const { model, allowFiltering, showFiltersButton, allowViewCustomization } = this.props; const displayColumns = model.displayColumns; - if (!allowFiltering && !showFiltersButton && !allowViewCustomization) - return; + if (!allowFiltering && !showFiltersButton && !allowViewCustomization) return; // if the user clicked to edit an existing filter, use that filter's column name when opening the modal // else open modal with the first field selected From 386db9423d59d56779ab986057ce7ef6c50cb4fc Mon Sep 17 00:00:00 2001 From: XingY Date: Wed, 17 Dec 2025 17:41:41 -0800 Subject: [PATCH 07/12] metrics --- .../entities/FindDerivativesButton.tsx | 22 +++++++++++-------- .../components/search/QueryFilterPanel.tsx | 4 +++- .../src/public/QueryModel/GridPanel.tsx | 4 +--- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/packages/components/src/internal/components/entities/FindDerivativesButton.tsx b/packages/components/src/internal/components/entities/FindDerivativesButton.tsx index 68ba55dec3..2bc57d1da8 100644 --- a/packages/components/src/internal/components/entities/FindDerivativesButton.tsx +++ b/packages/components/src/internal/components/entities/FindDerivativesButton.tsx @@ -130,18 +130,22 @@ export function getSearchFilterObj(filterProp: FilterProps): any { const filterPropObj = { ...filterProp }; delete filterPropObj['entityDataType']; // don't persist the entire entitydatatype - filterPropObj['sampleFinderCardType'] = filterProp.entityDataType.sampleFinderCardType; + if (filterProp.entityDataType) + filterPropObj['sampleFinderCardType'] = filterProp.entityDataType.sampleFinderCardType; const filterArrayObjs = []; - [...filterPropObj.filterArray].forEach(field => { - filterArrayObjs.push({ - fieldKey: field.fieldKey, - fieldCaption: field.fieldCaption, - filter: filterToJson(field.filter), - jsonType: field.jsonType, + if (filterPropObj.filterArray) { + [...filterPropObj.filterArray]?.forEach(field => { + filterArrayObjs.push({ + fieldKey: field.fieldKey, + fieldCaption: field.fieldCaption, + filter: filterToJson(field.filter), + jsonType: field.jsonType, + }); }); - }); - filterPropObj.filterArray = filterArrayObjs; + filterPropObj.filterArray = filterArrayObjs; + } + return filterPropObj; } diff --git a/packages/components/src/internal/components/search/QueryFilterPanel.tsx b/packages/components/src/internal/components/search/QueryFilterPanel.tsx index 4d35ec3f98..955ff1a000 100644 --- a/packages/components/src/internal/components/search/QueryFilterPanel.tsx +++ b/packages/components/src/internal/components/search/QueryFilterPanel.tsx @@ -47,6 +47,7 @@ interface Props { hasAllValuesInQuery?: boolean; hasNotInQueryFilter?: boolean; hasNotInQueryFilterLabel?: string; + allInQueryFilterLabel?: string; isAncestor?: boolean; metricFeatureArea?: string; onAllValuesInQueryChange?: (check: boolean) => void; @@ -80,6 +81,7 @@ export const QueryFilterPanel: FC = memo(props => { onHasNoValueInQueryChange, onAllValuesInQueryChange, hasNotInQueryFilterLabel, + allInQueryFilterLabel, altQueryName, fields, isAncestor, @@ -258,7 +260,7 @@ export const QueryFilterPanel: FC = memo(props => { type="checkbox" />
- All {entityDataType.nounAsParentPlural ?? entityDataType.nounPlural} + {allInQueryFilterLabel ?? 'All data'}
)} diff --git a/packages/components/src/public/QueryModel/GridPanel.tsx b/packages/components/src/public/QueryModel/GridPanel.tsx index 39b1149b78..1586085ea5 100644 --- a/packages/components/src/public/QueryModel/GridPanel.tsx +++ b/packages/components/src/public/QueryModel/GridPanel.tsx @@ -690,11 +690,9 @@ export class GridPanel extends PureComponent, State> { }; showFilterModal = (actionValue?: ActionValue): void => { - const { model, allowFiltering, showFiltersButton, allowViewCustomization } = this.props; + const { model } = this.props; const displayColumns = model.displayColumns; - if (!allowFiltering && !showFiltersButton && !allowViewCustomization) return; - // if the user clicked to edit an existing filter, use that filter's column name when opening the modal // else open modal with the first field selected const columnName = actionValue?.valueObject?.getColumnName(); From b3ba8de3992315029262b2c68bc05ab325917da8 Mon Sep 17 00:00:00 2001 From: XingY Date: Wed, 17 Dec 2025 17:47:50 -0800 Subject: [PATCH 08/12] merge from develop --- packages/components/package-lock.json | 4 ++-- packages/components/package.json | 2 +- .../src/internal/components/search/QueryFilterPanel.tsx | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index 0f08625c9d..2231af1cfc 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "7.4.0-fb-jobTaskFilter.3", + "version": "7.4.0-fb-jobTaskFilter.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "7.4.0-fb-jobTaskFilter.3", + "version": "7.4.0-fb-jobTaskFilter.4", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", diff --git a/packages/components/package.json b/packages/components/package.json index d1b869392b..bb7aa97059 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "7.4.0-fb-jobTaskFilter.3", + "version": "7.4.0-fb-jobTaskFilter.4", "description": "Components, models, actions, and utility functions for LabKey applications and pages", "sideEffects": false, "files": [ diff --git a/packages/components/src/internal/components/search/QueryFilterPanel.tsx b/packages/components/src/internal/components/search/QueryFilterPanel.tsx index 955ff1a000..744c228a47 100644 --- a/packages/components/src/internal/components/search/QueryFilterPanel.tsx +++ b/packages/components/src/internal/components/search/QueryFilterPanel.tsx @@ -33,6 +33,7 @@ enum FieldFilterTabs { const DEFAULT_VIEW_NAME = ''; // always use default view for selection, if none provided interface Props { + allInQueryFilterLabel?: string; allowRelativeDateFilter?: boolean; altQueryName?: string; api?: ComponentsAPIWrapper; @@ -47,7 +48,6 @@ interface Props { hasAllValuesInQuery?: boolean; hasNotInQueryFilter?: boolean; hasNotInQueryFilterLabel?: string; - allInQueryFilterLabel?: string; isAncestor?: boolean; metricFeatureArea?: string; onAllValuesInQueryChange?: (check: boolean) => void; From d6a6dfd57eb5d8bf9a579b8fab9639f5ca06ba8f Mon Sep 17 00:00:00 2001 From: XingY Date: Wed, 17 Dec 2025 19:37:32 -0800 Subject: [PATCH 09/12] clean --- packages/components/package-lock.json | 4 ++-- packages/components/package.json | 2 +- packages/components/releaseNotes/components.md | 5 ++++- .../components/entities/FindDerivativesButton.tsx | 2 +- .../components/search/QueryFilterPanel.test.tsx | 4 ++-- .../src/public/QueryModel/grid/actions/Filter.ts | 15 ++++++++++----- .../src/public/QueryModel/grid/utils.test.ts | 2 +- 7 files changed, 21 insertions(+), 13 deletions(-) diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index 2231af1cfc..dd87d1da06 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "7.4.0-fb-jobTaskFilter.4", + "version": "7.4.0-fb-jobTaskFilter.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "7.4.0-fb-jobTaskFilter.4", + "version": "7.4.0-fb-jobTaskFilter.5", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", diff --git a/packages/components/package.json b/packages/components/package.json index bb7aa97059..db7320ff62 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "7.4.0-fb-jobTaskFilter.4", + "version": "7.4.0-fb-jobTaskFilter.5", "description": "Components, models, actions, and utility functions for LabKey applications and pages", "sideEffects": false, "files": [ diff --git a/packages/components/releaseNotes/components.md b/packages/components/releaseNotes/components.md index 578dca320c..347cf9c04c 100644 --- a/packages/components/releaseNotes/components.md +++ b/packages/components/releaseNotes/components.md @@ -4,7 +4,10 @@ Components, models, actions, and utility functions for LabKey applications and p ### version 7.X *Released*: x December 2025 - Workflow Automation: Task action to filter samples for selected task - - TODO + - Added `lockReadOnlyForDelete` prop for workflow filter components to prevent modification of read-only filters used for deletion. + - Introduced `EntityFieldFilter` type (renamed from `FieldFilter`) for improved clarity and consistency across workflow task filters. + - Added new filter action helper functions: `getActionValuesForFilterProps` and `removeFilterValueForFilterProps` to simplify reading and updating filter values. + - Added support for `supportAllValueInQuery` in entity data types so workflow filters can include an "All" option when querying entities. ### version 7.3.2 *Released*: 15 December 2025 diff --git a/packages/components/src/internal/components/entities/FindDerivativesButton.tsx b/packages/components/src/internal/components/entities/FindDerivativesButton.tsx index 2bc57d1da8..2f79ffd27e 100644 --- a/packages/components/src/internal/components/entities/FindDerivativesButton.tsx +++ b/packages/components/src/internal/components/entities/FindDerivativesButton.tsx @@ -135,7 +135,7 @@ export function getSearchFilterObj(filterProp: FilterProps): any { const filterArrayObjs = []; if (filterPropObj.filterArray) { - [...filterPropObj.filterArray]?.forEach(field => { + [...filterPropObj.filterArray].forEach(field => { filterArrayObjs.push({ fieldKey: field.fieldKey, fieldCaption: field.fieldCaption, diff --git a/packages/components/src/internal/components/search/QueryFilterPanel.test.tsx b/packages/components/src/internal/components/search/QueryFilterPanel.test.tsx index 296219adb0..57c8218f31 100644 --- a/packages/components/src/internal/components/search/QueryFilterPanel.test.tsx +++ b/packages/components/src/internal/components/search/QueryFilterPanel.test.tsx @@ -229,7 +229,7 @@ describe('QueryFilterPanel', () => { test('supportAllValueInQuery checkbox, not checked', () => { const { unmount } = render( - + ); validate(10); expect(document.querySelector('.filter-modal__fields-col-any-msg')!.textContent).toBe('All Samples'); @@ -250,7 +250,7 @@ describe('QueryFilterPanel', () => { /> ); validate(10); - expect(document.querySelector('.filter-modal__fields-col-any-msg')!.textContent).toBe('All Samples'); + expect(document.querySelector('.filter-modal__fields-col-any-msg')!.textContent).toBe('All data'); expect(document.querySelector('input[name="field-value-allvalues-check"]').getAttribute('checked')).toBe(''); expect(document.querySelectorAll('.field-modal__col-content-disabled')).toHaveLength(0); diff --git a/packages/components/src/public/QueryModel/grid/actions/Filter.ts b/packages/components/src/public/QueryModel/grid/actions/Filter.ts index 98d5a45c9a..2a9628e039 100644 --- a/packages/components/src/public/QueryModel/grid/actions/Filter.ts +++ b/packages/components/src/public/QueryModel/grid/actions/Filter.ts @@ -182,12 +182,17 @@ export function removeFilterValueForFilterProps( ): EntityFieldFilter[] { const value = actionValues[valueIndex]?.valueObject; if (!value) return entityFilterProps.filterArray; - const viewFilterIndex = entityFilterProps.filterArray.findIndex(f => filtersEqual(f.filter, value)) ?? -1; - const updatedFilterArray = [...entityFilterProps.filterArray]; - if (viewFilterIndex > -1) { - updatedFilterArray.splice(viewFilterIndex, 1); + + if (entityFilterProps.filterArray) { + const viewFilterIndex = entityFilterProps.filterArray.findIndex(f => filtersEqual(f.filter, value)); + const updatedFilterArray = [...entityFilterProps.filterArray]; + if (viewFilterIndex > -1) { + updatedFilterArray.splice(viewFilterIndex, 1); + } + return updatedFilterArray } - return updatedFilterArray; + + return null; } export class FilterAction implements Action { diff --git a/packages/components/src/public/QueryModel/grid/utils.test.ts b/packages/components/src/public/QueryModel/grid/utils.test.ts index 4790db2b4f..7f30bccfac 100644 --- a/packages/components/src/public/QueryModel/grid/utils.test.ts +++ b/packages/components/src/public/QueryModel/grid/utils.test.ts @@ -72,7 +72,7 @@ describe('filterActionValuesByType', () => { expect(result.length).toBe(0); }); - it('returns an empty array when all matching action values are read-only and lockReadOnlyForDelete is true', () => { + it('returns non-readonly filter action values when lockReadOnlyForDelete is true', () => { const result = filterActionValuesByType(mockActionValues, 'filter', true); expect(result.length).toBe(1); expect(result[0].isReadOnly).toBeUndefined(); From bc67ea3a500e92304c2511897e434dbde1503bdf Mon Sep 17 00:00:00 2001 From: XingY Date: Mon, 22 Dec 2025 10:57:34 -0800 Subject: [PATCH 10/12] jest tests, bug fixes --- .../src/public/QueryModel/grid/Value.test.tsx | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/components/src/public/QueryModel/grid/Value.test.tsx b/packages/components/src/public/QueryModel/grid/Value.test.tsx index f38e31b80a..a95859d5fb 100644 --- a/packages/components/src/public/QueryModel/grid/Value.test.tsx +++ b/packages/components/src/public/QueryModel/grid/Value.test.tsx @@ -99,6 +99,26 @@ describe('Value', () => { expect(onRemove).toHaveBeenCalledTimes(1); }); + test('isReadOnly and lockReadOnlyForDelete', async () => { + const onClick = jest.fn(); + const onRemove = jest.fn(); + render(); + expect(document.querySelectorAll('.filter-status-value')).toHaveLength(1); + expect(document.querySelectorAll('.is-active')).toHaveLength(0); + expect(document.querySelectorAll('.is-disabled')).toHaveLength(0); + expect(document.querySelectorAll('.is-readonly')).toHaveLength(1); + expect(document.querySelectorAll('.read-lock')).toHaveLength(1); + expect(document.querySelectorAll('.symbol')).toHaveLength(0); + expect(document.querySelectorAll('.fa-close')).toHaveLength(0); + expect(document.querySelectorAll('.fa-filter')).toHaveLength(0); + + expect(onClick).toHaveBeenCalledTimes(0); + await userEvent.click(document.querySelector('.filter-status-value span')); + expect(onClick).toHaveBeenCalledTimes(0); + expect(onRemove).toHaveBeenCalledTimes(0); + + }); + test('click nonRemovableAction action', async () => { const onClick = jest.fn(); const onRemove = jest.fn(); From c91dde7cf3276a9a95b8e91395c19f5d849d9a0e Mon Sep 17 00:00:00 2001 From: XingY Date: Tue, 23 Dec 2025 14:14:29 -0800 Subject: [PATCH 11/12] Disabled filter --- packages/components/package-lock.json | 4 ++-- packages/components/package.json | 2 +- .../components/src/public/QueryModel/grid/Value.tsx | 5 +++-- packages/components/src/theme/query-model.scss | 13 ++++--------- 4 files changed, 10 insertions(+), 14 deletions(-) diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index cf20773ca6..e85f80f65e 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "7.6.0-fb-jobTaskFilter.1", + "version": "7.6.0-fb-jobTaskFilter.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "7.6.0-fb-jobTaskFilter.1", + "version": "7.6.0-fb-jobTaskFilter.2", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", diff --git a/packages/components/package.json b/packages/components/package.json index 847d55aa67..fe84b96284 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "7.6.0-fb-jobTaskFilter.1", + "version": "7.6.0-fb-jobTaskFilter.2", "description": "Components, models, actions, and utility functions for LabKey applications and pages", "sideEffects": false, "files": [ diff --git a/packages/components/src/public/QueryModel/grid/Value.tsx b/packages/components/src/public/QueryModel/grid/Value.tsx index 26bd21510b..554885efce 100644 --- a/packages/components/src/public/QueryModel/grid/Value.tsx +++ b/packages/components/src/public/QueryModel/grid/Value.tsx @@ -87,7 +87,7 @@ export class Value extends React.Component { const className = classNames(valueClassName, { 'is-active': this.state.isActive, - 'is-disabled': this.state.isDisabled, + 'is-disabled': this.state.isDisabled || (lockReadOnlyForDelete && isReadOnly), 'is-readonly': isReadOnly !== undefined, }); @@ -103,9 +103,10 @@ export class Value extends React.Component { onClick={this.onClick} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave} + title={isReadOnly} > {(!lockReadOnlyForDelete || !isReadOnly) && } - {isReadOnly ? : null} + {isReadOnly ? : null} {displayValue ?? value}
); diff --git a/packages/components/src/theme/query-model.scss b/packages/components/src/theme/query-model.scss index 63e347dec3..a77767fbd3 100644 --- a/packages/components/src/theme/query-model.scss +++ b/packages/components/src/theme/query-model.scss @@ -134,8 +134,10 @@ } &.is-disabled { - background-color: $grid-action-item-disabled-bg; - border: 1px solid $grid-action-item-disabled-border-color; + opacity: 0.6; + cursor: not-allowed; + background-color: $input-bg-disabled; + border: 1px solid $border-color; color: $grid-action-item-disabled-color; i.symbol { @@ -143,13 +145,6 @@ border-color: $grid-action-item-disabled-border-color; } - &:hover, - &:focus { - background-color: $grid-action-item-disabled-bg; - } - &:active { - background-color: $grid-action-item-disabled-bg; - } } } From 3010a63140e0ed1a044210aea11a5ce5167fecd8 Mon Sep 17 00:00:00 2001 From: XingY Date: Fri, 26 Dec 2025 13:20:20 -0800 Subject: [PATCH 12/12] lint --- packages/components/package-lock.json | 4 ++-- .../components/search/QueryFilterPanel.test.tsx | 7 ++++++- .../src/public/QueryModel/grid/Value.test.tsx | 13 ++++++++++--- .../components/src/public/QueryModel/grid/Value.tsx | 2 +- .../src/public/QueryModel/grid/actions/Filter.ts | 2 +- 5 files changed, 20 insertions(+), 8 deletions(-) diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index 26bd5b2471..b37df871a0 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "7.5.1", + "version": "7.6.0-fb-jobTaskFilter.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "7.5.1", + "version": "7.6.0-fb-jobTaskFilter.3", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", diff --git a/packages/components/src/internal/components/search/QueryFilterPanel.test.tsx b/packages/components/src/internal/components/search/QueryFilterPanel.test.tsx index 57c8218f31..1e1887f3e3 100644 --- a/packages/components/src/internal/components/search/QueryFilterPanel.test.tsx +++ b/packages/components/src/internal/components/search/QueryFilterPanel.test.tsx @@ -229,7 +229,12 @@ describe('QueryFilterPanel', () => { test('supportAllValueInQuery checkbox, not checked', () => { const { unmount } = render( - + ); validate(10); expect(document.querySelector('.filter-modal__fields-col-any-msg')!.textContent).toBe('All Samples'); diff --git a/packages/components/src/public/QueryModel/grid/Value.test.tsx b/packages/components/src/public/QueryModel/grid/Value.test.tsx index a95859d5fb..08921ff6a7 100644 --- a/packages/components/src/public/QueryModel/grid/Value.test.tsx +++ b/packages/components/src/public/QueryModel/grid/Value.test.tsx @@ -102,10 +102,18 @@ describe('Value', () => { test('isReadOnly and lockReadOnlyForDelete', async () => { const onClick = jest.fn(); const onRemove = jest.fn(); - render(); + render( + + ); expect(document.querySelectorAll('.filter-status-value')).toHaveLength(1); expect(document.querySelectorAll('.is-active')).toHaveLength(0); - expect(document.querySelectorAll('.is-disabled')).toHaveLength(0); + expect(document.querySelectorAll('.is-disabled')).toHaveLength(1); expect(document.querySelectorAll('.is-readonly')).toHaveLength(1); expect(document.querySelectorAll('.read-lock')).toHaveLength(1); expect(document.querySelectorAll('.symbol')).toHaveLength(0); @@ -116,7 +124,6 @@ describe('Value', () => { await userEvent.click(document.querySelector('.filter-status-value span')); expect(onClick).toHaveBeenCalledTimes(0); expect(onRemove).toHaveBeenCalledTimes(0); - }); test('click nonRemovableAction action', async () => { diff --git a/packages/components/src/public/QueryModel/grid/Value.tsx b/packages/components/src/public/QueryModel/grid/Value.tsx index 554885efce..2a5f45911b 100644 --- a/packages/components/src/public/QueryModel/grid/Value.tsx +++ b/packages/components/src/public/QueryModel/grid/Value.tsx @@ -106,7 +106,7 @@ export class Value extends React.Component { title={isReadOnly} > {(!lockReadOnlyForDelete || !isReadOnly) && } - {isReadOnly ? : null} + {isReadOnly ? : null} {displayValue ?? value}
); diff --git a/packages/components/src/public/QueryModel/grid/actions/Filter.ts b/packages/components/src/public/QueryModel/grid/actions/Filter.ts index 2a9628e039..2649f98267 100644 --- a/packages/components/src/public/QueryModel/grid/actions/Filter.ts +++ b/packages/components/src/public/QueryModel/grid/actions/Filter.ts @@ -189,7 +189,7 @@ export function removeFilterValueForFilterProps( if (viewFilterIndex > -1) { updatedFilterArray.splice(viewFilterIndex, 1); } - return updatedFilterArray + return updatedFilterArray; } return null;