diff --git a/frontend/packages/data-portal/app/components/Breadcrumbs.tsx b/frontend/packages/data-portal/app/components/Breadcrumbs.tsx index 3083d98a4..1c3db88db 100644 --- a/frontend/packages/data-portal/app/components/Breadcrumbs.tsx +++ b/frontend/packages/data-portal/app/components/Breadcrumbs.tsx @@ -48,7 +48,8 @@ export function Breadcrumbs({ const { browseDatasetHistory } = useBrowseDatasetFilterHistory() const { singleDatasetHistory } = useSingleDatasetFilterHistory() - const { previousDepositionId } = useDepositionHistory() + const { previousDepositionId, previousSingleDepositionParams } = + useDepositionHistory() const browseAllLink = useMemo(() => { const url = @@ -83,8 +84,7 @@ export function Breadcrumbs({ {previousDepositionId != null && variant !== 'deposition' && ( {t('returnToDeposition')} diff --git a/frontend/packages/data-portal/app/components/Dataset/RunsTable.tsx b/frontend/packages/data-portal/app/components/Dataset/RunsTable.tsx index c1a39521c..d3983c7ad 100644 --- a/frontend/packages/data-portal/app/components/Dataset/RunsTable.tsx +++ b/frontend/packages/data-portal/app/components/Dataset/RunsTable.tsx @@ -4,7 +4,7 @@ import Skeleton from '@mui/material/Skeleton' import { useNavigate, useSearchParams } from '@remix-run/react' import { ColumnDef, createColumnHelper } from '@tanstack/react-table' import { range } from 'lodash-es' -import { useEffect, useMemo, useState } from 'react' +import { useCallback, useEffect, useMemo, useState } from 'react' import { GetDatasetByIdQuery } from 'app/__generated__/graphql' import { AnnotatedObjectsList } from 'app/components/AnnotatedObjectsList' @@ -16,6 +16,7 @@ import { TiltSeriesQualityScoreBadge } from 'app/components/TiltSeriesQualitySco import { ViewTomogramButton } from 'app/components/ViewTomogramButton' import { RUN_FILTERS } from 'app/constants/filterQueryParams' import { MAX_PER_PAGE } from 'app/constants/pagination' +import { QueryParams } from 'app/constants/query' import { RunTableWidths } from 'app/constants/table' import { TiltSeriesScore } from 'app/constants/tiltSeries' import { useDatasetById } from 'app/hooks/useDatasetById' @@ -27,6 +28,7 @@ import { } from 'app/state/filterHistory' import { cnsNoMerge } from 'app/utils/cns' import { inQualityScoreRange } from 'app/utils/tiltSeries' +import { carryOverFilterParams, createUrl } from 'app/utils/url' type Run = GetDatasetByIdQuery['datasets'][number]['runs'][number] @@ -39,7 +41,7 @@ const LOADING_RUNS = range(0, MAX_PER_PAGE).map(() => ({ export function RunsTable() { const { isLoadingDebounced } = useIsLoading() - const { dataset } = useDatasetById() + const { dataset, deposition } = useDatasetById() const runs = dataset.runs as unknown as Run[] const { t } = useI18n() const { setSingleDatasetHistory } = useSingleDatasetFilterHistory() @@ -61,6 +63,25 @@ export function RunsTable() { [searchParams, setSingleDatasetHistory], ) + const getRunUrl = useCallback( + (id: number) => { + const url = createUrl(`/runs/${id}`) + + carryOverFilterParams({ + filters: RUN_FILTERS, + params: url.searchParams, + prevParams: searchParams, + }) + + if (deposition && searchParams.has(QueryParams.DepositionId)) { + url.searchParams.set(QueryParams.DepositionId, `${deposition.id}`) + } + + return url.pathname + url.search + }, + [deposition, searchParams], + ) + const columns = useMemo(() => { const columnHelper = createColumnHelper() @@ -106,7 +127,7 @@ export function RunsTable() { ), cell({ row: { original: run } }) { - const runUrl = `/runs/${run.id}` + const runUrl = getRunUrl(run.id) return ( [] }, [ - dataset.id, - dataset.organism_name, isLoadingDebounced, - t, isHoveringOverInteractable, + t, + getRunUrl, + dataset.id, + dataset.organism_name, ]) return ( @@ -260,7 +282,7 @@ export function RunsTable() { data={isLoadingDebounced ? LOADING_RUNS : runs} columns={columns} onTableRowClick={(row) => - !isHoveringOverInteractable && navigate(`/runs/${row.original.id}`) + !isHoveringOverInteractable && navigate(getRunUrl(row.original.id)) } hoverType="group" /> diff --git a/frontend/packages/data-portal/app/components/Deposition/DatasetsTable.tsx b/frontend/packages/data-portal/app/components/Deposition/DatasetsTable.tsx index bba62b91f..62210bd3f 100644 --- a/frontend/packages/data-portal/app/components/Deposition/DatasetsTable.tsx +++ b/frontend/packages/data-portal/app/components/Deposition/DatasetsTable.tsx @@ -5,7 +5,7 @@ import Skeleton from '@mui/material/Skeleton' import { useSearchParams } from '@remix-run/react' import { ColumnDef, createColumnHelper } from '@tanstack/react-table' import { range, sum } from 'lodash-es' -import { useMemo } from 'react' +import { useCallback, useMemo } from 'react' import { AnnotatedObjectsList } from 'app/components/AnnotatedObjectsList' import { AuthorList } from 'app/components/AuthorList' @@ -13,7 +13,9 @@ import { I18n } from 'app/components/I18n' import { KeyPhoto } from 'app/components/KeyPhoto' import { Link } from 'app/components/Link' import { CellHeader, PageTable, TableCell } from 'app/components/Table' +import { DATASET_FILTERS } from 'app/constants/filterQueryParams' import { ANNOTATED_OBJECTS_MAX, MAX_PER_PAGE } from 'app/constants/pagination' +import { QueryParams } from 'app/constants/query' import { DepositionPageDatasetTableWidths } from 'app/constants/table' import { Dataset, useDepositionById } from 'app/hooks/useDepositionById' import { useI18n } from 'app/hooks/useI18n' @@ -22,6 +24,7 @@ import { LogLevel } from 'app/types/logging' import { cnsNoMerge } from 'app/utils/cns' import { sendLogs } from 'app/utils/logging' import { getErrorMessage } from 'app/utils/string' +import { carryOverFilterParams, createUrl } from 'app/utils/url' const LOADING_DATASETS = range(0, MAX_PER_PAGE).map( (value) => @@ -45,6 +48,23 @@ export function DatasetsTable() { const { isLoadingDebounced } = useIsLoading() + const getDatasetUrl = useCallback( + (id: number) => { + const url = createUrl(`/datasets/${id}`) + + carryOverFilterParams({ + filters: DATASET_FILTERS, + params: url.searchParams, + prevParams: searchParams, + }) + + url.searchParams.set(QueryParams.DepositionId, `${id}`) + + return url.pathname + url.search + }, + [searchParams], + ) + const columns = useMemo(() => { const columnHelper = createColumnHelper() @@ -54,7 +74,7 @@ export function DatasetsTable() { header: () =>

, cell({ row: { original: dataset } }) { - const datasetUrl = `/datasets/${dataset.id}` + const datasetUrl = getDatasetUrl(dataset.id) return ( (QueryParams.DepositionId) + + return ( + +

+

+ +

+ + +
+ + ) +} diff --git a/frontend/packages/data-portal/app/components/Link/Link.tsx b/frontend/packages/data-portal/app/components/Link/Link.tsx index 6075fc685..39149cb7c 100644 --- a/frontend/packages/data-portal/app/components/Link/Link.tsx +++ b/frontend/packages/data-portal/app/components/Link/Link.tsx @@ -1,15 +1,13 @@ import { Link as RemixLink, LinkProps } from '@remix-run/react' import { ForwardedRef, forwardRef } from 'react' +import { + DASHED_BORDERED_CLASSES, + DASHED_UNDERLINED_CLASSES, +} from 'app/utils/classNames' import { cnsNoMerge } from 'app/utils/cns' import { isExternalUrl } from 'app/utils/url' -export const DASHED_BORDERED_CLASSES = - 'border-b border-dashed hover:border-solid border-current' - -export const DASHED_UNDERLINED_CLASSES = - 'underline underline-offset-[3px] decoration-dashed hover:decoration-solid' - export type VariantLinkProps = LinkProps & { variant?: 'dashed-bordered' | 'dashed-underlined' } diff --git a/frontend/packages/data-portal/app/components/Run/AnnotationTable.tsx b/frontend/packages/data-portal/app/components/Run/AnnotationTable.tsx index 9995e6735..7f50f446b 100644 --- a/frontend/packages/data-portal/app/components/Run/AnnotationTable.tsx +++ b/frontend/packages/data-portal/app/components/Run/AnnotationTable.tsx @@ -13,7 +13,6 @@ import { ComponentProps, ReactNode, useCallback, useMemo } from 'react' import { AuthorList } from 'app/components/AuthorList' import { I18n } from 'app/components/I18n' -import { DASHED_BORDERED_CLASSES } from 'app/components/Link' import { CellHeader, PageTable, TableCell } from 'app/components/Table' import { Tooltip } from 'app/components/Tooltip' import { @@ -35,6 +34,7 @@ import { import { useRunById } from 'app/hooks/useRunById' import { AnnotationRow, useAnnotation } from 'app/state/annotation' import { I18nKeys } from 'app/types/i18n' +import { DASHED_BORDERED_CLASSES } from 'app/utils/classNames' import { cns, cnsNoMerge } from 'app/utils/cns' const LOADING_ANNOTATIONS = range(0, MAX_PER_PAGE).map(() => ({ diff --git a/frontend/packages/data-portal/app/components/TablePageLayout.tsx b/frontend/packages/data-portal/app/components/TablePageLayout.tsx index 5a557e1f9..bec259868 100644 --- a/frontend/packages/data-portal/app/components/TablePageLayout.tsx +++ b/frontend/packages/data-portal/app/components/TablePageLayout.tsx @@ -14,6 +14,7 @@ import { TableCount } from './Table/TableCount' import { Tabs } from './Tabs' export interface TablePageLayoutProps { + banner?: ReactNode header?: ReactNode tabs: TableLayoutTab[] // If there is only 1 tab, the tab selector will not show. @@ -26,6 +27,7 @@ export interface TablePageLayoutProps { export interface TableLayoutTab { title: string + banner?: ReactNode filterPanel?: ReactNode table: ReactNode @@ -44,6 +46,7 @@ export function TablePageLayout({ tabsTitle, downloadModal, drawers, + banner, }: TablePageLayoutProps) { const [searchParams, setSearchParams] = useSearchParams() @@ -87,7 +90,7 @@ export function TablePageLayout({ )} - + {drawers} @@ -105,6 +108,7 @@ function TablePageTabContent({ pageQueryParamKey = QueryParams.Page, totalCount, countLabel, + banner, }: TableLayoutTab) { const [searchParams, setSearchParams] = useSearchParams() const pageQueryParamValue = +(searchParams.get(pageQueryParamKey) ?? '1') @@ -165,6 +169,8 @@ function TablePageTabContent({ filterPanel && 'screen-2040:translate-x-[-100px] max-w-content', )} > +
{banner}
+

{title} diff --git a/frontend/packages/data-portal/app/constants/filterQueryParams.ts b/frontend/packages/data-portal/app/constants/filterQueryParams.ts index 9117747e1..186fadec8 100644 --- a/frontend/packages/data-portal/app/constants/filterQueryParams.ts +++ b/frontend/packages/data-portal/app/constants/filterQueryParams.ts @@ -1,12 +1,10 @@ import { QueryParams } from './query' -export const DATASET_FILTERS = [ +const COMMON_DATASET_FILTERS = [ QueryParams.GroundTruthAnnotation, QueryParams.AvailableFiles, QueryParams.NumberOfRuns, QueryParams.DatasetId, - QueryParams.EmpiarId, - QueryParams.EmdbId, QueryParams.AuthorName, QueryParams.AuthorOrcid, QueryParams.Organism, @@ -20,6 +18,13 @@ export const DATASET_FILTERS = [ QueryParams.ObjectShapeType, ] as const +export const DATASET_FILTERS = [ + ...COMMON_DATASET_FILTERS, + QueryParams.EmpiarId, + QueryParams.EmdbId, + QueryParams.DepositionId, +] as const + export const RUN_FILTERS = [ QueryParams.GroundTruthAnnotation, QueryParams.QualityScore, @@ -27,6 +32,7 @@ export const RUN_FILTERS = [ QueryParams.TiltRangeMax, QueryParams.ObjectName, QueryParams.ObjectShapeType, + QueryParams.DepositionId, ] as const export const ANNOTATION_FILTERS = [ @@ -38,3 +44,8 @@ export const ANNOTATION_FILTERS = [ QueryParams.MethodType, QueryParams.AnnotationSoftware, ] as const + +export const DEPOSITION_FILTERS = [ + ...COMMON_DATASET_FILTERS, + QueryParams.ObjectId, +] as const diff --git a/frontend/packages/data-portal/app/constants/query.ts b/frontend/packages/data-portal/app/constants/query.ts index 1e4d85f88..fb303ce0d 100644 --- a/frontend/packages/data-portal/app/constants/query.ts +++ b/frontend/packages/data-portal/app/constants/query.ts @@ -1,12 +1,13 @@ export enum QueryParams { AnnotationId = 'annotation_id', - AnnotationsPage = 'annotations-page', AnnotationSoftware = 'annotation-software', + AnnotationsPage = 'annotations-page', AuthorName = 'author', AuthorOrcid = 'author_orcid', AvailableFiles = 'files', CameraManufacturer = 'camera_manufacturer', DatasetId = 'dataset_id', + DepositionId = 'deposition-id', DownloadConfig = 'download-config', DownloadStep = 'download-step', DownloadTab = 'download-tab', diff --git a/frontend/packages/data-portal/app/graphql/getDatasetById.server.ts b/frontend/packages/data-portal/app/graphql/getDatasetById.server.ts index 8b789f5f4..9b40d3556 100644 --- a/frontend/packages/data-portal/app/graphql/getDatasetById.server.ts +++ b/frontend/packages/data-portal/app/graphql/getDatasetById.server.ts @@ -16,6 +16,7 @@ const GET_DATASET_BY_ID = gql(` $run_limit: Int, $run_offset: Int, $filter: runs_bool_exp, + $deposition_id: Int, ) { datasets(where: { id: { _eq: $id } }) { s3_prefix @@ -149,6 +150,11 @@ const GET_DATASET_BY_ID = gql(` } } } + + deposition: datasets(where: { id: { _eq: $deposition_id } }) { + id + title + } } `) @@ -221,11 +227,13 @@ function getFilter(filterState: FilterState) { export async function getDatasetById({ client, + depositionId = -1, id, page = 1, params = new URLSearchParams(), }: { client: ApolloClient + depositionId?: number id: number page?: number params?: URLSearchParams @@ -234,6 +242,7 @@ export async function getDatasetById({ query: GET_DATASET_BY_ID, variables: { id, + deposition_id: depositionId, run_limit: MAX_PER_PAGE, run_offset: (page - 1) * MAX_PER_PAGE, filter: getFilter(getFilterState(params)), diff --git a/frontend/packages/data-portal/app/graphql/getRunById.server.ts b/frontend/packages/data-portal/app/graphql/getRunById.server.ts index 69eb268d8..d90114b2a 100644 --- a/frontend/packages/data-portal/app/graphql/getRunById.server.ts +++ b/frontend/packages/data-portal/app/graphql/getRunById.server.ts @@ -3,7 +3,6 @@ import type { ApolloQueryResult, NormalizedCacheObject, } from '@apollo/client' -import { URLSearchParams } from 'url' import { gql } from 'app/__generated__' import { @@ -21,6 +20,7 @@ const GET_RUN_BY_ID_QUERY = gql(` $annotationsOffset: Int, $filter: [annotations_bool_exp!], $fileFilter: [annotation_files_bool_exp!] + $deposition_id: Int, ) { runs(where: { id: { _eq: $id } }) { id @@ -405,6 +405,11 @@ const GET_RUN_BY_ID_QUERY = gql(` count } } + + deposition: datasets(where: { id: { _eq: $deposition_id } }) { + id + title + } } `) @@ -492,11 +497,13 @@ export async function getRunById({ id, annotationsPage, params = new URLSearchParams(), + depositionId = -1, }: { client: ApolloClient id: number annotationsPage: number params?: URLSearchParams + depositionId?: number }): Promise> { return client.query({ query: GET_RUN_BY_ID_QUERY, @@ -506,6 +513,7 @@ export async function getRunById({ annotationsOffset: (annotationsPage - 1) * MAX_PER_PAGE, filter: getFilter(getFilterState(params)), fileFilter: getFileFilter(getFilterState(params)), + deposition_id: depositionId, }, }) } diff --git a/frontend/packages/data-portal/app/hooks/useDatasetById.ts b/frontend/packages/data-portal/app/hooks/useDatasetById.ts index dae14674b..f6b329853 100644 --- a/frontend/packages/data-portal/app/hooks/useDatasetById.ts +++ b/frontend/packages/data-portal/app/hooks/useDatasetById.ts @@ -6,6 +6,7 @@ import { GetDatasetByIdQuery } from 'app/__generated__/graphql' export function useDatasetById() { const { datasets: [dataset], + deposition, } = useTypedLoaderData() const objectNames = useMemo( @@ -40,5 +41,10 @@ export function useDatasetById() { [dataset.run_stats], ) - return { dataset, objectNames, objectShapeTypes } + return { + dataset, + objectNames, + objectShapeTypes, + deposition: deposition.at(0), + } } diff --git a/frontend/packages/data-portal/app/hooks/useRunById.ts b/frontend/packages/data-portal/app/hooks/useRunById.ts index 3007b589b..2dde4715e 100644 --- a/frontend/packages/data-portal/app/hooks/useRunById.ts +++ b/frontend/packages/data-portal/app/hooks/useRunById.ts @@ -57,5 +57,6 @@ export function useRunById() { resolutions, annotationFilesAggregates, tomogramsCount, + deposition: data.deposition.at(0), } } diff --git a/frontend/packages/data-portal/app/routes/datasets.$id.tsx b/frontend/packages/data-portal/app/routes/datasets.$id.tsx index 40fbe1974..355ed3c1b 100644 --- a/frontend/packages/data-portal/app/routes/datasets.$id.tsx +++ b/frontend/packages/data-portal/app/routes/datasets.$id.tsx @@ -3,16 +3,18 @@ import { json, LoaderFunctionArgs } from '@remix-run/server-runtime' import { apolloClient } from 'app/apollo.server' -import { TablePageLayout } from 'app/components//TablePageLayout' import { DatasetMetadataDrawer } from 'app/components/Dataset' import { DatasetHeader } from 'app/components/Dataset/DatasetHeader' import { RunsTable } from 'app/components/Dataset/RunsTable' +import { DepositionFilterBanner } from 'app/components/DepositionFilterBanner' import { DownloadModal } from 'app/components/Download' import { RunFilter } from 'app/components/RunFilter' +import { TablePageLayout } from 'app/components/TablePageLayout' import { QueryParams } from 'app/constants/query' import { getDatasetById } from 'app/graphql/getDatasetById.server' import { useDatasetById } from 'app/hooks/useDatasetById' import { useI18n } from 'app/hooks/useI18n' +import { useQueryParam } from 'app/hooks/useQueryParam' import { i18n } from 'app/i18n' export async function loader({ params, request }: LoaderFunctionArgs) { @@ -20,6 +22,7 @@ export async function loader({ params, request }: LoaderFunctionArgs) { const url = new URL(request.url) const page = +(url.searchParams.get(QueryParams.Page) ?? '1') + const depositionId = Number(url.searchParams.get(QueryParams.DepositionId)) if (Number.isNaN(+id)) { throw new Response(null, { @@ -33,6 +36,7 @@ export async function loader({ params, request }: LoaderFunctionArgs) { page, client: apolloClient, params: url.searchParams, + depositionId: Number.isNaN(depositionId) ? undefined : depositionId, }) if (data.datasets.length === 0) { @@ -46,11 +50,21 @@ export async function loader({ params, request }: LoaderFunctionArgs) { } export default function DatasetByIdPage() { - const { dataset } = useDatasetById() + const { dataset, deposition } = useDatasetById() const { t } = useI18n() + const [depositionId] = useQueryParam(QueryParams.DepositionId) return ( + ) + } header={} tabs={[ { diff --git a/frontend/packages/data-portal/app/routes/depositions.$id.tsx b/frontend/packages/data-portal/app/routes/depositions.$id.tsx index d36e8ded0..09b58ba81 100644 --- a/frontend/packages/data-portal/app/routes/depositions.$id.tsx +++ b/frontend/packages/data-portal/app/routes/depositions.$id.tsx @@ -11,12 +11,16 @@ import { DepositionMetadataDrawer } from 'app/components/Deposition' import { DatasetsTable } from 'app/components/Deposition/DatasetsTable' import { DepositionHeader } from 'app/components/Deposition/DepositionHeader' import { TablePageLayout } from 'app/components/TablePageLayout' +import { DEPOSITION_FILTERS } from 'app/constants/filterQueryParams' import { QueryParams } from 'app/constants/query' import { getDatasetsFilterData } from 'app/graphql/getDatasetsFilterData.server' import { getDepositionById } from 'app/graphql/getDepositionById.server' import { useDepositionById } from 'app/hooks/useDepositionById' import { useI18n } from 'app/hooks/useI18n' -import { useDepositionHistory } from 'app/state/filterHistory' +import { + useDepositionHistory, + useSyncParamsWithState, +} from 'app/state/filterHistory' import { getFeatureFlag } from 'app/utils/featureFlags' export async function loader({ params, request }: LoaderFunctionArgs) { @@ -84,12 +88,19 @@ export default function DepositionByIdPage() { const { deposition } = useDepositionById() const { t } = useI18n() - const { setPreviousDepositionId } = useDepositionHistory() + const { setPreviousDepositionId, setPreviousSingleDepositionParams } = + useDepositionHistory() + useEffect( () => setPreviousDepositionId(deposition.id), [deposition.id, setPreviousDepositionId], ) + useSyncParamsWithState({ + filters: DEPOSITION_FILTERS, + setHistory: setPreviousSingleDepositionParams, + }) + return ( } diff --git a/frontend/packages/data-portal/app/routes/runs.$id.tsx b/frontend/packages/data-portal/app/routes/runs.$id.tsx index 6038bc972..85821f613 100644 --- a/frontend/packages/data-portal/app/routes/runs.$id.tsx +++ b/frontend/packages/data-portal/app/routes/runs.$id.tsx @@ -7,8 +7,8 @@ import { useMemo } from 'react' import { match } from 'ts-pattern' import { apolloClient } from 'app/apollo.server' -import { TablePageLayout } from 'app/components//TablePageLayout' import { AnnotationFilter } from 'app/components/AnnotationFilter/AnnotationFilter' +import { DepositionFilterBanner } from 'app/components/DepositionFilterBanner' import { DownloadModal } from 'app/components/Download' import { RunHeader } from 'app/components/Run' import { AnnotationDrawer } from 'app/components/Run/AnnotationDrawer' @@ -16,11 +16,13 @@ import { AnnotationTable } from 'app/components/Run/AnnotationTable' import { RunMetadataDrawer } from 'app/components/Run/RunMetadataDrawer' import { TomogramMetadataDrawer } from 'app/components/Run/TomogramMetadataDrawer' import { TomogramsTable } from 'app/components/Run/TomogramTable' +import { TablePageLayout } from 'app/components/TablePageLayout' import { QueryParams } from 'app/constants/query' import { getRunById } from 'app/graphql/getRunById.server' import { useDownloadModalQueryParamState } from 'app/hooks/useDownloadModalQueryParamState' import { useFileSize } from 'app/hooks/useFileSize' import { useI18n } from 'app/hooks/useI18n' +import { useQueryParam } from 'app/hooks/useQueryParam' import { useRunById } from 'app/hooks/useRunById' import { BaseAnnotation } from 'app/state/annotation' import { DownloadConfig } from 'app/types/download' @@ -41,10 +43,12 @@ export async function loader({ request, params }: LoaderFunctionArgs) { const annotationsPage = +( url.searchParams.get(QueryParams.AnnotationsPage) ?? '1' ) + const depositionId = +(url.searchParams.get(QueryParams.DepositionId) ?? '-1') const { data } = await getRunById({ id, annotationsPage, + depositionId, client: apolloClient, params: url.searchParams, }) @@ -84,6 +88,7 @@ export default function RunByIdPage() { tomogramsForDownload, annotationFilesAggregates, tomogramsCount, + deposition, } = useRunById() const { @@ -135,10 +140,21 @@ export default function RunByIdPage() { enabled: fileFormat !== 'zarr', }) + const [depositionId] = useQueryParam(QueryParams.DepositionId) + return ( } tabsTitle={multipleTomogramsEnabled ? t('browseRunData') : undefined} + banner={ + depositionId && + deposition && ( + + ) + } tabs={[ { title: t('annotations'), diff --git a/frontend/packages/data-portal/app/state/filterHistory.ts b/frontend/packages/data-portal/app/state/filterHistory.ts index d110ca2a1..53994c7ba 100644 --- a/frontend/packages/data-portal/app/state/filterHistory.ts +++ b/frontend/packages/data-portal/app/state/filterHistory.ts @@ -1,6 +1,9 @@ +import { useSearchParams } from '@remix-run/react' import { atom, useAtom } from 'jotai' +import { useEffect } from 'react' import { DATASET_FILTERS, RUN_FILTERS } from 'app/constants/filterQueryParams' +import { QueryParams } from 'app/constants/query' export type BrowseDatasetHistory = Map< (typeof DATASET_FILTERS)[number], @@ -14,6 +17,7 @@ export type SingleDatasetHistory = Map< const browseDatasetHistoryAtom = atom(null) const singleDatasetHistoryAtom = atom(null) const previousDepositionIdAtom = atom(null) +const previousSingleDepositionParamsAtom = atom('') export function useBrowseDatasetFilterHistory() { const [browseDatasetHistory, setBrowseDatasetHistory] = useAtom( @@ -42,8 +46,37 @@ export function useDepositionHistory() { previousDepositionIdAtom, ) + const [previousSingleDepositionParams, setPreviousSingleDepositionParams] = + useAtom(previousSingleDepositionParamsAtom) + return { previousDepositionId, setPreviousDepositionId, + previousSingleDepositionParams, + setPreviousSingleDepositionParams, } } + +export function useSyncParamsWithState({ + filters, + setHistory, +}: { + filters: readonly QueryParams[] + setHistory(params: string): void +}) { + const [searchParams] = useSearchParams() + + useEffect(() => { + const newParams = new URLSearchParams(searchParams) + + for (const key of newParams.keys()) { + if (!filters.includes(key as QueryParams)) { + newParams.delete(key) + } + } + + newParams.sort() + + setHistory(newParams.toString()) + }, [filters, searchParams, setHistory]) +} diff --git a/frontend/packages/data-portal/app/utils/classNames.ts b/frontend/packages/data-portal/app/utils/classNames.ts new file mode 100644 index 000000000..195a3c3b1 --- /dev/null +++ b/frontend/packages/data-portal/app/utils/classNames.ts @@ -0,0 +1,5 @@ +export const DASHED_BORDERED_CLASSES = + 'border-b border-dashed hover:border-solid border-current' + +export const DASHED_UNDERLINED_CLASSES = + 'underline underline-offset-[3px] decoration-dashed hover:decoration-solid' diff --git a/frontend/packages/data-portal/app/utils/url.test.ts b/frontend/packages/data-portal/app/utils/url.test.ts index ee7b7c4d4..2ce35ba28 100644 --- a/frontend/packages/data-portal/app/utils/url.test.ts +++ b/frontend/packages/data-portal/app/utils/url.test.ts @@ -1,4 +1,6 @@ -import { createUrl, isExternalUrl } from './url' +import { QueryParams } from 'app/constants/query' + +import { carryOverFilterParams, createUrl, isExternalUrl } from './url' describe('utils/url', () => { describe('isExternalUrl()', () => { @@ -32,4 +34,40 @@ describe('utils/url', () => { } }) }) + + describe('carryOverFilterParams()', () => { + it('should carry over filter params', () => { + const testCases = [ + { + input: { + filters: [QueryParams.ObjectName, QueryParams.Organism], + params: new URLSearchParams(), + prevParams: new URLSearchParams([ + [QueryParams.ObjectName, 'foobar'], + [QueryParams.ReconstructionMethod, 'foobar'], + ]), + }, + output: new URLSearchParams([[QueryParams.ObjectName, 'foobar']]), + }, + + { + input: { + filters: [QueryParams.ObjectName, QueryParams.Organism], + params: new URLSearchParams(), + prevParams: new URLSearchParams([ + [QueryParams.ReconstructionMethod, 'foobar'], + ]), + }, + output: new URLSearchParams(), + }, + ] as const + + for (const testCase of testCases) { + const { input, output } = testCase + const result = carryOverFilterParams(input) + + expect(output.toString()).toBe(result.toString()) + } + }) + }) }) diff --git a/frontend/packages/data-portal/app/utils/url.ts b/frontend/packages/data-portal/app/utils/url.ts index 1ee19c949..e0c044485 100644 --- a/frontend/packages/data-portal/app/utils/url.ts +++ b/frontend/packages/data-portal/app/utils/url.ts @@ -1,3 +1,5 @@ +import { QueryParams } from 'app/constants/query' + /** * Checks if the string is an external URL. This works by using the value to * create a URL object. URL objects will throw errors for relative URLs if a @@ -37,3 +39,23 @@ export function createUrl(urlOrPath: string, baseUrl?: string): URL { export function getNeuroglancerUrl(config: string): string { return `https://neuroglancer-demo.appspot.com/#!${encodeURIComponent(config)}` } + +export function carryOverFilterParams({ + filters, + params, + prevParams, +}: { + filters: readonly QueryParams[] + params: URLSearchParams + prevParams: URLSearchParams +}) { + for (const key of filters) { + const value = prevParams.get(key) + + if (value) { + params.set(key, value) + } + } + + return params +} diff --git a/frontend/packages/data-portal/public/locales/en/translation.json b/frontend/packages/data-portal/public/locales/en/translation.json index fd2484935..da1fd8057 100644 --- a/frontend/packages/data-portal/public/locales/en/translation.json +++ b/frontend/packages/data-portal/public/locales/en/translation.json @@ -238,6 +238,8 @@ "objectShapeType": "Object Shape Type", "objectShapeTypes": "Object Shape Types", "objectState": "Object State", + "onlyDisplayingAnnotations": "Only displaying annotations from Deposition CZCDP-{{id}}: \"{{title}}\"", + "onlyDisplayingRunsWithAnnotations": "Only displaying runs with annotations from Deposition CZCDP-{{id}}: \"{{title}}\"", "openDataset": "Open Dataset", "openDeposition": "Open Deposition", "openRun": "Open Run",