diff --git a/src/views/clinicalGenomic/clinicalGenomicSearch.js b/src/views/clinicalGenomic/clinicalGenomicSearch.js index b65955a..27df959 100644 --- a/src/views/clinicalGenomic/clinicalGenomicSearch.js +++ b/src/views/clinicalGenomic/clinicalGenomicSearch.js @@ -14,6 +14,7 @@ import SearchHandler from './search/SearchHandler'; import GenomicData from './widgets/genomicData'; import { SearchIndicator } from 'ui-component/LoadingIndicator/SearchIndicator'; import AuthorizationSections from './widgets/authorizationSections'; +import SearchExplainer from './widgets/searchExplainer'; const PREFIX = 'ClinicalGenomicSearch'; @@ -22,9 +23,11 @@ const classes = { sidebarOffset: `${PREFIX}-sidebarOffset`, noSidebarOffset: `${PREFIX}-noSidebarOffset`, headerSpacing: `${PREFIX}-headerSpacing`, + headerSize: `${PREFIX}-headerSize`, anchor: `${PREFIX}-anchor`, navigationLink: `${PREFIX}-navigationLink`, - mainContent: `${PREFIX}-mainContent` + mainContent: `${PREFIX}-mainContent`, + toolbar: `${PREFIX}-toolbar` }; // TODO jss-to-styled codemod: The Fragment root was replaced by div. Change the tag if needed. @@ -39,24 +42,28 @@ const Root = styled('div')(({ _ }) => ({ }, [`& .${classes.sidebarOffset}`]: { - width: 'calc(100% - 320px)', + width: 'calc(100% - 300px)', left: 280 }, [`& .${classes.noSidebarOffset}`]: { - width: 'calc(100% - 80px)', - left: 40 + width: 'calc(100% - 35px)', + left: 18 + }, + + [`& .${classes.headerSize}`]: { + height: 110 }, [`& .${classes.headerSpacing}`]: { - height: 50 + height: 70 }, [`& .${classes.anchor}`]: { display: 'block', position: 'relative', visibility: 'hidden', - top: -150 + top: -250 }, [`& .${classes.navigationLink}`]: { @@ -66,6 +73,13 @@ const Root = styled('div')(({ _ }) => ({ [`& .${classes.mainContent}`]: { padding: '16px !important' + }, + + [`& .${classes.toolbar}`]: { + padding: 5, + paddingLeft: 20, + paddingRight: 20, + minHeight: 58 } })); @@ -82,8 +96,8 @@ const StyledMainCard = styled(MainCard)((_) => ({ const sections = [ { id: 'cohorts summary', - header: 'Cohorts Summary', - component: + header: undefined, + component: }, { id: 'counts', @@ -97,7 +111,7 @@ const sections = [ }, { id: 'authorized cohorts', - header: 'Authorized Cohorts', + header: undefined, component: }, { @@ -129,30 +143,34 @@ function ClinicalGenomicSearch() { {/* Top bar */} - + Federated Search - {sections.map((section) => ( - - ))} + {sections + .map((section) => + section.header !== undefined ? ( + + ) : undefined + ) + .filter((obj) => obj !== undefined)} + {/* Empty div to make sure the header takes up space */} +
{sections.map((section) => ( diff --git a/src/views/clinicalGenomic/search/SearchHandler.js b/src/views/clinicalGenomic/search/SearchHandler.js index 954cc16..bb640d8 100644 --- a/src/views/clinicalGenomic/search/SearchHandler.js +++ b/src/views/clinicalGenomic/search/SearchHandler.js @@ -112,7 +112,7 @@ function SearchHandler({ setLoading }) { summaryFetchAbort.current = newAbort; }, [reader.reqNum]); - // Query 2: when the search query changes, re-query the server + // Query 3: when the search query changes, re-query the server useEffect(() => { // First, we abort any currently-running search promises clinicalFetchAbort.current.abort('New request started'); diff --git a/src/views/clinicalGenomic/widgets/searchExplainer.js b/src/views/clinicalGenomic/widgets/searchExplainer.js new file mode 100644 index 0000000..d1821f7 --- /dev/null +++ b/src/views/clinicalGenomic/widgets/searchExplainer.js @@ -0,0 +1,95 @@ +import { useEffect, useMemo } from 'react'; +import { styled } from '@mui/material/styles'; +import { Box, Chip } from '@mui/material'; +import { useSearchQueryReaderContext, useSearchResultsWriterContext } from '../SearchResultsContext'; + +const PREFIX = 'SearchExplainer'; + +const Root = styled('div')(({ theme }) => ({ + [`& .${PREFIX}-chiptext`]: { + textTransform: 'capitalize' + }, + [`& .${PREFIX}-background`]: { + backgroundColor: theme.palette.primary.light, + color: 'black', + paddingLeft: 15, + paddingBottom: 20 + }, + [`& .${PREFIX}-chip`]: { + backgroundColor: 'white', + marginRight: 5, + marginLeft: 5, + marginTop: 20 + }, + [`& .${PREFIX}-bold`]: { + position: 'relative', + top: 10 + } +})); + +function SearchExplainer() { + const reader = useSearchQueryReaderContext(); + const writer = useSearchResultsWriterContext(); + const query = reader.query; + const queryChips = []; + + // Decompose the query into its roots: what are we searching on? + if (query !== undefined) { + Object.keys(query).forEach((key) => { + if (key !== undefined && query[key] !== undefined) { + const onDelete = () => { + writer((old) => ({ ...old, clear: key })); + }; + const splitQuery = query[key].split('|'); + const newVal = splitQuery.flatMap((value) => [ OR , value]).slice(1); + const formattedKey = key.replaceAll('_', ' '); + newVal.splice( + 0, + 0, + + {formattedKey}:{' '} + + ); + queryChips.push([key, newVal, onDelete]); + } + }); + } + + useEffect(() => { + writer((old) => ({ ...old, clear: '' })); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [reader.reqNum]); + + if (queryChips.length === 0) { + queryChips.push(['all', 'All results', undefined]); + } + + return useMemo( + () => ( + + + {queryChips + /* NB: FlatMap+slice(1) to insert ANDs between entries */ + .flatMap((chip) => [ + +  AND  + , + + ]) + .slice(1)} + + + ), + // eslint-disable-next-line react-hooks/exhaustive-deps + [reader.reqNum] + ); +} + +export default SearchExplainer; diff --git a/src/views/clinicalGenomic/widgets/sidebar.js b/src/views/clinicalGenomic/widgets/sidebar.js index 7b0b7a4..582af3d 100644 --- a/src/views/clinicalGenomic/widgets/sidebar.js +++ b/src/views/clinicalGenomic/widgets/sidebar.js @@ -1,6 +1,7 @@ import { useEffect, useState } from 'react'; import { + Chip, Checkbox, FormControl, FormControlLabel, @@ -193,7 +194,7 @@ function StyledCheckboxList(props) { options={options} disableCloseOnSelect renderOption={(props, option, { selected }) => ( -
  • +
  • )} renderInput={(params) => } + renderTags={(tagValue, getTagProps) => + tagValue.map((option, index) => ) + } // set width to match parent sx={{ width: '100%', paddingTop: '0.5em', paddingBottom: '0.5em' }} onChange={(_, value, reason) => { @@ -424,6 +428,67 @@ function Sidebar() { writerContext(() => ({ reqNum: 0 })); }, [writerContext]); + // Certain webpage components can cause the sidebar to clear a particular entry (e.g. the search explanation) + useEffect(() => { + if (readerContext.clear === 'nodes') { + setSelectedNodes({}); + writerContext((old) => ({ + ...old, + filter: { + ...old.filter, + node: [readerContext?.programs?.map((loc) => loc.location.name) || []] + }, + reqNum: old.reqNum + 1 + })); + } else if (readerContext.clear === 'cohorts') { + setSelectedCohorts({}); + writerContext((old) => ({ + ...old, + filter: { + ...old.filter, + exclude_cohorts: [ + readerContext?.programs?.map((loc) => loc?.results?.items?.map((cohort) => cohort.program_id)).flat(1) || [] + ] + }, + reqNum: old.reqNum + 1 + })); + } else if (readerContext.clear === 'gene' || readerContext.clear === 'chrom' || readerContext.clear === 'assembly') { + setSelectedGenes(''); + setSelectedChromosomes(''); + setStartPos('0'); + setEndPos('0'); + writerContext((old) => { + const retVal = { ...old, reqNum: old.reqNum + 1 }; + delete retVal.query.chrom; + delete retVal.query.gene; + delete retVal.query.assembly; + return retVal; + }); + } else if (readerContext.clear === 'treatment') { + setSelectedTreatment({}); + writerContext((old) => { + const retVal = { ...old, reqNum: old.reqNum + 1 }; + delete retVal.query.treatment; + return retVal; + }); + } else if (readerContext.clear === 'primary_site') { + setSelectedPrimarySite({}); + writerContext((old) => { + const retVal = { ...old, reqNum: old.reqNum + 1 }; + delete retVal.query.primary_site; + return retVal; + }); + } else if (readerContext.clear === 'drug_name') { + setSelectedSystemicTherapy({}); + writerContext((old) => { + const retVal = { ...old, reqNum: old.reqNum + 1 }; + delete retVal.query.drug_name; + return retVal; + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [readerContext.clear]); + const triggerSearch = () => { writerContext((old) => ({ ...old, reqNum: 'reqNum' in old ? old.reqNum + 1 : 0 })); };