From 835a04025acee26f5d2f41f6a30daff7493b932d Mon Sep 17 00:00:00 2001 From: jay-hodgson Date: Tue, 20 Aug 2024 09:34:57 -0700 Subject: [PATCH 1/4] PORTALS-3052 --- .../src/components/FullTextSearch.tsx | 64 +++++++++++++------ .../src/utils/functions/TypeUtils.ts | 0 .../test/containers/FullTextSearch.test.tsx | 40 ++++++++++++ 3 files changed, 86 insertions(+), 18 deletions(-) delete mode 100644 packages/synapse-react-client/src/utils/functions/TypeUtils.ts diff --git a/packages/synapse-react-client/src/components/FullTextSearch.tsx b/packages/synapse-react-client/src/components/FullTextSearch.tsx index 56a145b6a4..a4da024188 100644 --- a/packages/synapse-react-client/src/components/FullTextSearch.tsx +++ b/packages/synapse-react-client/src/components/FullTextSearch.tsx @@ -1,11 +1,16 @@ 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' // See PLFM-7011 const MIN_SEARCH_QUERY_LENGTH = 3 @@ -19,7 +24,7 @@ export const FullTextSearch: React.FunctionComponent = ({ helpMessage = 'This search bar is powered by MySQL Full Text Search.', helpUrl, }: FullTextSearchProps) => { - const { executeQueryRequest } = useQueryContext() + const { executeQueryRequest, getColumnModel } = useQueryContext() const { showSearchBar } = useQueryVisualizationContext() const [searchText, setSearchText] = useState('') const searchInputRef = useRef(null) @@ -36,23 +41,46 @@ 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 idColumnModel = getColumnModel('id') + const isSynapseID = searchText.match(SYNAPSE_ENTITY_ID_REGEX) + if (isSynapseID && idColumnModel) { + const singleValueQueryFilter: ColumnSingleValueQueryFilter = { + concreteType: + 'org.sagebionetworks.repo.model.table.ColumnSingleValueQueryFilter', + columnName: 'id', + operator: ColumnSingleValueFilterOperator.IN, + values: [searchText], + } + 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..393f90ed2e 100644 --- a/packages/synapse-react-client/test/containers/FullTextSearch.test.tsx +++ b/packages/synapse-react-client/test/containers/FullTextSearch.test.tsx @@ -11,6 +11,10 @@ import { QueryContextType, } from '../../src/components/QueryContext/QueryContext' import { createWrapper } from '../../src/testutils/TestingLibraryUtils' +import { + ColumnModel, + ColumnSingleValueFilterOperator, +} from '@sage-bionetworks/synapse-types' const renderComponent = ( queryContext: Partial, @@ -32,10 +36,12 @@ const renderComponent = ( const mockExecuteQueryRequest = jest.fn() const mockGetLastQueryRequest = jest.fn() +const mockGetColumnModel = jest.fn() const queryContext: Partial = { executeQueryRequest: mockExecuteQueryRequest, getCurrentQueryRequest: mockGetLastQueryRequest, + getColumnModel: mockGetColumnModel, } const queryVisualizationContext: Partial = { @@ -90,6 +96,40 @@ describe('FullTextSearch tests', () => { ) }) + it('adds the appropriate QueryFilter when searching for Synapse ID', async () => { + const idColModel: ColumnModel = { + name: 'id', + columnType: 'ENTITYID', + id: '1', + } + mockGetColumnModel.mockReturnValue(idColModel) + 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) From 9d17e18ab4b62f28841d899d78bd5358539150d8 Mon Sep 17 00:00:00 2001 From: jay-hodgson Date: Tue, 20 Aug 2024 09:47:06 -0700 Subject: [PATCH 2/4] the id column must be of type entity id as well --- .../synapse-react-client/src/components/FullTextSearch.tsx | 7 ++++++- .../test/containers/FullTextSearch.test.tsx | 3 ++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/synapse-react-client/src/components/FullTextSearch.tsx b/packages/synapse-react-client/src/components/FullTextSearch.tsx index a4da024188..de091cbd6b 100644 --- a/packages/synapse-react-client/src/components/FullTextSearch.tsx +++ b/packages/synapse-react-client/src/components/FullTextSearch.tsx @@ -3,6 +3,7 @@ import React, { ChangeEvent, useRef, useState } from 'react' import { ColumnSingleValueFilterOperator, ColumnSingleValueQueryFilter, + ColumnTypeEnum, TextMatchesQueryFilter, } from '@sage-bionetworks/synapse-types' import { useQueryContext } from './QueryContext' @@ -43,7 +44,11 @@ export const FullTextSearch: React.FunctionComponent = ({ const { additionalFilters = [] } = request.query const idColumnModel = getColumnModel('id') const isSynapseID = searchText.match(SYNAPSE_ENTITY_ID_REGEX) - if (isSynapseID && idColumnModel) { + if ( + isSynapseID && + idColumnModel && + idColumnModel.columnType == ColumnTypeEnum.ENTITYID + ) { const singleValueQueryFilter: ColumnSingleValueQueryFilter = { concreteType: 'org.sagebionetworks.repo.model.table.ColumnSingleValueQueryFilter', diff --git a/packages/synapse-react-client/test/containers/FullTextSearch.test.tsx b/packages/synapse-react-client/test/containers/FullTextSearch.test.tsx index 393f90ed2e..8dc3ff3ef6 100644 --- a/packages/synapse-react-client/test/containers/FullTextSearch.test.tsx +++ b/packages/synapse-react-client/test/containers/FullTextSearch.test.tsx @@ -14,6 +14,7 @@ import { createWrapper } from '../../src/testutils/TestingLibraryUtils' import { ColumnModel, ColumnSingleValueFilterOperator, + ColumnTypeEnum, } from '@sage-bionetworks/synapse-types' const renderComponent = ( @@ -99,7 +100,7 @@ describe('FullTextSearch tests', () => { it('adds the appropriate QueryFilter when searching for Synapse ID', async () => { const idColModel: ColumnModel = { name: 'id', - columnType: 'ENTITYID', + columnType: ColumnTypeEnum.ENTITYID, id: '1', } mockGetColumnModel.mockReturnValue(idColModel) From 614b5ed3744629ce5fb78a07368c1e286cdab2f1 Mon Sep 17 00:00:00 2001 From: Jay Hodgson Date: Tue, 20 Aug 2024 10:29:49 -0700 Subject: [PATCH 3/4] Update packages/synapse-react-client/src/components/FullTextSearch.tsx Co-authored-by: Nick Grosenbacher --- packages/synapse-react-client/src/components/FullTextSearch.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/synapse-react-client/src/components/FullTextSearch.tsx b/packages/synapse-react-client/src/components/FullTextSearch.tsx index de091cbd6b..6678dab7e1 100644 --- a/packages/synapse-react-client/src/components/FullTextSearch.tsx +++ b/packages/synapse-react-client/src/components/FullTextSearch.tsx @@ -56,6 +56,7 @@ export const FullTextSearch: React.FunctionComponent = ({ operator: ColumnSingleValueFilterOperator.IN, values: [searchText], } + // Replace the active filter on the column, if one exists const matchingFilter = additionalFilters.find( filter => filter.concreteType == singleValueQueryFilter.concreteType && From 3e0662c44384471d1abce6d8da1725fc9e5716d1 Mon Sep 17 00:00:00 2001 From: jay-hodgson Date: Tue, 20 Aug 2024 10:57:36 -0700 Subject: [PATCH 4/4] from code review --- .../src/components/FullTextSearch.tsx | 21 +++++++++-------- .../test/containers/FullTextSearch.test.tsx | 23 +++++++++++-------- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/packages/synapse-react-client/src/components/FullTextSearch.tsx b/packages/synapse-react-client/src/components/FullTextSearch.tsx index 6678dab7e1..4338287cc2 100644 --- a/packages/synapse-react-client/src/components/FullTextSearch.tsx +++ b/packages/synapse-react-client/src/components/FullTextSearch.tsx @@ -3,7 +3,6 @@ import React, { ChangeEvent, useRef, useState } from 'react' import { ColumnSingleValueFilterOperator, ColumnSingleValueQueryFilter, - ColumnTypeEnum, TextMatchesQueryFilter, } from '@sage-bionetworks/synapse-types' import { useQueryContext } from './QueryContext' @@ -12,6 +11,9 @@ 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 @@ -25,7 +27,9 @@ export const FullTextSearch: React.FunctionComponent = ({ helpMessage = 'This search bar is powered by MySQL Full Text Search.', helpUrl, }: FullTextSearchProps) => { - const { executeQueryRequest, getColumnModel } = useQueryContext() + const { executeQueryRequest } = useQueryContext() + const data = useAtomValue(tableQueryDataAtom) + const columnModels = data?.columnModels const { showSearchBar } = useQueryVisualizationContext() const [searchText, setSearchText] = useState('') const searchInputRef = useRef(null) @@ -42,17 +46,16 @@ export const FullTextSearch: React.FunctionComponent = ({ } else { executeQueryRequest(request => { const { additionalFilters = [] } = request.query - const idColumnModel = getColumnModel('id') + const synIdColumnModelId = getFileColumnModelId(columnModels) const isSynapseID = searchText.match(SYNAPSE_ENTITY_ID_REGEX) - if ( - isSynapseID && - idColumnModel && - idColumnModel.columnType == ColumnTypeEnum.ENTITYID - ) { + if (isSynapseID && synIdColumnModelId) { + const idColumnModel = columnModels?.filter( + el => el.id === synIdColumnModelId, + ) const singleValueQueryFilter: ColumnSingleValueQueryFilter = { concreteType: 'org.sagebionetworks.repo.model.table.ColumnSingleValueQueryFilter', - columnName: 'id', + columnName: idColumnModel![0].name, operator: ColumnSingleValueFilterOperator.IN, values: [searchText], } diff --git a/packages/synapse-react-client/test/containers/FullTextSearch.test.tsx b/packages/synapse-react-client/test/containers/FullTextSearch.test.tsx index 8dc3ff3ef6..b8a22044cd 100644 --- a/packages/synapse-react-client/test/containers/FullTextSearch.test.tsx +++ b/packages/synapse-react-client/test/containers/FullTextSearch.test.tsx @@ -16,6 +16,16 @@ import { 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, @@ -26,7 +36,9 @@ const renderComponent = ( - + + + , { @@ -37,12 +49,10 @@ const renderComponent = ( const mockExecuteQueryRequest = jest.fn() const mockGetLastQueryRequest = jest.fn() -const mockGetColumnModel = jest.fn() const queryContext: Partial = { executeQueryRequest: mockExecuteQueryRequest, getCurrentQueryRequest: mockGetLastQueryRequest, - getColumnModel: mockGetColumnModel, } const queryVisualizationContext: Partial = { @@ -98,12 +108,7 @@ describe('FullTextSearch tests', () => { }) it('adds the appropriate QueryFilter when searching for Synapse ID', async () => { - const idColModel: ColumnModel = { - name: 'id', - columnType: ColumnTypeEnum.ENTITYID, - id: '1', - } - mockGetColumnModel.mockReturnValue(idColModel) + setQueryData!(mockQueryResultBundle) renderComponent(queryContext, queryVisualizationContext) const searchBox = screen.getByRole('textbox')