diff --git a/packages/synapse-react-client/src/components/FullTextSearch.tsx b/packages/synapse-react-client/src/components/FullTextSearch.tsx index 56a145b6a4..4338287cc2 100644 --- a/packages/synapse-react-client/src/components/FullTextSearch.tsx +++ b/packages/synapse-react-client/src/components/FullTextSearch.tsx @@ -1,11 +1,19 @@ import { Collapse, TextField } from '@mui/material' import React, { ChangeEvent, useRef, useState } from 'react' -import { TextMatchesQueryFilter } from '@sage-bionetworks/synapse-types' +import { + ColumnSingleValueFilterOperator, + ColumnSingleValueQueryFilter, + TextMatchesQueryFilter, +} from '@sage-bionetworks/synapse-types' import { useQueryContext } from './QueryContext' import { useQueryVisualizationContext } from './QueryVisualizationWrapper' import { HelpPopover } from './HelpPopover/HelpPopover' import IconSvg from './IconSvg/IconSvg' import { IconSvgButton } from './IconSvgButton' +import { SYNAPSE_ENTITY_ID_REGEX } from '../utils/functions/RegularExpressions' +import { useAtomValue } from 'jotai' +import { tableQueryDataAtom } from './QueryWrapper/QueryWrapper' +import { getFileColumnModelId } from './SynapseTable/SynapseTableUtils' // See PLFM-7011 const MIN_SEARCH_QUERY_LENGTH = 3 @@ -20,6 +28,8 @@ export const FullTextSearch: React.FunctionComponent = ({ helpUrl, }: FullTextSearchProps) => { const { executeQueryRequest } = useQueryContext() + const data = useAtomValue(tableQueryDataAtom) + const columnModels = data?.columnModels const { showSearchBar } = useQueryVisualizationContext() const [searchText, setSearchText] = useState('') const searchInputRef = useRef(null) @@ -36,23 +46,50 @@ export const FullTextSearch: React.FunctionComponent = ({ } else { executeQueryRequest(request => { const { additionalFilters = [] } = request.query - - const textMatchesQueryFilter: TextMatchesQueryFilter = { - concreteType: - 'org.sagebionetworks.repo.model.table.TextMatchesQueryFilter', - searchExpression: searchText, - } - // PORTALS-2093: does this additional filter already exist? - const found = additionalFilters.find( - filter => - filter.concreteType == textMatchesQueryFilter.concreteType && - filter.searchExpression == textMatchesQueryFilter.searchExpression, - ) - if (found) { - return request + const synIdColumnModelId = getFileColumnModelId(columnModels) + const isSynapseID = searchText.match(SYNAPSE_ENTITY_ID_REGEX) + if (isSynapseID && synIdColumnModelId) { + const idColumnModel = columnModels?.filter( + el => el.id === synIdColumnModelId, + ) + const singleValueQueryFilter: ColumnSingleValueQueryFilter = { + concreteType: + 'org.sagebionetworks.repo.model.table.ColumnSingleValueQueryFilter', + columnName: idColumnModel![0].name, + operator: ColumnSingleValueFilterOperator.IN, + values: [searchText], + } + // Replace the active filter on the column, if one exists + const matchingFilter = additionalFilters.find( + filter => + filter.concreteType == singleValueQueryFilter.concreteType && + filter.columnName == singleValueQueryFilter.columnName, + ) as ColumnSingleValueQueryFilter + if (matchingFilter) { + if (!matchingFilter.values.includes(searchText)) { + matchingFilter.values.push(searchText) + } + return request + } + additionalFilters.push(singleValueQueryFilter) + } else { + const textMatchesQueryFilter: TextMatchesQueryFilter = { + concreteType: + 'org.sagebionetworks.repo.model.table.TextMatchesQueryFilter', + searchExpression: searchText, + } + // PORTALS-2093: does this additional filter already exist? + const found = additionalFilters.find( + filter => + filter.concreteType == textMatchesQueryFilter.concreteType && + filter.searchExpression == + textMatchesQueryFilter.searchExpression, + ) + if (found) { + return request + } + additionalFilters.push(textMatchesQueryFilter) } - additionalFilters.push(textMatchesQueryFilter) - request.query.additionalFilters = additionalFilters // reset the search text after adding this filter setSearchText('') diff --git a/packages/synapse-react-client/src/utils/functions/TypeUtils.ts b/packages/synapse-react-client/src/utils/functions/TypeUtils.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/synapse-react-client/test/containers/FullTextSearch.test.tsx b/packages/synapse-react-client/test/containers/FullTextSearch.test.tsx index 17906ce4f3..b8a22044cd 100644 --- a/packages/synapse-react-client/test/containers/FullTextSearch.test.tsx +++ b/packages/synapse-react-client/test/containers/FullTextSearch.test.tsx @@ -11,6 +11,21 @@ import { QueryContextType, } from '../../src/components/QueryContext/QueryContext' import { createWrapper } from '../../src/testutils/TestingLibraryUtils' +import { + ColumnModel, + ColumnSingleValueFilterOperator, + ColumnTypeEnum, +} from '@sage-bionetworks/synapse-types' +import { useSetAtom } from 'jotai' +import { tableQueryDataAtom } from '../../src/components/QueryWrapper/QueryWrapper' +import { mockQueryResultBundle } from '../../src/mocks/mockFileViewQuery' + +let setQueryData: ReturnType | undefined + +function ContextReceiver(props: React.PropsWithChildren) { + setQueryData = useSetAtom(tableQueryDataAtom) + return <>{props.children} +} const renderComponent = ( queryContext: Partial, @@ -21,7 +36,9 @@ const renderComponent = ( - + + + , { @@ -90,6 +107,35 @@ describe('FullTextSearch tests', () => { ) }) + it('adds the appropriate QueryFilter when searching for Synapse ID', async () => { + setQueryData!(mockQueryResultBundle) + renderComponent(queryContext, queryVisualizationContext) + + const searchBox = screen.getByRole('textbox') + + const searchQuery = 'syn123' + await userEvent.type(searchBox, searchQuery + '{enter}') + + expect(mockExecuteQueryRequest).toHaveBeenCalled() + const queryTransformFn = mockExecuteQueryRequest.mock.lastCall[0] + expect(typeof queryTransformFn).toBe('function') + expect(queryTransformFn({ query: {} })).toEqual( + expect.objectContaining({ + query: expect.objectContaining({ + additionalFilters: [ + { + concreteType: + 'org.sagebionetworks.repo.model.table.ColumnSingleValueQueryFilter', + columnName: 'id', + operator: ColumnSingleValueFilterOperator.IN, + values: [searchQuery], + }, + ], + }), + }), + ) + }) + it('enforces a minimum character requirement', async () => { renderComponent(queryContext, queryVisualizationContext)