diff --git a/frontend/packages/data-portal/app/components/AccordionMetadataTable.tsx b/frontend/packages/data-portal/app/components/AccordionMetadataTable.tsx new file mode 100644 index 000000000..db766fde1 --- /dev/null +++ b/frontend/packages/data-portal/app/components/AccordionMetadataTable.tsx @@ -0,0 +1,38 @@ +import { ComponentProps } from 'react' + +import { Accordion } from 'app/components/Accordion' +import { MetadataTable } from 'app/components/Table' + +type AccordionProps = Pick< + ComponentProps, + 'id' | 'header' | 'initialOpen' +> +type MetadataTableProps = Pick< + ComponentProps, + 'data' | 'tableCellProps' | 'tableHeaderProps' +> + +export function AccordionMetadataTable({ + data, + header, + id, + initialOpen = true, + tableCellProps, + tableHeaderProps, +}: AccordionProps & MetadataTableProps) { + return ( + + + + ) +} diff --git a/frontend/packages/data-portal/app/components/DatabaseEntry.tsx b/frontend/packages/data-portal/app/components/DatabaseEntry.tsx new file mode 100644 index 000000000..ba1059472 --- /dev/null +++ b/frontend/packages/data-portal/app/components/DatabaseEntry.tsx @@ -0,0 +1,51 @@ +import { Link } from 'app/components/Link' +import { + DatabaseType, + LABEL_MAP, + REGEX_MAP, + URL_MAP, +} from 'app/constants/external-dbs' +import { cns } from 'app/utils/cns' + +interface DatabaseEntryProps { + entry: string + inline?: boolean +} + +export function DatabaseEntry(props: DatabaseEntryProps) { + const { entry, inline } = props + let dbtype: DatabaseType | undefined + let id: string = '' + + for (const [dbt, pattern] of REGEX_MAP) { + const match = pattern.exec(entry) + if (match !== null) { + dbtype = dbt + // eslint-disable-next-line prefer-destructuring + id = match[1] + break + } + } + + if (dbtype === undefined) { + return

{entry}

+ } + + return ( +

+ {(!inline || dbtype === DatabaseType.DOI) && ( + + {LABEL_MAP.get(dbtype)}: + + )} + + {entry} + +

+ ) +} diff --git a/frontend/packages/data-portal/app/components/Dataset/AccordionMetadataTable.tsx b/frontend/packages/data-portal/app/components/Dataset/AccordionMetadataTable.tsx deleted file mode 100644 index 56ea6f76a..000000000 --- a/frontend/packages/data-portal/app/components/Dataset/AccordionMetadataTable.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { ComponentProps } from 'react' - -import { Accordion } from 'app/components/Accordion' -import { MetadataTable } from 'app/components/Table' - -type AccordionProps = Pick, 'id' | 'header'> -type MetadataTableProps = Pick, 'data'> - -export function AccordionMetadataTable({ - data, - header, - id, -}: AccordionProps & MetadataTableProps) { - return ( - - - - ) -} diff --git a/frontend/packages/data-portal/app/components/Dataset/DatasetAuthors.tsx b/frontend/packages/data-portal/app/components/Dataset/DatasetAuthors.tsx new file mode 100644 index 000000000..6c8fc0bea --- /dev/null +++ b/frontend/packages/data-portal/app/components/Dataset/DatasetAuthors.tsx @@ -0,0 +1,69 @@ +import { Dataset_Authors } from 'app/__generated__/graphql' +import { EnvelopeIcon } from 'app/components/icons' +import { Link } from 'app/components/Link' + +export type AuthorInfo = Pick< + Dataset_Authors, + 'name' | 'primary_author_status' | 'corresponding_author_status' | 'email' +> + +export function DatasetAuthors({ + authors, + className, +}: { + authors: AuthorInfo[] + className?: string +}) { + // TODO: make the below grouping more efficient and/or use GraphQL ordering + const authorsPrimary = authors.filter( + (author) => author.primary_author_status, + ) + const authorsCorresponding = authors.filter( + (author) => author.corresponding_author_status, + ) + const authorsOther = authors.filter( + (author) => + !(author.primary_author_status || author.corresponding_author_status), + ) + + const envelopeIcon = ( + + ) + + // TODO: let's find a better way of doing this + return ( +

+ + {authorsPrimary.map((author, i, arr) => ( + <> + {author.name} + {!( + authorsOther.length + authorsCorresponding.length === 0 && + arr.length - 1 === i + ) && '; '} + + ))} + + + {authorsOther.map((author, i, arr) => ( + <> + {author.name} + {!(authorsCorresponding.length === 0 && arr.length - 1 === i) && + '; '} + + ))} + {authorsCorresponding.map((author, i, arr) => ( + <> + {author.name} + {author.email ? ( + {envelopeIcon} + ) : ( + envelopeIcon + )} + {!(arr.length - 1 === i) && '; '} + + ))} + +

+ ) +} diff --git a/frontend/packages/data-portal/app/components/Dataset/DatasetDescription.tsx b/frontend/packages/data-portal/app/components/Dataset/DatasetDescription.tsx index bc30e4b7a..ad7e7c3e6 100644 --- a/frontend/packages/data-portal/app/components/Dataset/DatasetDescription.tsx +++ b/frontend/packages/data-portal/app/components/Dataset/DatasetDescription.tsx @@ -2,18 +2,13 @@ import { Button } from '@czi-sds/components' import clsx from 'clsx' import { useState } from 'react' -import { EnvelopeIcon } from 'app/components/icons' -import { Link } from 'app/components/Link' -import { - DatabaseType, - DOI_ID, - LABEL_MAP, - REGEX_MAP, - URL_MAP, -} from 'app/constants/external-dbs' +import { DatabaseEntry } from 'app/components/DatabaseEntry' +import { DOI_ID } from 'app/constants/external-dbs' import { useDatasetById } from 'app/hooks/useDatasetById' import { i18n } from 'app/i18n' +import { DatasetAuthors } from './DatasetAuthors' + // use clsx here instead of cns since it erroneously merges text-sds-gray-500 and text-sds-caps-xxxs const sectionHeaderStyles = clsx( 'font-semibold uppercase', @@ -21,41 +16,6 @@ const sectionHeaderStyles = clsx( 'text-sds-caps-xxxs leading-sds-caps-xxxs tracking-sds-caps', ) -interface DatabaseEntryProps { - entry: string -} - -function DatabaseEntry(props: DatabaseEntryProps) { - const { entry } = props - let dbtype: DatabaseType | undefined - let id: string = '' - - for (const [dbt, pattern] of REGEX_MAP) { - const match = pattern.exec(entry) - if (match !== null) { - dbtype = dbt - // eslint-disable-next-line prefer-destructuring - id = match[1] - break - } - } - - if (dbtype === undefined) { - return

