-
Notifications
You must be signed in to change notification settings - Fork 23
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1496 from jay-hodgson/PORTALS-3373
PORTALS-3373: Unlinked prototype of Portal FTS search in CCKP
- Loading branch information
Showing
12 changed files
with
414 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
157 changes: 157 additions & 0 deletions
157
apps/portals/cancercomplexity/src/pages/CCKPSearchPage.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
import { | ||
PortalSearchTabConfig, | ||
PortalSearchTabs, | ||
} from '@sage-bionetworks/synapse-portal-framework/components/PortalSearch/PortalSearchTabs' | ||
import { PortalFullTextSearchField } from '@sage-bionetworks/synapse-portal-framework/components/PortalSearch/PortalFullTextSearchField' | ||
import { SearchParamAwareStandaloneQueryWrapper } from '@sage-bionetworks/synapse-portal-framework/components/PortalSearch/SearchParamAwareStandaloneQueryWrapper' | ||
import { Box } from '@mui/material' | ||
import RedirectWithQuery from '@sage-bionetworks/synapse-portal-framework/components/RedirectWithQuery' | ||
import { Outlet, RouteObject } from 'react-router-dom' | ||
import cckpConfigs from 'src/config/synapseConfigs' | ||
import { QueryResultBundle } from '@sage-bionetworks/synapse-types' | ||
import { useState } from 'react' | ||
export const searchPageTabs: PortalSearchTabConfig[] = [ | ||
{ | ||
title: 'Grants', | ||
path: 'Grants', | ||
}, | ||
{ | ||
title: 'People', | ||
path: 'People', | ||
}, | ||
{ | ||
title: 'Publications', | ||
path: 'Publications', | ||
}, | ||
{ | ||
title: 'Datasets', | ||
path: 'Datasets', | ||
}, | ||
{ | ||
title: 'Tools', | ||
path: 'Tools', | ||
}, | ||
{ | ||
title: 'Educational Resources', | ||
path: 'EducationalResources', | ||
}, | ||
] | ||
|
||
export const searchPageChildRoutes: RouteObject[] = [ | ||
{ | ||
index: true, | ||
element: <RedirectWithQuery to={searchPageTabs[0].path} />, | ||
}, | ||
{ | ||
path: searchPageTabs[0].path, | ||
element: <CCKPSearchPage selectedTabIndex={0} />, | ||
}, | ||
{ | ||
path: searchPageTabs[1].path, | ||
element: <CCKPSearchPage selectedTabIndex={1} />, | ||
}, | ||
{ | ||
path: searchPageTabs[2].path, | ||
element: <CCKPSearchPage selectedTabIndex={2} />, | ||
}, | ||
{ | ||
path: searchPageTabs[3].path, | ||
element: <CCKPSearchPage selectedTabIndex={3} />, | ||
}, | ||
{ | ||
path: searchPageTabs[4].path, | ||
element: <CCKPSearchPage selectedTabIndex={4} />, | ||
}, | ||
{ | ||
path: searchPageTabs[5].path, | ||
element: <CCKPSearchPage selectedTabIndex={5} />, | ||
}, | ||
] | ||
|
||
export type CCKPSearchPageProps = { | ||
selectedTabIndex: number | ||
} | ||
|
||
function getQueryCount(queryResultBundleJSON: string) { | ||
const queryResultBundle = JSON.parse( | ||
queryResultBundleJSON, | ||
) as QueryResultBundle | ||
const { queryCount } = queryResultBundle | ||
return queryCount | ||
} | ||
|
||
export function CCKPSearchPage(props: CCKPSearchPageProps) { | ||
const { selectedTabIndex } = props | ||
const { datasets, education, grants, people, publications, tools } = | ||
cckpConfigs | ||
const [searchPageTabsState, setSearchPageTabsState] = | ||
useState<PortalSearchTabConfig[]>(searchPageTabs) | ||
// on search field value update, update the special search parameter FTS_SEARCH_TERM, which the QueryWrapperPlotNav will load as the search term | ||
return ( | ||
<Box sx={{ p: { xs: '10px', lg: '50px' } }}> | ||
<PortalFullTextSearchField /> | ||
<PortalSearchTabs tabConfig={searchPageTabsState} /> | ||
<SearchParamAwareStandaloneQueryWrapper | ||
isVisible={selectedTabIndex == 0} | ||
standaloneQueryWrapperProps={{ | ||
...grants, | ||
onQueryResultBundleChange: newQueryResultBundleJSON => { | ||
searchPageTabs[0].count = getQueryCount(newQueryResultBundleJSON) | ||
setSearchPageTabsState([...searchPageTabs]) | ||
}, | ||
}} | ||
/> | ||
<SearchParamAwareStandaloneQueryWrapper | ||
isVisible={selectedTabIndex == 1} | ||
standaloneQueryWrapperProps={{ | ||
...people, | ||
onQueryResultBundleChange: newQueryResultBundleJSON => { | ||
searchPageTabs[1].count = getQueryCount(newQueryResultBundleJSON) | ||
setSearchPageTabsState([...searchPageTabs]) | ||
}, | ||
}} | ||
/> | ||
<SearchParamAwareStandaloneQueryWrapper | ||
isVisible={selectedTabIndex == 2} | ||
standaloneQueryWrapperProps={{ | ||
...publications, | ||
onQueryResultBundleChange: newQueryResultBundleJSON => { | ||
searchPageTabs[2].count = getQueryCount(newQueryResultBundleJSON) | ||
setSearchPageTabsState([...searchPageTabs]) | ||
}, | ||
}} | ||
/> | ||
<SearchParamAwareStandaloneQueryWrapper | ||
isVisible={selectedTabIndex == 3} | ||
standaloneQueryWrapperProps={{ | ||
...datasets, | ||
onQueryResultBundleChange: newQueryResultBundleJSON => { | ||
searchPageTabs[3].count = getQueryCount(newQueryResultBundleJSON) | ||
setSearchPageTabsState([...searchPageTabs]) | ||
}, | ||
}} | ||
/> | ||
<SearchParamAwareStandaloneQueryWrapper | ||
isVisible={selectedTabIndex == 4} | ||
standaloneQueryWrapperProps={{ | ||
...tools, | ||
onQueryResultBundleChange: newQueryResultBundleJSON => { | ||
searchPageTabs[4].count = getQueryCount(newQueryResultBundleJSON) | ||
setSearchPageTabsState([...searchPageTabs]) | ||
}, | ||
}} | ||
/> | ||
<SearchParamAwareStandaloneQueryWrapper | ||
isVisible={selectedTabIndex == 5} | ||
standaloneQueryWrapperProps={{ | ||
...education, | ||
onQueryResultBundleChange: newQueryResultBundleJSON => { | ||
searchPageTabs[5].count = getQueryCount(newQueryResultBundleJSON) | ||
setSearchPageTabsState([...searchPageTabs]) | ||
}, | ||
}} | ||
/> | ||
<Outlet /> | ||
</Box> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
48 changes: 48 additions & 0 deletions
48
apps/synapse-portal-framework/src/components/PortalSearch/PortalFullTextSearchField.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import { useState } from 'react' | ||
import SearchIcon from '@mui/icons-material/Search' | ||
import { InputAdornment, TextField } from '@mui/material' | ||
import { useSearchParams } from 'react-router-dom' | ||
import { FTS_SEARCH_TERM } from 'synapse-react-client/utils/functions/SqlFunctions' | ||
|
||
export function PortalFullTextSearchField() { | ||
const [searchParams, setSearchParams] = useSearchParams() | ||
const [searchInput, setSearchInput] = useState( | ||
searchParams.get(FTS_SEARCH_TERM), | ||
) | ||
|
||
return ( | ||
<TextField | ||
size={'small'} | ||
placeholder="Search by keyword" | ||
value={searchInput} | ||
onChange={event => { | ||
setSearchInput(event.target.value) | ||
}} | ||
onKeyDown={(event: any) => { | ||
if (event.key === 'Enter') { | ||
const trimmedInput = event.target.value.trim() | ||
setSearchParams({ FTS_SEARCH_TERM: trimmedInput }) | ||
} | ||
}} | ||
InputProps={{ | ||
startAdornment: ( | ||
<InputAdornment position="start"> | ||
<SearchIcon /> | ||
</InputAdornment> | ||
), | ||
}} | ||
fullWidth | ||
sx={{ | ||
boxShadow: '0px 4px 6px rgba(0, 0, 0, 0.1)', | ||
border: '1px solid', | ||
borderColor: 'grey.300', | ||
'& .MuiOutlinedInput-root': { | ||
backgroundColor: 'white', | ||
}, | ||
mb: '20px', | ||
}} | ||
/> | ||
) | ||
} | ||
|
||
export default PortalFullTextSearchField |
102 changes: 102 additions & 0 deletions
102
apps/synapse-portal-framework/src/components/PortalSearch/PortalSearchTabs.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
import { Box, Tab, Tabs, useMediaQuery, useTheme, Chip } from '@mui/material' | ||
|
||
import { useNavigate, useLocation } from 'react-router-dom' | ||
import { CustomScrollButton } from '../Explore/ExploreWrapperTabs' | ||
export type PortalSearchTabConfig = { | ||
path: string | ||
title: string | ||
count?: number | ||
} | ||
|
||
export type PortalSearchTabUIProps = { | ||
tabConfig: PortalSearchTabConfig[] | ||
} | ||
export function PortalSearchTabs(props: PortalSearchTabUIProps) { | ||
const { tabConfig } = props | ||
const location = useLocation() | ||
const theme = useTheme() | ||
const isMobileView = useMediaQuery(theme.breakpoints.down('sm')) | ||
const navigate = useNavigate() | ||
return ( | ||
<> | ||
<Tabs | ||
value={location.pathname} | ||
variant="scrollable" | ||
orientation={isMobileView ? 'vertical' : 'horizontal'} | ||
scrollButtons="auto" | ||
ScrollButtonComponent={CustomScrollButton} | ||
aria-label="Search Object Types" | ||
sx={{ | ||
'.MuiTabs-flexContainer': { | ||
gap: { xs: 2, sm: 5 }, | ||
alignItems: 'center', | ||
}, | ||
}} | ||
TabIndicatorProps={{ | ||
style: { | ||
background: 'transparent', | ||
marginTop: '4px', | ||
position: 'relative', | ||
}, | ||
}} | ||
> | ||
{tabConfig.map(({ path, title, count }) => { | ||
const targetPathname = `/Search/${path}` | ||
return ( | ||
<Tab | ||
key={path} | ||
value={encodeURI(targetPathname)} | ||
label={ | ||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> | ||
{title}{' '} | ||
{count !== undefined && ( | ||
<Chip | ||
label={count} | ||
size="small" | ||
sx={{ | ||
backgroundColor: 'grey.100', | ||
color: 'grey.900', | ||
height: '21px', | ||
}} | ||
/> | ||
)} | ||
</Box> | ||
} | ||
onClick={() => | ||
navigate({ | ||
pathname: targetPathname, | ||
search: location.search, | ||
}) | ||
} | ||
sx={{ | ||
transition: 'all 400ms', | ||
fontSize: '16px', | ||
fontWeight: 700, | ||
color: 'grey.700', | ||
minWidth: { xs: '100%', sm: 'unset' }, | ||
py: 1, | ||
px: 0, | ||
borderBottom: '4px solid', | ||
borderBottomColor: 'transparent', | ||
'&.Mui-selected': { | ||
color: 'secondary.main', | ||
borderBottomColor: 'secondary.main', | ||
}, | ||
'&:hover:not(.Mui-selected)': { | ||
color: 'grey.800', | ||
}, | ||
}} | ||
/> | ||
) | ||
})} | ||
</Tabs> | ||
<Box | ||
sx={{ | ||
borderTop: '4px solid', | ||
borderColor: 'grey.400', | ||
mt: '-4px', | ||
}} | ||
/> | ||
</> | ||
) | ||
} |
48 changes: 48 additions & 0 deletions
48
...e-portal-framework/src/components/PortalSearch/SearchParamAwareStandaloneQueryWrapper.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import { useMemo } from 'react' | ||
import { useSearchParams } from 'react-router-dom' | ||
import { QueryWrapper, StandaloneQueryWrapper } from 'synapse-react-client' | ||
import { StandaloneQueryWrapperProps } from 'synapse-react-client' | ||
import { generateInitQueryRequest } from 'synapse-react-client/components/StandaloneQueryWrapper/StandaloneQueryWrapper' | ||
import { getAdditionalFilters } from 'synapse-react-client/utils/functions' | ||
|
||
export type SearchParamAwareStandaloneQueryWrapperProps = { | ||
isVisible: boolean | ||
standaloneQueryWrapperProps: StandaloneQueryWrapperProps | ||
} | ||
export function SearchParamAwareStandaloneQueryWrapper( | ||
props: SearchParamAwareStandaloneQueryWrapperProps, | ||
) { | ||
const { isVisible, standaloneQueryWrapperProps } = props | ||
const [searchParams] = useSearchParams() | ||
const searchParamsRecords = useMemo(() => { | ||
if (searchParams) { | ||
return Object.fromEntries(searchParams.entries()) | ||
} | ||
return undefined | ||
}, [searchParams]) | ||
// if is visible, render a StandaloneQueryWrapper. | ||
// if not, just run the query wrapper with the query request derived from the search params (to populate the cache and return the count) | ||
if (isVisible) { | ||
return ( | ||
<StandaloneQueryWrapper | ||
{...standaloneQueryWrapperProps} | ||
shouldDeepLink={false} | ||
searchParams={searchParamsRecords} | ||
/> | ||
) | ||
} | ||
//else | ||
const { sql } = standaloneQueryWrapperProps | ||
const derivedQueryRequestFromSearchParams = generateInitQueryRequest(sql) | ||
derivedQueryRequestFromSearchParams.query.additionalFilters = | ||
getAdditionalFilters(undefined, searchParamsRecords, undefined) | ||
return ( | ||
<QueryWrapper | ||
{...standaloneQueryWrapperProps} | ||
shouldDeepLink={false} | ||
initQueryRequest={derivedQueryRequestFromSearchParams} | ||
></QueryWrapper> | ||
) | ||
} | ||
|
||
export default SearchParamAwareStandaloneQueryWrapper |
Oops, something went wrong.