Skip to content

Commit

Permalink
Merge pull request #1496 from jay-hodgson/PORTALS-3373
Browse files Browse the repository at this point in the history
PORTALS-3373: Unlinked prototype of Portal FTS search in CCKP
  • Loading branch information
jay-hodgson authored Jan 13, 2025
2 parents c343236 + 946a733 commit 27ab198
Show file tree
Hide file tree
Showing 12 changed files with 414 additions and 12 deletions.
1 change: 1 addition & 0 deletions apps/portals/cancercomplexity/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"type": "module",
"dependencies": {
"@sage-bionetworks/synapse-types": "workspace:*",
"@mui/material": "^5.15.13",
"katex": "^0.16.10",
"@sage-bionetworks/synapse-portal-framework": "workspace:*",
"react": "^18.2.0",
Expand Down
5 changes: 5 additions & 0 deletions apps/portals/cancercomplexity/src/config/routesConfig.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
onIndividualThemeBarPlotPointClick,
onPointClick,
} from './synapseConfigs/onPointClick'
import { searchPageChildRoutes } from 'src/pages/CCKPSearchPage'

const routes: RouteObject[] = [
{
Expand Down Expand Up @@ -235,6 +236,10 @@ const routes: RouteObject[] = [
path: 'MC2Supplement',
element: <MC2Supplement />,
},
{
path: 'Search',
children: searchPageChildRoutes,
},
],
},
]
Expand Down
157 changes: 157 additions & 0 deletions apps/portals/cancercomplexity/src/pages/CCKPSearchPage.tsx
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>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
import { useLocation, useNavigate } from 'react-router-dom'
import { ExploreWrapperProps } from './ExploreWrapperProps'

function CustomScrollButton(props: TabScrollButtonProps) {
export function CustomScrollButton(props: TabScrollButtonProps) {
if (props.disabled) {
return <></>
}
Expand Down
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
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',
}}
/>
</>
)
}
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
Loading

0 comments on commit 27ab198

Please sign in to comment.