{entry}

- } - - return ( -

- - {LABEL_MAP.get(dbtype)}: - - - {entry} - -

- ) -} - interface DatabaseListProps { title: string entries?: string[] @@ -76,16 +36,11 @@ function DatabaseList(props: DatabaseListProps) {

{title}

{entries ? ( -
    +
      {entries.map( (e, i) => !(collapsible && collapsed && i + 1 > collapseAfter) && ( -
    • +
    • ), @@ -118,18 +73,6 @@ function DatabaseList(props: DatabaseListProps) { export function DatasetDescription() { const { dataset } = useDatasetById() - // TODO: make the below grouping more efficient and/or use GraphQL ordering - const authorsPrimary = dataset.authors.filter( - (author) => author.primary_author_status, - ) - const authorsCorresponding = dataset.authors.filter( - (author) => author.corresponding_author_status, - ) - const authorsOther = dataset.authors.filter( - (author) => - !(author.primary_author_status || author.corresponding_author_status), - ) - // clean up entries into lists const publicationEntries = dataset.dataset_publications ?.split(',') @@ -140,10 +83,6 @@ export function DatasetDescription() { ?.split(',') .map((e) => e.trim()) - const envelopeIcon = ( - - ) - return (

      @@ -151,40 +90,10 @@ export function DatasetDescription() {

      {i18n.authors}

      - {/* TODO: let's find a better way of doing this */} -

      - - {authorsPrimary.map((author, i, arr) => ( - <> - {author.name} - {!( - authorsOther.length + authorsCorresponding.length === 0 && - arr.length - 1 === i - ) && '; '} - - ))} - - - {authorsOther.map((author, i, arr) => ( - <> - {author.name} - {!(authorsCorresponding.length === 0 && arr.length - 1 === i) && - '; '} - - ))} - {authorsCorresponding.map((author, i, arr) => ( - <> - {author.name} - {author.email ? ( - {envelopeIcon} - ) : ( - envelopeIcon - )} - {!(arr.length - 1 === i) && '; '} - - ))} - -

      +
      @@ -66,7 +66,7 @@ export function DatasetHeader() { {/* portal ID */}

      - {i18n.portalIdBlank} + {i18n.portalIdBlank}:

      {dataset.id} diff --git a/frontend/packages/data-portal/app/components/Dataset/DatasetMetadataDrawer.tsx b/frontend/packages/data-portal/app/components/Dataset/DatasetMetadataDrawer.tsx index 8b6ab5300..b3a8d1769 100644 --- a/frontend/packages/data-portal/app/components/Dataset/DatasetMetadataDrawer.tsx +++ b/frontend/packages/data-portal/app/components/Dataset/DatasetMetadataDrawer.tsx @@ -1,122 +1,19 @@ -import { ButtonIcon } from '@czi-sds/components' -import { useSearchParams } from '@remix-run/react' -import { useEffect, useRef } from 'react' - -import { Demo } from 'app/components/Demo' -import { Drawer } from 'app/components/Drawer' -import { TabData, Tabs } from 'app/components/Tabs' +import { MetadataDrawer } from 'app/components/MetadataDrawer' import { useDatasetById } from 'app/hooks/useDatasetById' import { i18n } from 'app/i18n' -import { useDatasetDrawer } from 'app/state/drawer' -import { cns } from 'app/utils/cns' import { DatasetMetadataTable } from './DatasetMetadataTable' +import { DatasetTiltSeriesTable } from './DatasetTiltSeriesTable' import { SampleAndExperimentConditionsTable } from './SampleAndExperimentConditionsTable' -import { TiltSeriesTable } from './TiltSeriesTable' - -enum MetadataTab { - Metadata = 'metadata', - HowToCite = 'howToCite', -} - -const TAB_OPTIONS: TabData[] = [ - { - label: i18n.metadata, - value: MetadataTab.Metadata, - }, - { - label: i18n.howToCite, - value: MetadataTab.HowToCite, - }, -] - -const ACTIVE_TAB_PARAM = 'tab' export function DatasetMetadataDrawer() { - const drawer = useDatasetDrawer() const { dataset } = useDatasetById() - const [searchParams, setSearchParams] = useSearchParams() - const activeTab = (searchParams.get(ACTIVE_TAB_PARAM) ?? - MetadataTab.Metadata) as MetadataTab - - const initialLoadRef = useRef(true) - if (initialLoadRef.current && searchParams.has(ACTIVE_TAB_PARAM)) { - initialLoadRef.current = false - drawer.setOpen(true) - } - - useEffect(() => { - if (drawer.open && searchParams.get(ACTIVE_TAB_PARAM) !== activeTab) { - setSearchParams((params) => { - params.set(ACTIVE_TAB_PARAM, activeTab) - return params - }) - } else if (!drawer.open) { - setSearchParams((params) => { - params.delete(ACTIVE_TAB_PARAM) - return params - }) - } - }, [activeTab, drawer.open, searchParams, setSearchParams]) - return ( - drawer.setOpen(false)}> -

      -
      -
      -

      - {i18n.datasetDetails} -

      - -

      - {dataset.title} -

      -
      - - drawer.setOpen(false)} - sdsIcon="xMark" - sdsIconProps={{ - color: 'gray', - }} - /> -
      - -
      - - setSearchParams((params) => { - params.set(ACTIVE_TAB_PARAM, tab) - return params - }) - } - /> -
      - -
      - {activeTab === MetadataTab.Metadata && ( - <> - - - - - )} - - {activeTab === MetadataTab.HowToCite && How to cite} -
      -
      - + + + + + ) } diff --git a/frontend/packages/data-portal/app/components/Dataset/DatasetMetadataTable.tsx b/frontend/packages/data-portal/app/components/Dataset/DatasetMetadataTable.tsx index bf82b1217..377690c96 100644 --- a/frontend/packages/data-portal/app/components/Dataset/DatasetMetadataTable.tsx +++ b/frontend/packages/data-portal/app/components/Dataset/DatasetMetadataTable.tsx @@ -1,51 +1,138 @@ -import { useDatasetById } from 'app/hooks/useDatasetById' +import { Icon } from '@czi-sds/components' + +import { Dataset_Funding } from 'app/__generated__/graphql' +import { AccordionMetadataTable } from 'app/components/AccordionMetadataTable' +import { DatabaseEntry } from 'app/components/DatabaseEntry' +import { Link } from 'app/components/Link' +import { getTableData } from 'app/components/utils' +import { DOI_ID } from 'app/constants/external-dbs' import { i18n } from 'app/i18n' -import { AccordionMetadataTable } from './AccordionMetadataTable' -import { getTableData } from './utils' +import { AuthorInfo, DatasetAuthors } from './DatasetAuthors' +import { DatasetType } from './type' + +interface DatasetMetadataTableProps { + dataset: DatasetType + allFields?: boolean + initialOpen?: boolean +} -export function DatasetMetadataTable() { - const { dataset } = useDatasetById() +export function DatasetMetadataTable(props: DatasetMetadataTableProps) { + const { dataset, allFields, initialOpen } = props const datasetMetadata = getTableData( + !!allFields && { + label: i18n.datasetTitle, + values: [dataset.title!], + renderValue: (value) => { + return ( + + {value} + + + ) + }, + }, + !!allFields && { + label: i18n.portalIdBlank, + values: [`${dataset.id!}`], + }, + !!allFields && { + label: i18n.description, + values: [dataset.description!], + className: 'text-ellipsis line-clamp-3', + }, { label: i18n.depositionDate, - values: [dataset.deposition_date], + values: [dataset.deposition_date!], + }, + !!allFields && { + label: i18n.releaseDateBlank, + values: [dataset.release_date!], + }, + !!allFields && { + label: i18n.lastModifiedBlank, + values: [dataset.last_modified_date!], + }, + !!allFields && { + label: + dataset.authors && dataset.authors.length === 1 + ? i18n.Author + : i18n.Authors, + renderValue: () => { + return + }, + values: [], + className: 'leading-[20px]', }, { label: i18n.affiliationName, - values: dataset.authors_with_affiliation - .map((author) => author.affiliation_name) + values: dataset + .authors_with_affiliation!.map((author) => author.affiliation_name!) .filter((value): value is string => !!value), }, { - label: i18n.fundingAgency, - values: dataset.funding_sources.map( - (source) => source.funding_agency_name, - ), + label: i18n.grantID, + values: ['TBD'], }, { - label: i18n.relatedDatabases, - values: dataset.funding_sources.map( + label: i18n.fundingAgency, + values: (dataset.funding_sources as Dataset_Funding[]).map( (source) => source.funding_agency_name, ), }, { label: i18n.relatedDatabases, - // TODO implement when data is available - values: ['TBD'], + values: dataset.related_database_entries + ? dataset.related_database_entries.split(',').map((e) => e.trim()) + : [], + renderValue: (value) => { + return + }, + className: 'text-sds-body-s leading-sds-body-s', + }, + !!allFields && { + label: i18n.publications, + values: dataset.dataset_publications + ? dataset.dataset_publications + .split(',') + .map((e) => e.trim()) + .filter((e) => DOI_ID.exec(e)) + : [], + renderValue: (value) => { + return + }, + className: 'text-sds-body-s leading-sds-body-s', }, { label: i18n.citations, - // TODO implement when data is available - values: ['TBD'], + values: dataset.dataset_citations + ? dataset.dataset_citations + .split(',') + .map((e) => e.trim()) + .filter((e) => DOI_ID.exec(e)) + : [], + renderValue: (value) => { + return + }, + className: 'text-sds-body-s leading-sds-body-s', }, ) return ( ) } diff --git a/frontend/packages/data-portal/app/components/Dataset/DatasetTiltSeriesTable.tsx b/frontend/packages/data-portal/app/components/Dataset/DatasetTiltSeriesTable.tsx new file mode 100644 index 000000000..c84091b88 --- /dev/null +++ b/frontend/packages/data-portal/app/components/Dataset/DatasetTiltSeriesTable.tsx @@ -0,0 +1,26 @@ +import { TiltSeriesKeys, TiltSeriesTable } from 'app/components/TiltSeriesTable' +import { useDatasetById } from 'app/hooks/useDatasetById' + +const SELECT_FIELDS = [ + TiltSeriesKeys.AccelerationVoltage, + TiltSeriesKeys.SphericalAberrationConstant, + TiltSeriesKeys.MicroscopeManufacturer, + TiltSeriesKeys.MicroscopeModel, + TiltSeriesKeys.EnergyFilter, + TiltSeriesKeys.PhasePlate, + TiltSeriesKeys.ImageCorrector, + TiltSeriesKeys.AdditionalMicroscopeOpticalSetup, + TiltSeriesKeys.CameraManufacturer, + TiltSeriesKeys.CameraModel, +] + +export function DatasetTiltSeriesTable() { + const { dataset } = useDatasetById() + + return ( + + ) +} diff --git a/frontend/packages/data-portal/app/components/Dataset/SampleAndExperimentConditionsTable.tsx b/frontend/packages/data-portal/app/components/Dataset/SampleAndExperimentConditionsTable.tsx index 9e30a0a13..9dfe1db1f 100644 --- a/frontend/packages/data-portal/app/components/Dataset/SampleAndExperimentConditionsTable.tsx +++ b/frontend/packages/data-portal/app/components/Dataset/SampleAndExperimentConditionsTable.tsx @@ -1,16 +1,20 @@ -import { useDatasetById } from 'app/hooks/useDatasetById' +import { AccordionMetadataTable } from 'app/components/AccordionMetadataTable' +import { getTableData } from 'app/components/utils' import { i18n } from 'app/i18n' -import { AccordionMetadataTable } from './AccordionMetadataTable' -import { getTableData } from './utils' - -export function SampleAndExperimentConditionsTable() { - const { dataset } = useDatasetById() +import { DatasetType } from './type' +export function SampleAndExperimentConditionsTable({ + dataset, + initialOpen, +}: { + dataset: DatasetType + initialOpen?: boolean +}) { const sampleAndExperimentConditions = getTableData( { label: i18n.sampleType, - values: [dataset.sample_type], + values: [dataset.sample_type!], }, { label: i18n.organismName, @@ -52,6 +56,7 @@ export function SampleAndExperimentConditionsTable() { id="sample-and-experimental-conditions" header={i18n.sampleAndExperimentConditions} data={sampleAndExperimentConditions} + initialOpen={initialOpen} /> ) } diff --git a/frontend/packages/data-portal/app/components/Dataset/TiltSeriesTable.tsx b/frontend/packages/data-portal/app/components/Dataset/TiltSeriesTable.tsx deleted file mode 100644 index ee154a461..000000000 --- a/frontend/packages/data-portal/app/components/Dataset/TiltSeriesTable.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { useDatasetById } from 'app/hooks/useDatasetById' -import { i18n } from 'app/i18n' - -import { AccordionMetadataTable } from './AccordionMetadataTable' -import { getTableData } from './utils' - -export function TiltSeriesTable() { - const { dataset } = useDatasetById() - - const tiltSeriesData = dataset.run_metadata[0].tiltseries[0] - const tiltSeries = tiltSeriesData - ? getTableData( - { - label: i18n.accelerationVoltage, - values: [ - `${tiltSeriesData.acceleration_voltage as unknown as number}`, - ], - }, - { - label: i18n.sphericalAberrationConstant, - values: [`${tiltSeriesData.spherical_aberration_constant}`], - }, - { - label: i18n.microscopeManufacturer, - values: [tiltSeriesData.microscope_manufacturer], - }, - { - label: i18n.microscopeModel, - values: [tiltSeriesData.microscope_model], - }, - { - label: i18n.energyFilter, - values: [tiltSeriesData.microscope_energy_filter], - }, - { - label: i18n.phasePlate, - values: [tiltSeriesData.microscope_phase_plate ?? 'None'], - }, - { - label: i18n.imageCorrector, - values: [tiltSeriesData.microscope_image_corrector ?? 'None'], - }, - { - label: i18n.additionalMicroscopeOpticalSetup, - values: [tiltSeriesData.microscope_additional_info ?? 'None'], - }, - { - label: i18n.cameraManufacturer, - values: [tiltSeriesData.camera_manufacturer], - }, - { - label: i18n.cameraModel, - values: [tiltSeriesData.camera_model], - }, - ) - : [] - - return ( - - ) -} diff --git a/frontend/packages/data-portal/app/components/Dataset/type.ts b/frontend/packages/data-portal/app/components/Dataset/type.ts new file mode 100644 index 000000000..c54a0fcdc --- /dev/null +++ b/frontend/packages/data-portal/app/components/Dataset/type.ts @@ -0,0 +1,6 @@ +import { Dataset_Authors, Datasets } from 'app/__generated__/graphql' +import { RecursivePartial } from 'app/utils/RecursivePartial' + +export type DatasetType = RecursivePartial & { + authors_with_affiliation?: RecursivePartial[] +} diff --git a/frontend/packages/data-portal/app/components/Dataset/utils.ts b/frontend/packages/data-portal/app/components/Dataset/utils.ts deleted file mode 100644 index 3ae89e080..000000000 --- a/frontend/packages/data-portal/app/components/Dataset/utils.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { TableData } from 'app/components/Table/MetadataTable' - -export function getTableData(...metadata: TableData[]) { - return metadata.map((data) => { - const values = ( - data.values instanceof Function ? data.values() : data.values - ).filter(Boolean) - - return { - ...data, - values: values.length > 0 ? values : ['--'], - } - }) -} diff --git a/frontend/packages/data-portal/app/components/MetadataDrawer.tsx b/frontend/packages/data-portal/app/components/MetadataDrawer.tsx new file mode 100644 index 000000000..c935a562a --- /dev/null +++ b/frontend/packages/data-portal/app/components/MetadataDrawer.tsx @@ -0,0 +1,117 @@ +import { ButtonIcon } from '@czi-sds/components' +import { useSearchParams } from '@remix-run/react' +import { ReactNode, useEffect, useRef } from 'react' + +import { Demo } from 'app/components/Demo' +import { Drawer } from 'app/components/Drawer' +import { TabData, Tabs } from 'app/components/Tabs' +import { i18n } from 'app/i18n' +import { useDrawer } from 'app/state/drawer' +import { cns } from 'app/utils/cns' + +enum MetadataTab { + Metadata = 'metadata', + HowToCite = 'howToCite', +} + +const TAB_OPTIONS: TabData[] = [ + { + label: i18n.metadata, + value: MetadataTab.Metadata, + }, + { + label: i18n.howToCite, + value: MetadataTab.HowToCite, + }, +] + +const ACTIVE_TAB_PARAM = 'tab' + +interface MetaDataDrawerProps { + title: string + label: string + children: ReactNode +} + +export function MetadataDrawer(props: MetaDataDrawerProps) { + const { title, label, children } = props + const drawer = useDrawer() + + const [searchParams, setSearchParams] = useSearchParams() + const activeTab = (searchParams.get(ACTIVE_TAB_PARAM) ?? + MetadataTab.Metadata) as MetadataTab + + const initialLoadRef = useRef(true) + if (initialLoadRef.current && searchParams.has(ACTIVE_TAB_PARAM)) { + initialLoadRef.current = false + drawer.setOpen(true) + } + + useEffect(() => { + if (drawer.open && searchParams.get(ACTIVE_TAB_PARAM) !== activeTab) { + setSearchParams((params) => { + params.set(ACTIVE_TAB_PARAM, activeTab) + return params + }) + } else if (!drawer.open) { + setSearchParams((params) => { + params.delete(ACTIVE_TAB_PARAM) + return params + }) + } + }, [activeTab, drawer.open, searchParams, setSearchParams]) + + return ( + drawer.setOpen(false)}> +
      +
      +
      +

      + {label} +

      + +

      + {title} +

      +
      + + drawer.setOpen(false)} + sdsIcon="xMark" + sdsIconProps={{ + color: 'gray', + }} + /> +
      + +
      + + setSearchParams((params) => { + params.set(ACTIVE_TAB_PARAM, tab) + return params + }) + } + /> +
      + +
      + {activeTab === MetadataTab.Metadata && children} + + {activeTab === MetadataTab.HowToCite && How to cite} +
      +
      +
      + ) +} diff --git a/frontend/packages/data-portal/app/components/Run/Matrix4x4.tsx b/frontend/packages/data-portal/app/components/Run/Matrix4x4.tsx new file mode 100644 index 000000000..0b6aefb9b --- /dev/null +++ b/frontend/packages/data-portal/app/components/Run/Matrix4x4.tsx @@ -0,0 +1,30 @@ +import { cns } from 'app/utils/cns' + +export function Matrix4x4({ + matrix, + className, +}: { + matrix: string + className?: string +}) { + const commonBracketProps = 'w-sds-xs border border-solid border-black' + + return ( +
      + {/* left bracket */} +
      + {/* matrix */} +
      + {matrix.split(' ').map((value) => { + return ( +

      + {value} +

      + ) + })} +
      + {/* right bracket */} +
      +
      + ) +} diff --git a/frontend/packages/data-portal/app/components/Run/RunMetadataDrawer.tsx b/frontend/packages/data-portal/app/components/Run/RunMetadataDrawer.tsx new file mode 100644 index 000000000..c7e5b4c80 --- /dev/null +++ b/frontend/packages/data-portal/app/components/Run/RunMetadataDrawer.tsx @@ -0,0 +1,28 @@ +import { DatasetMetadataTable } from 'app/components/Dataset/DatasetMetadataTable' +import { SampleAndExperimentConditionsTable } from 'app/components/Dataset/SampleAndExperimentConditionsTable' +import { MetadataDrawer } from 'app/components/MetadataDrawer' +import { useRunById } from 'app/hooks/useRunById' +import { i18n } from 'app/i18n' + +import { RunTiltSeriesTable } from './RunTiltSeriesTable' +import { TomogramsTable } from './TomogramsTable' + +export function RunMetadataDrawer() { + const { run } = useRunById() + + return ( + + + + + + + ) +} diff --git a/frontend/packages/data-portal/app/components/Run/RunTiltSeriesTable.tsx b/frontend/packages/data-portal/app/components/Run/RunTiltSeriesTable.tsx new file mode 100644 index 000000000..b0c474e99 --- /dev/null +++ b/frontend/packages/data-portal/app/components/Run/RunTiltSeriesTable.tsx @@ -0,0 +1,37 @@ +import { TiltSeriesKeys, TiltSeriesTable } from 'app/components/TiltSeriesTable' +import { useRunById } from 'app/hooks/useRunById' + +const SELECT_FIELDS = [ + TiltSeriesKeys.MicroscopeManufacturer, + TiltSeriesKeys.MicroscopeModel, + TiltSeriesKeys.PhasePlate, + TiltSeriesKeys.ImageCorrector, + TiltSeriesKeys.AdditionalMicroscopeOpticalSetup, + TiltSeriesKeys.AccelerationVoltage, + TiltSeriesKeys.SphericalAberrationConstant, + TiltSeriesKeys.CameraManufacturer, + TiltSeriesKeys.CameraModel, + TiltSeriesKeys.EnergyFilter, + TiltSeriesKeys.DataAcquisitionSoftware, + TiltSeriesKeys.PixelSpacing, + TiltSeriesKeys.TiltAxis, + TiltSeriesKeys.TiltRange, + TiltSeriesKeys.TiltStep, + TiltSeriesKeys.TiltingScheme, + TiltSeriesKeys.TotalFlux, + TiltSeriesKeys.BinningFromFrames, + TiltSeriesKeys.SeriesIsAligned, + TiltSeriesKeys.AlignedBinning, + TiltSeriesKeys.RelatedEmpiarEntry, +] + +export function RunTiltSeriesTable() { + const { run } = useRunById() + + return ( + + ) +} diff --git a/frontend/packages/data-portal/app/components/Run/TomogramsTable.tsx b/frontend/packages/data-portal/app/components/Run/TomogramsTable.tsx new file mode 100644 index 000000000..588771f5d --- /dev/null +++ b/frontend/packages/data-portal/app/components/Run/TomogramsTable.tsx @@ -0,0 +1,66 @@ +import { AccordionMetadataTable } from 'app/components/AccordionMetadataTable' +import { getTableData } from 'app/components/utils' +import { useRunById } from 'app/hooks/useRunById' +import { i18n } from 'app/i18n' + +import { Matrix4x4 } from './Matrix4x4' + +export function TomogramsTable() { + const { run } = useRunById() + + const tomo = run.tomogram_voxel_spacings[0].tomograms[0] + + const tomogramsTableData = getTableData( + { + label: i18n.reconstructionSoftware, + values: [tomo.reconstruction_software], + }, + { + label: i18n.reconstructionMethod, + values: [tomo.reconstruction_method], + className: 'capitalize', + }, + { + label: i18n.processingSoftware, + values: [tomo.processing_software ?? ''], + }, + { + label: i18n.availableProcessing, + values: [tomo.processing], + className: 'capitalize', + }, + { + label: i18n.smallestAvailableVoxelSpacing, + values: [i18n.unitAngstrom(+tomo.voxel_spacing)], + }, + { + label: `${i18n.size} (x, y, z)`, + values: [`(${tomo.size_x}, ${tomo.size_y}, ${tomo.size_z}) px`], + }, + { + label: i18n.fiducialAlignmentStatus, + values: [ + tomo.fiducial_alignment_status === 'FIDUCIAL' ? i18n.true : i18n.false, + ], + }, + { + label: i18n.ctfCorrected, + values: [tomo.ctf_corrected ? i18n.yes : i18n.no], + }, + { + label: i18n.affineTransformationMatrix, + values: ['1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1'], + renderValue: (value) => { + return + }, + }, + ) + + return ( + + ) +} diff --git a/frontend/packages/data-portal/app/components/Run/index.ts b/frontend/packages/data-portal/app/components/Run/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/packages/data-portal/app/components/Table/MetadataTable.tsx b/frontend/packages/data-portal/app/components/Table/MetadataTable.tsx index 68e69347c..e9834a914 100644 --- a/frontend/packages/data-portal/app/components/Table/MetadataTable.tsx +++ b/frontend/packages/data-portal/app/components/Table/MetadataTable.tsx @@ -11,13 +11,16 @@ export interface TableData { label: string values: string[] | (() => string[]) renderValue?(value: string): ReactNode + className?: string } export function MetadataTable({ data, + tableHeaderProps, tableCellProps, }: { data: TableData[] + tableHeaderProps?: ComponentProps tableCellProps?: ComponentProps }) { return ( @@ -32,12 +35,19 @@ export function MetadataTable({ className={cns(idx % 2 !== 0 && 'bg-sds-gray-100')} key={datum.label + values.join(', ')} > - {datum.label} + + + {datum.label} + +
        {values.map((value) => ( -
      • +
      • {datum.renderValue?.(value) ?? value}
      • ))} diff --git a/frontend/packages/data-portal/app/components/TiltSeriesTable/TiltSeriesTable.tsx b/frontend/packages/data-portal/app/components/TiltSeriesTable/TiltSeriesTable.tsx new file mode 100644 index 000000000..626cdfb43 --- /dev/null +++ b/frontend/packages/data-portal/app/components/TiltSeriesTable/TiltSeriesTable.tsx @@ -0,0 +1,32 @@ +import { Tiltseries } from 'app/__generated__/graphql' +import { AccordionMetadataTable } from 'app/components/AccordionMetadataTable' +import { getTableData } from 'app/components/utils' +import { i18n } from 'app/i18n' + +import { TILT_SERIES_VALUE_MAPPINGS, TiltSeriesKeys } from './constants' + +interface TiltSeriesTableProps { + tiltSeriesData?: Partial + fields: TiltSeriesKeys[] +} + +export function TiltSeriesTable(props: TiltSeriesTableProps) { + const { tiltSeriesData, fields } = props + + const tiltSeries = tiltSeriesData + ? getTableData( + ...fields.map((field) => { + const getData = TILT_SERIES_VALUE_MAPPINGS.get(field) + return getData!(tiltSeriesData) + }), + ) + : [] + + return ( + + ) +} diff --git a/frontend/packages/data-portal/app/components/TiltSeriesTable/constants.tsx b/frontend/packages/data-portal/app/components/TiltSeriesTable/constants.tsx new file mode 100644 index 000000000..a794749a9 --- /dev/null +++ b/frontend/packages/data-portal/app/components/TiltSeriesTable/constants.tsx @@ -0,0 +1,232 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import { Tiltseries } from 'app/__generated__/graphql' +import { DatabaseEntry } from 'app/components/DatabaseEntry' +import { TableData } from 'app/components/Table' +import { i18n } from 'app/i18n' + +export const enum TiltSeriesKeys { + AccelerationVoltage, + AdditionalMicroscopeOpticalSetup, + AlignedBinning, + BinningFromFrames, + CameraManufacturer, + CameraModel, + DataAcquisitionSoftware, + EnergyFilter, + ImageCorrector, + MicroscopeManufacturer, + MicroscopeModel, + PhasePlate, + PixelSpacing, + RelatedEmpiarEntry, + SeriesIsAligned, + SphericalAberrationConstant, + TiltAxis, + TiltingScheme, + TiltRange, + TiltStep, + TotalFlux, +} + +export const TILT_SERIES_VALUE_MAPPINGS = new Map([ + [ + TiltSeriesKeys.AccelerationVoltage, + (data: Partial): TableData => { + return { + label: i18n.accelerationVoltage, + values: [ + i18n.unitVolts(data.acceleration_voltage as unknown as number), + ], + } + }, + ], + [ + TiltSeriesKeys.AdditionalMicroscopeOpticalSetup, + (data: Partial): TableData => { + return { + label: i18n.additionalMicroscopeOpticalSetup, + values: [data.microscope_additional_info ?? 'None'], + } + }, + ], + [ + TiltSeriesKeys.AlignedBinning, + (_data: Partial): TableData => { + return { + label: i18n.alignedTiltSeriesBinning, + values: ['TBD'], + } + }, + ], + [ + TiltSeriesKeys.BinningFromFrames, + (data: Partial): TableData => { + return { + label: i18n.bingingFromFrames, + values: [data.binning_from_frames], + } + }, + ], + [ + TiltSeriesKeys.CameraManufacturer, + (data: Partial): TableData => { + return { + label: i18n.cameraManufacturer, + values: [data.camera_manufacturer!], + } + }, + ], + [ + TiltSeriesKeys.CameraModel, + (data: Partial): TableData => { + return { + label: i18n.cameraModel, + values: [data.camera_model!], + } + }, + ], + [ + TiltSeriesKeys.DataAcquisitionSoftware, + (data: Partial): TableData => { + return { + label: i18n.dataAquisitionSoftware, + values: [data.data_acquisition_software!], + } + }, + ], + [ + TiltSeriesKeys.EnergyFilter, + (data: Partial): TableData => { + return { + label: i18n.energyFilter, + values: [data.microscope_energy_filter!], + } + }, + ], + [ + TiltSeriesKeys.ImageCorrector, + (data: Partial): TableData => { + return { + label: i18n.imageCorrector, + values: [data.microscope_image_corrector ?? 'None'], + } + }, + ], + [ + TiltSeriesKeys.MicroscopeManufacturer, + (data: Partial): TableData => { + return { + label: i18n.microscopeManufacturer, + values: [data.microscope_manufacturer!], + } + }, + ], + [ + TiltSeriesKeys.MicroscopeModel, + (data: Partial): TableData => { + return { + label: i18n.microscopeModel, + values: [data.microscope_model!], + } + }, + ], + [ + TiltSeriesKeys.PhasePlate, + (data: Partial): TableData => { + return { + label: i18n.phasePlate, + values: [data.microscope_phase_plate ?? 'None'], + } + }, + ], + [ + TiltSeriesKeys.PixelSpacing, + (_data: Partial): TableData => { + return { + label: i18n.pixelSpacing, + values: ['TBD'], + } + }, + ], + [ + TiltSeriesKeys.RelatedEmpiarEntry, + (data: Partial): TableData => { + return { + label: i18n.relatedEmpiarEntry, + renderValue: data.related_empiar_entry + ? (value) => + : undefined, + values: data.related_empiar_entry ? [data.related_empiar_entry] : [], + } + }, + ], + [ + TiltSeriesKeys.SeriesIsAligned, + (_data: Partial): TableData => { + return { + label: i18n.seriesIsAligned, + values: ['TBD'], + } + }, + ], + [ + TiltSeriesKeys.SphericalAberrationConstant, + (data: Partial): TableData => { + return { + label: i18n.sphericalAberrationConstant, + values: [i18n.unitMilimeter(+data.spherical_aberration_constant)], + } + }, + ], + [ + TiltSeriesKeys.TiltAxis, + (data: Partial): TableData => { + return { + label: i18n.tiltAxis, + values: [i18n.unitDegree(+data.tilt_axis)], + } + }, + ], + [ + TiltSeriesKeys.TiltingScheme, + (data: Partial): TableData => { + return { + label: i18n.tiltingScheme, + values: [data.tilting_scheme!], + } + }, + ], + [ + TiltSeriesKeys.TiltRange, + (data: Partial): TableData => { + return { + label: i18n.tiltRange, + values: [ + i18n.valueToValue( + i18n.unitDegree(+data.tilt_min), + i18n.unitDegree(+data.tilt_max), + ), + ], + } + }, + ], + [ + TiltSeriesKeys.TiltStep, + (data: Partial): TableData => { + return { + label: i18n.tiltStep, + values: [i18n.unitDegree(+data.tilt_step)], + } + }, + ], + [ + TiltSeriesKeys.TotalFlux, + (data: Partial): TableData => { + return { + label: i18n.totalFlux, + values: [i18n.unitAngstrom(+data.total_flux)], + } + }, + ], +]) diff --git a/frontend/packages/data-portal/app/components/TiltSeriesTable/index.ts b/frontend/packages/data-portal/app/components/TiltSeriesTable/index.ts new file mode 100644 index 000000000..4a6848b36 --- /dev/null +++ b/frontend/packages/data-portal/app/components/TiltSeriesTable/index.ts @@ -0,0 +1,2 @@ +export { TiltSeriesKeys } from './constants' +export { TiltSeriesTable } from './TiltSeriesTable' diff --git a/frontend/packages/data-portal/app/components/utils.ts b/frontend/packages/data-portal/app/components/utils.ts new file mode 100644 index 000000000..65287a514 --- /dev/null +++ b/frontend/packages/data-portal/app/components/utils.ts @@ -0,0 +1,18 @@ +import { TableData } from 'app/components/Table/MetadataTable' + +export function getTableData(...metadata: (TableData | boolean)[]) { + return metadata + .filter((i): i is TableData => { + return Boolean(i) + }) + .map((data) => { + const values = ( + data.values instanceof Function ? data.values() : data.values + ).filter(Boolean) + + return { + ...data, + values: values.length > 0 ? values : ['--'], + } + }) +} diff --git a/frontend/packages/data-portal/app/hooks/useRunById.ts b/frontend/packages/data-portal/app/hooks/useRunById.ts new file mode 100644 index 000000000..29e1fd45e --- /dev/null +++ b/frontend/packages/data-portal/app/hooks/useRunById.ts @@ -0,0 +1,11 @@ +import { useLoaderData } from '@remix-run/react' + +import { GetRunByIdQuery } from 'app/__generated__/graphql' + +export function useRunById() { + const { + runs: [run], + } = useLoaderData() + + return { run } +} diff --git a/frontend/packages/data-portal/app/i18n.ts b/frontend/packages/data-portal/app/i18n.ts index a5c543c0f..56982602f 100644 --- a/frontend/packages/data-portal/app/i18n.ts +++ b/frontend/packages/data-portal/app/i18n.ts @@ -9,9 +9,15 @@ export const i18n = { accelerationVoltage: 'Acceleration Voltage', additionalMicroscopeOpticalSetup: 'Additional microscope optical setup', affiliationName: 'Affiliation Name', + affineTransformationMatrix: 'Affine Transformation Matrix', + alignedTiltSeriesBinning: 'Aligned Tilt-Series Binning', annotatedObjects: 'Annotated Objects', api: 'API', + Author: 'Author', authors: 'Authors', + Authors: 'Authors', + availableProcessing: 'Available Processing', + bingingFromFrames: 'Binning from Frames', browseData: 'Browse Data', cameraManufacturer: 'Camera Manufacturer', cameraModel: 'Camera Model', @@ -20,18 +26,24 @@ export const i18n = { cellularComponent: 'Cellular Component', citations: 'Citations', cookiePolicy: 'Cookie Policy', + ctfCorrected: 'Ctf Corrected', + dataAquisitionSoftware: 'Data Acquisition Software', dataset: 'Dataset', datasetCount: (count: number, max: number) => `${count} of ${max} datasets`, - datasetDetails: 'Dataset details', + datasetDetails: 'Dataset Details', datasetMetadata: 'Dataset Metadata', datasets: 'Datasets', datasetsTab: (count: number) => `Datasets ${count}`, + datasetTitle: 'Dataset Title', depositionDate: 'Deposition Date', + description: 'Description', documentation: 'Documentation', empiarID: 'Empiar ID', energyFilter: 'Energy Filter', excellent: 'Excellent', + false: 'False', faq: 'FAQ', + fiducialAlignmentStatus: 'Fiducial Alignment Status', fundingAgency: 'Funding Agency', github: 'GitHub', good: 'Good', @@ -43,6 +55,7 @@ export const i18n = { imageCorrector: 'Image Corrector', keyPhoto: 'key photo', lastModified: (date: string) => `Last Modified: ${date}`, + lastModifiedBlank: 'Last Modified', license: 'License', metadata: 'Metadata', microscopeManufacturer: 'Microscope Manufacturer', @@ -50,38 +63,62 @@ export const i18n = { moderate: 'Moderate', napariPlugin: 'napari Plugin', nMoreObjects: (count: number) => `${count} More Objects`, + no: 'No', notSubmitted: 'Not Submitted', organism: 'Organism', organismName: 'Organism Name', otherSetup: 'Other Setup', phasePlate: 'Phase Plate', + pixelSpacing: 'Pixel Spacing', plusMore: (count: number) => `+${count} More`, poor: 'Poor', portalId: (id: number | string) => `Portal ID: ${id}`, - portalIdBlank: 'Portal ID:', + portalIdBlank: 'Portal ID', privacy: 'Privacy', privacyPolicy: 'Privacy Policy', + processingSoftware: 'Processing Software', publications: 'Publications', + reconstructionMethod: 'Reconstruction Method', + reconstructionSoftware: 'Reconstruction Software', relatedDatabases: 'Related Databases', + relatedEmpiarEntry: 'Related EMPIAR Entry', releaseDate: (date: string) => `Release Date: ${date}`, + releaseDateBlank: 'Release Date', reportIssueOnGithub: 'Report Issue on GitHub', run: 'Run', runCount: (count: number, max: number) => `${count} of ${max} Runs`, + runDetails: 'Run Details', runs: 'Runs', runsTab: (count: number) => `Runs ${count}`, sampleAndExperimentConditions: 'Sample and Experiment Conditions', samplePreparation: 'Sample Preparation', sampleType: 'Sample Type', search: 'Search', + seriesIsAligned: 'Series is Aligned', showLess: 'Show Less', + size: 'Size', + smallestAvailableVoxelSpacing: 'Smallest Available Voxel Spacing', sphericalAberrationConstant: 'Spherical Aberration Constant', submitFeedback: 'Submit Feedback', terms: 'Terms', termsOfUse: 'Terms of Use', + tiltAxis: 'Tilt Axis', + tiltingScheme: 'Tilting Scheme', + tiltRange: 'Tilt Range', tiltSeries: 'Tilt-Series', tiltSeriesQualityScore: 'Tilt-Series Quality Score', + tiltStep: 'Tilt Step', tissueName: 'Tissue Name', title: 'CryoET Data Portal', + tomograms: 'Tomograms', tools: 'Tools', + totalFlux: 'Total Flux', + true: 'True', + unitAngstrom: (x: number) => `${x}Å`, + unitDegree: (x: number) => `${x}°`, + unitMilimeter: (x: number) => `${x} mm`, + unitVolts: (x: number) => `${x} V`, + valueToValue: (x: string, y: string) => `${x} to ${y}`, veryPoor: 'Very Poor', + yes: 'Yes', } diff --git a/frontend/packages/data-portal/app/routes/datasets.$id.tsx b/frontend/packages/data-portal/app/routes/datasets.$id.tsx index 8e8848ae9..037764538 100644 --- a/frontend/packages/data-portal/app/routes/datasets.$id.tsx +++ b/frontend/packages/data-portal/app/routes/datasets.$id.tsx @@ -13,7 +13,7 @@ import { RunsTable } from 'app/components/Dataset/RunsTable' import { FilterPanel } from 'app/components/FilterPanel' import { MAX_PER_PAGE } from 'app/constants/pagination' import { useDatasetById } from 'app/hooks/useDatasetById' -import { useCloseDatasetDrawerOnUnmount } from 'app/state/drawer' +import { useCloseDrawerOnUnmount } from 'app/state/drawer' import { cns } from 'app/utils/cns' const GET_DATASET_BY_ID = gql(` @@ -136,7 +136,7 @@ export async function loader({ params, request }: LoaderFunctionArgs) { export default function DatasetByIdPage() { const { dataset } = useDatasetById() - useCloseDatasetDrawerOnUnmount() + useCloseDrawerOnUnmount() const [searchParams, setSearchParams] = useSearchParams() const page = +(searchParams.get('page') ?? '1') diff --git a/frontend/packages/data-portal/app/routes/runs.$id.tsx b/frontend/packages/data-portal/app/routes/runs.$id.tsx index dcfb9f658..d2a512ec5 100644 --- a/frontend/packages/data-portal/app/routes/runs.$id.tsx +++ b/frontend/packages/data-portal/app/routes/runs.$id.tsx @@ -1,12 +1,152 @@ +/* eslint-disable @typescript-eslint/no-throw-literal */ + import { useParams } from '@remix-run/react' +import { json, LoaderFunctionArgs } from '@remix-run/server-runtime' +import { gql } from 'app/__generated__' +import { apolloClient } from 'app/apollo.server' import { Demo } from 'app/components/Demo' +import { RunMetadataDrawer } from 'app/components/Run/RunMetadataDrawer' +import { useCloseDrawerOnUnmount, useDrawer } from 'app/state/drawer' + +const GET_RUN_BY_ID_QUERY = gql(` + query GetRunById($id: Int) { + runs(where: {id: {_eq: $id}}) { + name + tiltseries { + acceleration_voltage + camera_manufacturer + camera_model + data_acquisition_software + tilt_axis + tilt_max + tilt_min + tilt_range + tilt_series_quality + tilt_step + tilting_scheme + total_flux + microscope_additional_info + microscope_energy_filter + microscope_image_corrector + microscope_manufacturer + microscope_model + microscope_phase_plate + related_empiar_entry + spherical_aberration_constant + id + binning_from_frames + } + + dataset { + # Dataset dates + last_modified_date + release_date + deposition_date + + # Dataset metadata + id + title + description + funding_sources { + funding_agency_name + } + + # TODO Grant ID + related_database_entries + dataset_citations + + # Sample and experiments data + sample_type + organism_name + tissue_name + cell_name + cell_strain_name + # TODO cellular component + sample_preparation + grid_preparation + other_setup + + authors(distinct_on: name) { + name + email + primary_author_status + corresponding_author_status + } + + authors_with_affiliation: authors(where: {affiliation_name: {_is_null: false}}) { + name + affiliation_name + } + + # publication info + related_database_entries + dataset_publications + } + + tomogram_voxel_spacings(limit: 1) { + tomograms(limit: 1) { + ctf_corrected + fiducial_alignment_status + name + processing + reconstruction_method + processing_software + reconstruction_software + size_x + size_y + size_z + voxel_spacing + } + } + } + } +`) + +export async function loader({ params }: LoaderFunctionArgs) { + const id = params.id ? +params.id : NaN + + if (Number.isNaN(+id)) { + throw new Response(null, { + status: 400, + statusText: 'ID is not defined', + }) + } + + const { data } = await apolloClient.query({ + query: GET_RUN_BY_ID_QUERY, + variables: { + id: +id, + }, + }) + + if (data.runs.length === 0) { + throw new Response(null, { + status: 404, + statusText: `Run with ID ${id} not found`, + }) + } + + return json(data) +} export default function RunByIdPage() { const params = useParams() + const drawer = useDrawer() + useCloseDrawerOnUnmount() + return ( - - Run Page ID = {params.id} - + <> + +
        + Run Page ID = {params.id} + + +
        +
        + + ) } diff --git a/frontend/packages/data-portal/app/state/drawer.ts b/frontend/packages/data-portal/app/state/drawer.ts index 271544360..02d56047d 100644 --- a/frontend/packages/data-portal/app/state/drawer.ts +++ b/frontend/packages/data-portal/app/state/drawer.ts @@ -2,10 +2,10 @@ import { useUnmountEffect } from '@react-hookz/web' import { atom, useAtom, useSetAtom } from 'jotai' import { useMemo } from 'react' -const datasetDrawerOpenAtom = atom(false) +const drawerOpenAtom = atom(false) -export function useDatasetDrawer() { - const [open, setOpen] = useAtom(datasetDrawerOpenAtom) +export function useDrawer() { + const [open, setOpen] = useAtom(drawerOpenAtom) return useMemo( () => ({ @@ -17,8 +17,8 @@ export function useDatasetDrawer() { ) } -export function useCloseDatasetDrawerOnUnmount() { - const setDrawerOpen = useSetAtom(datasetDrawerOpenAtom) +export function useCloseDrawerOnUnmount() { + const setDrawerOpen = useSetAtom(drawerOpenAtom) // Reset drawer state on page unmount useUnmountEffect(() => setDrawerOpen(false)) diff --git a/frontend/packages/data-portal/app/utils/RecursivePartial.ts b/frontend/packages/data-portal/app/utils/RecursivePartial.ts new file mode 100644 index 000000000..cc2f064ac --- /dev/null +++ b/frontend/packages/data-portal/app/utils/RecursivePartial.ts @@ -0,0 +1,7 @@ +/* +from https://stackoverflow.com/a/47914631 +*/ + +export type RecursivePartial = { + [P in keyof T]?: RecursivePartial +}