From 9a2e886a936b3bfda3b7bf2166fc4d7f5e7d1420 Mon Sep 17 00:00:00 2001 From: prabhat Date: Fri, 11 Oct 2024 14:36:58 +0100 Subject: [PATCH] reviewed result and query, consistent behaviour on loading --- src/styles/search/_ImpactSearchResults.scss | 4 + src/ui/pages/query/QueryPage.tsx | 78 +++++--- src/ui/pages/result/ResultPage.tsx | 190 ++++++++++++-------- src/ui/pages/result/ResultTable.tsx | 23 +-- 4 files changed, 170 insertions(+), 125 deletions(-) diff --git a/src/styles/search/_ImpactSearchResults.scss b/src/styles/search/_ImpactSearchResults.scss index 161f46be..f3a71233 100644 --- a/src/styles/search/_ImpactSearchResults.scss +++ b/src/styles/search/_ImpactSearchResults.scss @@ -819,4 +819,8 @@ div { padding: 1px 3px 1px 3px; } +} + +.result-warning { + padding: 10px; } \ No newline at end of file diff --git a/src/ui/pages/query/QueryPage.tsx b/src/ui/pages/query/QueryPage.tsx index 5494868e..27e443e9 100644 --- a/src/ui/pages/query/QueryPage.tsx +++ b/src/ui/pages/query/QueryPage.tsx @@ -12,6 +12,10 @@ import {HelpButton} from "../../components/help/HelpButton"; import {HelpContent} from "../../components/help/HelpContent"; import {ShareLink} from "../../components/common/ShareLink"; import Spaces from "../../elements/Spaces"; +import Loader from "../../elements/Loader"; +import {NO_DATA, NO_RESULT, UNEXPECTED_ERR} from "../result/ResultPage"; + +const INVALID_QUERY = 'Invalid search query' const chromosomeRegex = /^chr([1-9]|1[0-9]|2[0-2]|X|Y|MT)$/; // Uniprot accession regular expression: @@ -34,10 +38,10 @@ const QueryPageContent = (props: QueryPageProps) => { const location = useLocation() const [searchParams] = useSearchParams(); const {param1, param2, param3, param4} = useParams(); - const [query, setQuery] = useState() + const [query, setQuery] = useState() const [data, setData] = useState(null) const [loading, setLoading] = useState(true) - const [error, setError] = useState('') + const [warning, setWarning] = useState('') const loadData = useCallback((queryType: string, searchParams: URLSearchParams, param1?: string, param2?: string, param3?: string, param4?: string) => { setLoading(true) @@ -70,33 +74,50 @@ const QueryPageContent = (props: QueryPageProps) => { } const assembly = searchParams.get('assembly'); - console.log('query', q) + //console.log('query', q) if (q) { - document.title = `${q} - ${TITLE}` setQuery(q) mappings([q], assembly ?? undefined) .then((response) => { - if (response.data && response?.data?.inputs?.length > 0) { - const mappingResponse = toPagedMappingResponse(response.data) - setData(mappingResponse) + if (response.data) { + if (response.data.inputs?.length > 0) { + const mappingResponse = toPagedMappingResponse(response.data) + if (mappingResponse.content?.inputs?.length > 0) { + setData(mappingResponse) + } else { + setWarning(NO_RESULT) + } + } + } else { + setWarning(NO_DATA) } }) .catch((err) => { - console.log(err); + if (err.response) { + if (err.response.status === 404) { + setWarning(NO_RESULT); + } else { + setWarning(`Error ${err.response.status}: ${err.response.statusText}`); + } + } else { + setWarning(UNEXPECTED_ERR); + } }).finally(() => { setLoading(false) }) } else { - setError('Invalid search query') + setWarning(INVALID_QUERY) } setLoading(false) }, []); useEffect(() => { - setError('') + setWarning('') loadData(props.queryType, searchParams, param1, param2, param3, param4) - }, [props.queryType, searchParams, param1, param2, param3, param4, loadData, error]); + }, [props.queryType, searchParams, param1, param2, param3, param4, loadData]); + + document.title = query ? `${query} | ${TITLE}` : TITLE const shareUrl = `${APP_URL}${location.pathname}${location.search}` @@ -104,29 +125,28 @@ const QueryPageContent = (props: QueryPageProps) => {
Query {query}
- }/> + }/>
-
+ {data && +
- {data && -
- - - - -
- } +
+ + + + +
-
- {error && ( - - {' '} - {error} - - )} - +
} + + {warning && (
+ {' '} + {warning} +
)} + {!data && loading && } + } diff --git a/src/ui/pages/result/ResultPage.tsx b/src/ui/pages/result/ResultPage.tsx index 736a5e59..e7545956 100644 --- a/src/ui/pages/result/ResultPage.tsx +++ b/src/ui/pages/result/ResultPage.tsx @@ -16,13 +16,20 @@ import {HelpButton} from "../../components/help/HelpButton"; import {HelpContent} from "../../components/help/HelpContent"; import {ShareLink} from "../../components/common/ShareLink"; import Spaces from "../../elements/Spaces"; +import Loader from "../../elements/Loader"; + +const INVALID_PAGE = `The requested page number is invalid or out of range. Displaying page ${DEFAULT_PAGE} by default.` +const INVALID_PAGE_SIZE = `The specified page size is invalid. Using the default page size of ${DEFAULT_PAGE_SIZE} instead.` +const MAX_PAGE_EXCEEDED = `The requested page number exceeds the total number of available pages (total pages: {totalPages}).` +export const NO_DATA = 'No data' +export const NO_RESULT = 'No result to display' +export const UNEXPECTED_ERR = 'An unexpected error occurred' function ResultPageContent(props: ResultPageProps) { const location = useLocation(); const {id} = useParams<{ id?: string }>(); const [searchParams] = useSearchParams(); - const [title, setTitle] = useState('') - const [paginate, setPaginate] = useState(false) + const [resultTitle, setResultTitle] = useState(id) // components that alter search params: // 1) PaginationRow @@ -38,8 +45,8 @@ function ResultPageContent(props: ResultPageProps) { const [data, setData] = useState(null) const [loading, setLoading] = useState(true) - const [error, setError] = useState('') - const { getItem, setItem } = useLocalStorage(); + const [warning, setWarning] = useState('') + const {getItem, setItem} = useLocalStorage(); const viewedRecord = useCallback((id: string, url: string) => { const now = new Date().toISOString(); @@ -57,29 +64,31 @@ function ResultPageContent(props: ResultPageProps) { savedRecords.unshift(movedRecord); } else { // if no matching record is found // add new record to beginning of array - savedRecords = [{ id, url, lastViewed: now }, ...savedRecords] + savedRecords = [{id, url, lastViewed: now}, ...savedRecords] } setItem(LOCAL_RESULTS, savedRecords); }, [getItem, setItem]); - const loadData = useCallback((inputType: InputType, id: string|undefined - , page: number, pageSize: number, assembly: string|null) => { - document.title = `${title} - ${TITLE}`; + const loadData = useCallback((inputType: InputType, id: string | undefined + , page: number, pageSize: number, assembly: string | null) => { + if (!id) { return; } setLoading(true) + // for testing, add a delay here + // const pageIsValid = !isNaN(page) && page > 0; const pageSizeIsValid = !isNaN(pageSize) && PERMITTED_PAGE_SIZES.includes(pageSize); if (!pageIsValid) { - setError(`The requested page number is invalid or out of range. Displaying page ${DEFAULT_PAGE} by default.`) + setWarning(INVALID_PAGE) //Notify.warn('hey') page = DEFAULT_PAGE } if (!pageSizeIsValid) { - setError(`The specified page size is invalid. Using the default page size of ${DEFAULT_PAGE_SIZE} instead.`) + setWarning(INVALID_PAGE_SIZE) pageSize = DEFAULT_PAGE_SIZE } @@ -90,94 +99,123 @@ function ResultPageContent(props: ResultPageProps) { getResult(inputType, id, page, pageSize, assembly) .then((response) => { - // checks each level of response obj hierarchy exists and if inputs is non-empty. - // if any part of the chain is null or undefined, the entire expr short-circuits - // returns false. - - if (page > (response?.data?.totalPages ?? 0)) { - // Handle case where page exceeds totalPages - setError(`The requested page number exceeds the total number of available pages (total pages: ${response?.data?.totalPages}). No results to display.`) - //page = response.data.totalPages - // navigate to last page? - } - - if (response?.data?.content?.inputs?.length > 0) { - setData(response.data) - viewedRecord(response.data.id, location.pathname + location.search) - - if (inputType === InputType.PROTEIN_ACCESSION) { - setTitle(`${id} (${Math.trunc(response.data.totalItems/3)} AA)`) + if (response.data) { + // checks each level of response obj hierarchy exists and if inputs is non-empty. + // if any part of the chain is null or undefined, the entire expr short-circuits + // returns false. + + if (response.data.content?.inputs?.length > 0) { + setData(response.data) + viewedRecord(response.data.id, location.pathname + location.search) + + if (inputType === InputType.PROTEIN_ACCESSION) { + setResultTitle(`${id} (${Math.trunc(response.data.totalItems / 3)} AA)`) + } else { + const totalItems = response.data.totalItems + const firstInputLine = totalItems === 1 ? + response.data.content.inputs[0].inputStr : + `${response.data.content.inputs[0].inputStr} ...+${totalItems - 1} more ` + setResultTitle(firstInputLine) + } + } else { + setWarning(NO_RESULT) + } + // if no result warning has been set, the following will override it + const totalPages = response.data.totalPages ?? 0 + if (page !== DEFAULT_PAGE && page > totalPages) { + // Handle case where page exceeds totalPages + setWarning(MAX_PAGE_EXCEEDED.replace("{totalPages}", totalPages.toString())) + //page = response.data.totalPages + // navigate to last page? + } } else { - const totalItems = response.data.totalItems - const pageTitle = totalItems === 1 ? - response.data.content.inputs[0].inputStr : - `${response.data.content.inputs[0].inputStr} ...+${totalItems-1} more ` - setTitle(pageTitle) - } - - if (response?.data?.totalPages > 1) { - setPaginate(true) + setWarning(NO_DATA) } - document.title = `${title} - ${TITLE}`; - /* -response.data.content.messages?.forEach(message => { - if (message.type === INFO) { - Notify.info(message.text) - } else if (message.type === WARN) { - Notify.warn(message.text) - } else if (message.type === ERROR) { - Notify.err(message.text) - } -});*/ - } - }) + }) .catch((err) => { - console.log(err) + if (err.response) { + if (err.response.status === 404) { + setWarning(NO_RESULT); + } else { + setWarning(`Error ${err.response.status}: ${err.response.statusText}`); + } + } else { + setWarning(UNEXPECTED_ERR); + } }).finally(() => { setLoading(false) }) - }, [viewedRecord, location, title]) + }, [viewedRecord, location]) useEffect(() => { - setError('') + setWarning('') loadData(props.inputType, id, page, pageSize, assembly); }, [props.inputType, id, page, pageSize, assembly, loadData]) // listening for change in id, and searchParams + document.title = `${resultTitle} | ${TITLE}` + const shareUrl = `${APP_URL}${location.pathname}${location.search}` return
-
Result {title}
+
Result {resultTitle}
- }/> + }/>
-
- {paginate && } - - {data && -
- - - - -
- } -
-
- {error && ( -

- {' '} - {error} -

- )} - - {paginate && - + { + /* + Page components: + if data + && totalPages>1 [pagination] + [shareResults][viewLegends][downloadResults] + if warning + [warning] + if data (else nothing) + [resultTable] + if data && totalPages>1 + [pagination] + + To check: data (null), warning (''), loading (true) + Order updated: + - warning reset every time on load + - loadData + -- set loading true + -- if page/Size invalid, set warning + -- getResult -> set warning if page requested > totalPage and set data if any + -- finally set loading false + + 1. Warning is always displayed at the top of the table. It remains visible even when the table is hidden, + such as when there are no results to display. + 2. If no data (on first load) and loading, show Loader (not shown when navigating between pages using Next/Prev). + 3. All other conditions will show an appropriate message (warning) e.g. No data + */ } + + {data && +
1 ? 'space-between' : 'flex-end', width: '100%'}}> + {data.totalPages > 1 && } + +
+ + + + +
+
+
} + + {warning && (
+ {' '} + {warning} +
)} + + {!data && loading && } + + {data && data.totalPages > 1 && }
} diff --git a/src/ui/pages/result/ResultTable.tsx b/src/ui/pages/result/ResultTable.tsx index f54376d9..a22dc7c2 100644 --- a/src/ui/pages/result/ResultTable.tsx +++ b/src/ui/pages/result/ResultTable.tsx @@ -13,28 +13,17 @@ import {StringVoidFun} from "../../../constants/CommonTypes"; import {getAlternateIsoFormRow} from "./AlternateIsoFormRow"; import {getNewPrimaryRow} from "./PrimaryRow"; import {AppContext} from "../../App"; -import Loader from "../../elements/Loader"; import MsgRow from "./MsgRow"; import {useLocation, useNavigate, useSearchParams} from "react-router-dom"; -function ResultTable(props: {loading: boolean, data: PagedMappingResponse | null}) { +function ResultTable(props: {data: PagedMappingResponse | null}) { const stdColor = useContext(AppContext).stdColor const navigate = useNavigate(); const location = useLocation(); const [searchParams] = useSearchParams(); const [isoformGroupExpanded, setIsoformGroupExpanded] = useState('') const [annotationExpanded, setAnnotationExpanded] = useState(searchParams.get('annotation') ?? '') -/* - const [searchParams] = useSearchParams(); - const annotation = searchParams.get('annotation') || '' - if (annotation) { - setAnnotationExpanded(annotation) - } -*/ -// useEffect(() => { -// setAnnotationExpanded(annotation ? annotation : '') -// }, [annotation]) function toggleIsoformGroup(key: string) { setIsoformGroupExpanded(isoformGroupExpanded === key ? '' : key); } @@ -51,14 +40,8 @@ function ResultTable(props: {loading: boolean, data: PagedMappingResponse | null navigate(url); } - // if loading and no data -> show loader - // if not loading and no data -> No result found. - // if loading and data -> show (curr) data & Prev/Next -> Loading - // if not loading and data -> show data - if (props.loading && !props.data) - return - if (!props.loading && !props.data) - return
No result found
Try another link or searching for variants again.
+ if (!props.data) + return null const tableRows = getTableRows(props.data, isoformGroupExpanded, toggleIsoformGroup, annotationExpanded, toggleAnnotation, stdColor); return