diff --git a/frontend/packages/data-portal/app/components/TablePageLayout.tsx b/frontend/packages/data-portal/app/components/TablePageLayout.tsx
index ce3c68c20..24ab8c9c4 100644
--- a/frontend/packages/data-portal/app/components/TablePageLayout.tsx
+++ b/frontend/packages/data-portal/app/components/TablePageLayout.tsx
@@ -4,54 +4,121 @@ import { ReactNode, useEffect, useMemo } from 'react'
import { TABLE_PAGE_LAYOUT_LOG_ID } from 'app/constants/error'
import { MAX_PER_PAGE } from 'app/constants/pagination'
+import { QueryParams } from 'app/constants/query'
import { TestIds } from 'app/constants/testIds'
import { LayoutContext, LayoutContextValue } from 'app/context/Layout.context'
import { cns } from 'app/utils/cns'
import { ErrorBoundary } from './ErrorBoundary'
import { TableCount } from './Table/TableCount'
+import { Tabs } from './Tabs'
+export interface TablePageLayoutProps {
+ header?: ReactNode
+
+ tabs: TableLayoutTab[] // If there is only 1 tab, the tab selector will not show.
+ tabsTitle?: string
+
+ downloadModal?: ReactNode
+ drawers?: ReactNode
+}
+
+export interface TableLayoutTab {
+ title: string
+
+ filterPanel?: ReactNode
+
+ table: ReactNode
+ noResults?: ReactNode
+ pageQueryParamKey?: string
+
+ filteredCount: number
+ totalCount: number
+ countLabel: string // e.g. "objects" in "1 of 3 objects".
+}
+
+/** Standard page structure for browsing + filtering list(s) of objects. */
export function TablePageLayout({
+ header,
+ tabs,
+ tabsTitle,
downloadModal,
drawers,
+}: TablePageLayoutProps) {
+ const [searchParams, setSearchParams] = useSearchParams()
+
+ const activeTabTitle = searchParams.get(QueryParams.TableTab)
+ const activeTab = tabs.find((tab) => tab.title === activeTabTitle) ?? tabs[0]
+
+ return (
+ <>
+ {downloadModal}
+
+
+ {header}
+
+ {tabs.length > 1 && (
+ <>
+ {tabsTitle &&
{tabsTitle}
}
+
{
+ setSearchParams((prev) => {
+ prev.set(QueryParams.TableTab, tabTitle)
+ return prev
+ })
+ }}
+ tabs={tabs.map((tab) => ({
+ label: (
+ <>
+ {tab.title}
+
+ {tab.filteredCount}
+
+ >
+ ),
+ value: tab.title,
+ }))}
+ />
+ >
+ )}
+
+
+ {drawers}
+
+ >
+ )
+}
+
+/** Table + filters for 1 tab. */
+function TablePageTabContent({
+ title,
+ filterPanel,
filteredCount,
- filters: filterPanel,
- header,
- noResults,
table,
- title,
+ noResults,
+ pageQueryParamKey = QueryParams.Page,
totalCount,
- type,
-}: {
- downloadModal?: ReactNode
- drawers?: ReactNode
- filteredCount: number
- filters?: ReactNode
- header?: ReactNode
- noResults?: ReactNode
- table: ReactNode
- title: string
- totalCount: number
- type: string
-}) {
+ countLabel,
+}: TableLayoutTab) {
const [searchParams, setSearchParams] = useSearchParams()
- const page = +(searchParams.get('page') ?? '1')
+ const pageQueryParamValue = +(searchParams.get(pageQueryParamKey) ?? '1')
useEffect(() => {
- if (Math.ceil(filteredCount / MAX_PER_PAGE) < page) {
+ if (Math.ceil(filteredCount / MAX_PER_PAGE) < pageQueryParamValue) {
setSearchParams(
(prev) => {
- prev.delete('page')
+ prev.delete(pageQueryParamKey)
return prev
},
{ replace: true },
)
}
- }, [filteredCount, page, setSearchParams])
+ }, [filteredCount, pageQueryParamKey, pageQueryParamValue, setSearchParams])
function setPage(nextPage: number) {
setSearchParams((prev) => {
- prev.set('page', `${nextPage}`)
+ prev.set(pageQueryParamKey, `${nextPage}`)
return prev
})
}
@@ -65,83 +132,73 @@ export function TablePageLayout({
return (
- {downloadModal}
-
-
- {header}
-
-
- {filterPanel && (
-
- {filterPanel}
-
+
+ {filterPanel && (
+
+ {filterPanel}
+
+ )}
+
+
-
- // Translate to the left by half the filter panel width to align with the header
- filterPanel && 'screen-2040:translate-x-[-100px] max-w-content',
+
+ {table}
+
+
+
+ {filteredCount === 0 && noResults}
+
+ {filteredCount > MAX_PER_PAGE && (
+
+
setPage(pageQueryParamValue + 1)}
+ onPreviousPage={() => setPage(pageQueryParamValue - 1)}
+ onPageChange={(nextPage) => setPage(nextPage)}
+ />
+
)}
- >
-
-
-
- {table}
-
-
-
- {filteredCount === 0 && noResults}
-
- {filteredCount > MAX_PER_PAGE && (
-
-
setPage(page + 1)}
- onPreviousPage={() => setPage(page - 1)}
- onPageChange={(nextPage) => setPage(nextPage)}
- />
-
- )}
-
-
- {drawers}
)
diff --git a/frontend/packages/data-portal/app/components/Tabs.tsx b/frontend/packages/data-portal/app/components/Tabs.tsx
index 5e61442b4..852a4554b 100644
--- a/frontend/packages/data-portal/app/components/Tabs.tsx
+++ b/frontend/packages/data-portal/app/components/Tabs.tsx
@@ -1,14 +1,15 @@
import Tab from '@mui/material/Tab'
import MUITabs, { TabsProps } from '@mui/material/Tabs'
+import { ReactNode } from 'react'
import { cns } from 'app/utils/cns'
-export interface TabData
{
- label: string
+export interface TabData {
+ label: ReactNode
value: T
}
-export function Tabs({
+export function Tabs({
tabs,
value,
onChange,
@@ -43,7 +44,7 @@ export function Tabs({
),
selected: '!text-black',
}}
- key={tab.value}
+ key={String(tab.value)}
{...tab}
/>
))}
diff --git a/frontend/packages/data-portal/app/constants/query.ts b/frontend/packages/data-portal/app/constants/query.ts
index a275eb746..8166816ca 100644
--- a/frontend/packages/data-portal/app/constants/query.ts
+++ b/frontend/packages/data-portal/app/constants/query.ts
@@ -1,5 +1,6 @@
export enum QueryParams {
AnnotationId = 'annotation_id',
+ AnnotationsPage = 'annotations-page',
AnnotationSoftware = 'annotation-software',
AuthorName = 'author',
AuthorOrcid = 'author_orcid',
@@ -26,6 +27,7 @@ export enum QueryParams {
ReconstructionMethod = 'reconstruction_method',
ReconstructionSoftware = 'reconstruction_software',
Tab = 'tab',
+ TableTab = 'table-tab',
TiltRangeMax = 'tilt_max',
TiltRangeMin = 'tilt_min',
TomogramProcessing = 'tomogram-processing',
diff --git a/frontend/packages/data-portal/app/graphql/getRunById.server.ts b/frontend/packages/data-portal/app/graphql/getRunById.server.ts
index 1e1c6a742..0c19ae320 100644
--- a/frontend/packages/data-portal/app/graphql/getRunById.server.ts
+++ b/frontend/packages/data-portal/app/graphql/getRunById.server.ts
@@ -15,7 +15,7 @@ import { MAX_PER_PAGE } from 'app/constants/pagination'
import { FilterState, getFilterState } from 'app/hooks/useFilter'
const GET_RUN_BY_ID_QUERY = gql(`
- query GetRunById($id: Int, $limit: Int, $offset: Int, $filter: annotations_bool_exp, $fileFilter: annotation_files_bool_exp) {
+ query GetRunById($id: Int, $limit: Int, $annotationsOffset: Int, $filter: annotations_bool_exp, $fileFilter: annotation_files_bool_exp) {
runs(where: { id: { _eq: $id } }) {
id
name
@@ -128,7 +128,7 @@ const GET_RUN_BY_ID_QUERY = gql(`
annotation_table: tomogram_voxel_spacings {
annotations(
limit: $limit,
- offset: $offset,
+ offset: $annotationsOffset,
where: $filter,
order_by: [
{ ground_truth_status: desc }
@@ -343,12 +343,12 @@ function getFileFilter(filterState: FilterState) {
export async function getRunById({
client,
id,
- page = 1,
+ annotationsPage,
params = new URLSearchParams(),
}: {
client: ApolloClient
id: number
- page?: number
+ annotationsPage: number
params?: URLSearchParams
}): Promise> {
return client.query({
@@ -356,7 +356,7 @@ export async function getRunById({
variables: {
id,
limit: MAX_PER_PAGE,
- offset: (page - 1) * MAX_PER_PAGE,
+ annotationsOffset: (annotationsPage - 1) * MAX_PER_PAGE,
filter: getFilter(getFilterState(params)),
fileFilter: getFileFilter(getFilterState(params)),
},
diff --git a/frontend/packages/data-portal/app/routes/browse-data.datasets.tsx b/frontend/packages/data-portal/app/routes/browse-data.datasets.tsx
index 3405da2fc..c52cd47f5 100644
--- a/frontend/packages/data-portal/app/routes/browse-data.datasets.tsx
+++ b/frontend/packages/data-portal/app/routes/browse-data.datasets.tsx
@@ -11,7 +11,6 @@ import { getBrowseDatasets } from 'app/graphql/getBrowseDatasets.server'
import { useDatasets } from 'app/hooks/useDatasets'
import { useFilter } from 'app/hooks/useFilter'
import { useI18n } from 'app/hooks/useI18n'
-import { i18n } from 'app/i18n'
export async function loader({ request }: LoaderFunctionArgs) {
const url = new URL(request.url)
@@ -45,19 +44,23 @@ export default function BrowseDatasetsPage() {
return (
}
- table={}
- totalCount={datasetCount}
- noResults={
- {i18n.clearFilters}}
- />
- }
+ tabs={[
+ {
+ title: t('datasets'),
+ filterPanel: ,
+ table: ,
+ noResults: (
+ {t('clearFilters')}}
+ />
+ ),
+ filteredCount: filteredDatasetCount,
+ totalCount: datasetCount,
+ countLabel: t('datasets'),
+ },
+ ]}
/>
)
}
diff --git a/frontend/packages/data-portal/app/routes/browse-data.depositions.tsx b/frontend/packages/data-portal/app/routes/browse-data.depositions.tsx
index 53950351e..9f6ad10f6 100644
--- a/frontend/packages/data-portal/app/routes/browse-data.depositions.tsx
+++ b/frontend/packages/data-portal/app/routes/browse-data.depositions.tsx
@@ -56,18 +56,22 @@ export default function BrowseDepositionsPage() {
return (
}
- totalCount={depositionCount}
- noResults={
- {t('clearFilters')}}
- />
- }
+ tabs={[
+ {
+ title: t('depositions'),
+ table: ,
+ noResults: (
+ {t('clearFilters')}}
+ />
+ ),
+ filteredCount: filteredDepositionCount,
+ totalCount: depositionCount,
+ countLabel: t('depositions'),
+ },
+ ]}
/>
)
}
diff --git a/frontend/packages/data-portal/app/routes/datasets.$id.tsx b/frontend/packages/data-portal/app/routes/datasets.$id.tsx
index 56eaf08d6..40fbe1974 100644
--- a/frontend/packages/data-portal/app/routes/datasets.$id.tsx
+++ b/frontend/packages/data-portal/app/routes/datasets.$id.tsx
@@ -3,12 +3,12 @@
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 { 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'
@@ -51,8 +51,17 @@ export default function DatasetByIdPage() {
return (
}
+ tabs={[
+ {
+ title: t('runs'),
+ filterPanel: ,
+ table: ,
+ filteredCount: dataset.filtered_runs_count.aggregate?.count ?? 0,
+ totalCount: dataset.runs_aggregate.aggregate?.count ?? 0,
+ countLabel: i18n.runs,
+ },
+ ]}
downloadModal={
}
drawers={}
- filteredCount={dataset.filtered_runs_count.aggregate?.count ?? 0}
- header={}
- table={}
- totalCount={dataset.runs_aggregate.aggregate?.count ?? 0}
- filters={}
/>
)
}
diff --git a/frontend/packages/data-portal/app/routes/runs.$id.tsx b/frontend/packages/data-portal/app/routes/runs.$id.tsx
index c276b53f0..f63288b25 100644
--- a/frontend/packages/data-portal/app/routes/runs.$id.tsx
+++ b/frontend/packages/data-portal/app/routes/runs.$id.tsx
@@ -7,13 +7,13 @@ 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 { DownloadModal } from 'app/components/Download'
import { RunHeader } from 'app/components/Run'
import { AnnotationDrawer } from 'app/components/Run/AnnotationDrawer'
import { AnnotationTable } from 'app/components/Run/AnnotationTable'
import { RunMetadataDrawer } from 'app/components/Run/RunMetadataDrawer'
-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'
@@ -36,11 +36,13 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
}
const url = new URL(request.url)
- const page = +(url.searchParams.get(QueryParams.Page) ?? '1')
+ const annotationsPage = +(
+ url.searchParams.get(QueryParams.AnnotationsPage) ?? '1'
+ )
const { data } = await getRunById({
id,
- page,
+ annotationsPage,
client: apolloClient,
params: url.searchParams,
})
@@ -66,6 +68,7 @@ export function shouldRevalidate(args: ShouldRevalidateFunctionArgs) {
QueryParams.ObjectShapeType,
QueryParams.MethodType,
QueryParams.AnnotationSoftware,
+ QueryParams.AnnotationsPage,
],
})
}
@@ -175,8 +178,18 @@ export default function RunByIdPage() {
return (
}
+ tabs={[
+ {
+ title: t('annotations'),
+ filterPanel: ,
+ filteredCount,
+ table: ,
+ pageQueryParamKey: QueryParams.AnnotationsPage,
+ totalCount,
+ countLabel: i18n.annotations,
+ },
+ ]}
downloadModal={
>
}
- filters={}
- filteredCount={filteredCount}
- header={}
- table={}
- totalCount={totalCount}
/>
)
}
diff --git a/frontend/packages/data-portal/e2e/dialogs/singleRun.ts b/frontend/packages/data-portal/e2e/dialogs/singleRun.ts
index 0ab7b466d..0caf7d814 100644
--- a/frontend/packages/data-portal/e2e/dialogs/singleRun.ts
+++ b/frontend/packages/data-portal/e2e/dialogs/singleRun.ts
@@ -30,7 +30,7 @@ const TOMOTABS = [
async function fetchData() {
const client = getApolloClient()
- return getRunById({ client, id: +E2E_CONFIG.runId })
+ return getRunById({ client, id: +E2E_CONFIG.runId, annotationsPage: 1 })
}
export function testSingleRunDownloadDialog() {
diff --git a/frontend/packages/data-portal/e2e/filters/utils.ts b/frontend/packages/data-portal/e2e/filters/utils.ts
index cf4e72dc7..7ba5c0985 100644
--- a/frontend/packages/data-portal/e2e/filters/utils.ts
+++ b/frontend/packages/data-portal/e2e/filters/utils.ts
@@ -217,14 +217,14 @@ export async function validateAnnotationsTable({
client,
page,
params,
- pageNumber,
+ pageNumber = 1,
id = +E2E_CONFIG.runId,
}: TableValidatorOptions & { id?: number }) {
const { data } = await getRunById({
client,
params,
id,
- page: pageNumber,
+ annotationsPage: pageNumber,
})
await validateTable({
diff --git a/frontend/packages/data-portal/e2e/pageObjects/filters/filtersActor.ts b/frontend/packages/data-portal/e2e/pageObjects/filters/filtersActor.ts
index 9856793fd..a640c779f 100644
--- a/frontend/packages/data-portal/e2e/pageObjects/filters/filtersActor.ts
+++ b/frontend/packages/data-portal/e2e/pageObjects/filters/filtersActor.ts
@@ -46,7 +46,7 @@ export class FiltersActor {
}: {
client: ApolloClient
id: number
- pageNumber?: number
+ pageNumber: number
url: string
queryParamKey?: QueryParams
queryParamValue: string
@@ -63,7 +63,7 @@ export class FiltersActor {
client,
params,
id,
- page: pageNumber,
+ annotationsPage: pageNumber,
})
return data
@@ -166,7 +166,7 @@ export class FiltersActor {
}: {
client: ApolloClient
id: number
- pageNumber?: number
+ pageNumber: number
url: string
queryParamKey?: QueryParams
queryParamValue: string
diff --git a/frontend/packages/data-portal/e2e/pageObjects/filters/utils.ts b/frontend/packages/data-portal/e2e/pageObjects/filters/utils.ts
index c90c067ce..171dbcf47 100644
--- a/frontend/packages/data-portal/e2e/pageObjects/filters/utils.ts
+++ b/frontend/packages/data-portal/e2e/pageObjects/filters/utils.ts
@@ -36,7 +36,7 @@ export function getExpectedUrlWithQueryParams({
export async function getAnnotationsTableTestData({
client,
params,
- pageNumber,
+ pageNumber = 1,
id = +E2E_CONFIG.runId,
}: TableValidatorOptions & { id?: number }): Promise<
ApolloQueryResult['data']
@@ -45,7 +45,7 @@ export async function getAnnotationsTableTestData({
client,
params,
id,
- page: pageNumber,
+ annotationsPage: pageNumber,
})
return data
diff --git a/frontend/packages/data-portal/e2e/pageObjects/metadataDrawer/utils.ts b/frontend/packages/data-portal/e2e/pageObjects/metadataDrawer/utils.ts
index c81a0ea86..33777180d 100644
--- a/frontend/packages/data-portal/e2e/pageObjects/metadataDrawer/utils.ts
+++ b/frontend/packages/data-portal/e2e/pageObjects/metadataDrawer/utils.ts
@@ -177,6 +177,7 @@ export async function getSingleRunTestMetadata(
const { data } = await getRunById({
client,
id: +E2E_CONFIG.runId,
+ annotationsPage: 1,
})
const [run] = data.runs
@@ -207,6 +208,7 @@ export async function getAnnotationTestData(
const { data } = await getRunById({
client,
id: +E2E_CONFIG.runId,
+ annotationsPage: 1,
})
const [run] = data.runs
diff --git a/frontend/packages/eslint-config/typescript.cjs b/frontend/packages/eslint-config/typescript.cjs
index e43a379ef..f52450bd7 100644
--- a/frontend/packages/eslint-config/typescript.cjs
+++ b/frontend/packages/eslint-config/typescript.cjs
@@ -62,6 +62,8 @@ module.exports = {
// Sometimes it's safe to call async functions and not handle their errors.
'@typescript-eslint/no-misused-promises': 'off',
+ // Allow us to use function/variable hoisting.
+ '@typescript-eslint/no-use-before-define': 'off',
'unused-imports/no-unused-imports': 'error',
'unused-imports/no-unused-vars': [