Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { EyeOutlined } from '@ant-design/icons';
import type { ExperimentResult } from '@/types/clinPhen/experiments/experimentResult';
import type { ConditionalDescriptionItem } from '@/types/descriptions';

import PhenopacketLink from '@/components/ClinPhen/PhenopacketLink';
import CustomTable, { type CustomTableColumns } from '@Util/CustomTable';
import FileModal from '@Util/FileModal';
import TDescriptions from '@Util/TDescriptions';
Expand Down Expand Up @@ -36,12 +37,18 @@ export const ExperimentResultIndices = ({ indices }: { indices: ExperimentResult
);
};

export const ExperimentResultExpandedRow = ({
experimentResult,
}: {
type ExperimentResultExpandedRowProps = {
packetId?: string;
currentExperiment?: string;
experimentResult: ExperimentResult;
searchRow?: boolean;
}) => {
};

export const ExperimentResultExpandedRow = ({
packetId,
currentExperiment,
experimentResult,
}: ExperimentResultExpandedRowProps) => {
const items: ConditionalDescriptionItem[] = [
{
key: 'identifier',
Expand Down Expand Up @@ -78,10 +85,17 @@ export const ExperimentResultExpandedRow = ({
{ key: 'created_by', children: experimentResult.created_by },
// Not in the true Experiment Result schema, but can be added by the front end for linking purposes:
{
key: 'experiment_ids',
key: 'experiments',
label: 'entities.experiment_other',
children: (experimentResult.experiment_ids ?? []).join(', '), // TODO: link to expanded row + scrollTo
isVisible: objectToBoolean(experimentResult.experiment_ids),
// TODO: link to expanded row + scrollTo:
children: (
<PhenopacketLink.Experiments
packetId={packetId}
current={currentExperiment}
experiments={experimentResult.experiments ?? []}
/>
),
isVisible: objectToBoolean(experimentResult.experiments),
},
];

Expand Down Expand Up @@ -186,11 +200,19 @@ export const ExperimentResultActions = (props: ExperimentResultActionsProps) =>
};

type ExperimentResultViewProps = {
packetId?: string;
// Optional - if this is in the context of a single experiment, don't link the selected experiment:
currentExperiment?: string;
experimentResults: ExperimentResult[];
urlAware?: boolean;
};

const ExperimentResultView = ({ experimentResults, urlAware }: ExperimentResultViewProps) => {
const ExperimentResultView = ({
packetId,
currentExperiment,
experimentResults,
urlAware,
}: ExperimentResultViewProps) => {
const { hasAttempted: attemptedCanDownload, hasPermission: canDownload } = useScopeDownloadData();

urlAware = urlAware ?? true;
Expand All @@ -203,12 +225,15 @@ const ExperimentResultView = ({ experimentResults, urlAware }: ExperimentResultV
// TODO: nice render with modal to reference genome:
{ title: 'experiment_result.genome_assembly_id', dataIndex: 'genome_assembly_id' },
{ title: 'experiment_result.file_format', dataIndex: 'file_format' },
// TODO: link
{
title: 'experiment_result.experiment_ids',
dataIndex: 'experiment_ids',
isEmpty: (es: string[] | undefined) => !objectToBoolean(es),
render: (es: string[] | undefined) => (es ?? []).join(', '),
title: 'experiment_result.experiments',
dataIndex: 'experiments',
// Don't show experiments if we don't have any OR if the only experiment is the currently-selected one
isEmpty: (es: string[] | undefined) =>
!objectToBoolean(es) || (es?.length === 1 && es[0] === currentExperiment),
render: (es: string[] | undefined) => (
<PhenopacketLink.Experiments packetId={packetId} current={currentExperiment} experiments={es ?? []} />
),
},
{
title: 'general.actions',
Expand All @@ -218,14 +243,20 @@ const ExperimentResultView = ({ experimentResults, urlAware }: ExperimentResultV
render: (_, er) => <ExperimentResultActions url={er.url} filename={er.filename} fileFormat={er.file_format} />,
},
],
[attemptedCanDownload, canDownload]
[attemptedCanDownload, canDownload, packetId, currentExperiment]
);

return (
<CustomTable<ExperimentResult>
dataSource={experimentResults}
columns={columns}
expandedRowRender={(record) => <ExperimentResultExpandedRow experimentResult={record} />}
expandedRowRender={(record) => (
<ExperimentResultExpandedRow
packetId={packetId}
currentExperiment={currentExperiment}
experimentResult={record}
/>
)}
rowKey="id"
queryKey="experimentResult"
urlAware={urlAware}
Expand Down
16 changes: 14 additions & 2 deletions src/js/components/ClinPhen/ExperimentDisplay/ExperimentView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,13 @@ import { T_PLURAL_COUNT } from '@/constants/i18n';
import { useTranslationFn } from '@/hooks';
import { objectToBoolean } from '@/utils/boolean';

export const ExperimentExpandedRow = ({ experiment }: { experiment: Experiment; searchRow?: boolean }) => {
type ExperimentExpandedRowProps = {
packetId?: string;
experiment: Experiment;
searchRow?: boolean;
};

export const ExperimentExpandedRow = ({ packetId, experiment }: ExperimentExpandedRowProps) => {
const t = useTranslationFn();

const items: ConditionalDescriptionItem[] = [
Expand Down Expand Up @@ -64,7 +70,12 @@ export const ExperimentExpandedRow = ({ experiment }: { experiment: Experiment;
{/*
Cannot use query key row expansion in this nested view, so we force ExperimentResultView to use local state.
*/}
<ExperimentResultView experimentResults={experiment.experiment_results!} urlAware={false} />
<ExperimentResultView
packetId={packetId}
currentExperiment={experiment.id}
experimentResults={experiment.experiment_results!}
urlAware={false}
/>
</>
) : null}
</Space>
Expand Down Expand Up @@ -112,6 +123,7 @@ const EXPERIMENT_VIEW_COLUMNS: CustomTableColumns<Experiment> = [
];

type ExperimentViewProps = {
packetId?: string;
experiments: Experiment[];
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ const phenopacketExperimentResults = (p: Phenopacket): ExperimentResult[] => {
phenopacketExperiments(p).forEach((e: Experiment) => {
(e.experiment_results ?? []).forEach((er: ExperimentResult) => {
if (!(er.id in experimentResults)) {
experimentResults[er.id] = { ...er, experiment_ids: [] };
experimentResults[er.id] = { ...er, experiments: [] };
}
experimentResults[er.id].experiment_ids?.push(e.id);
experimentResults[er.id].experiments?.push(e.id);
});
});

Expand Down Expand Up @@ -106,13 +106,13 @@ export const SECTION_SPECS: Record<SectionKey, SectionSpec> = {
experiments: {
titleTranslationKey: 'entities.experiment_other',
enabled: (p) => has(phenopacketExperiments(p)),
render: (p) => <ExperimentView experiments={phenopacketExperiments(p)} />,
render: (p) => <ExperimentView packetId={p.id} experiments={phenopacketExperiments(p)} />,
order: 7,
},
experimentResults: {
titleTranslationKey: 'entities.experiment_result_other',
enabled: (p) => has(phenopacketExperimentResults(p)),
render: (p) => <ExperimentResultView experimentResults={phenopacketExperimentResults(p)} />,
render: (p) => <ExperimentResultView packetId={p.id} experimentResults={phenopacketExperimentResults(p)} />,
order: 8,
},
};
61 changes: 43 additions & 18 deletions src/js/components/ClinPhen/PhenopacketLink.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ReactNode } from 'react';
import { Fragment, type ReactNode } from 'react';
import { Link } from 'react-router-dom';
import { useLangPrefixedUrl } from '@/hooks/navigation';
import { PHENOPACKET_EXPANDED_URL_QUERY_KEY } from './PhenopacketDisplay/PhenopacketOverview';
Expand All @@ -13,40 +13,65 @@ const usePhenopacketOverviewLink = (
return `${baseUrl}?${params.toString()}`;
};

export const PhenopacketSubjectLink = ({ children, packetId }: { children: ReactNode; packetId?: string }) => {
// TODO: replace with context?
type BaseLinkProps = { packetId?: string };

type SubjectLinkProps = BaseLinkProps & { children: ReactNode };
const SubjectLink = ({ children, packetId }: SubjectLinkProps) => {
const url = usePhenopacketOverviewLink(packetId, 'subject');
return packetId ? <Link to={url}>{children}</Link> : children;
};

export const PhenopacketBiosampleLink = ({ packetId, sampleId }: { packetId: string; sampleId: string }) => {
type BiosampleLinkProps = BaseLinkProps & { sampleId: string };
const BiosampleLink = ({ packetId, sampleId }: BiosampleLinkProps) => {
const url = usePhenopacketOverviewLink(packetId, 'biosamples', { biosample: sampleId });
return <Link to={url}>{sampleId}</Link>;
return packetId ? <Link to={url}>{sampleId}</Link> : sampleId;
};

export const PhenopacketExperimentLink = ({ packetId, experimentId }: { packetId?: string; experimentId: string }) => {
type BiosampleLinkListProps = BaseLinkProps & { biosamples: string[] };
const BiosampleLinkList = ({ packetId, biosamples }: BiosampleLinkListProps) => (
<>
{biosamples.map((bb, bbi) => (
<Fragment key={bb}>
<BiosampleLink packetId={packetId} sampleId={bb} />
{bbi < biosamples.length - 1 ? ', ' : ''}
</Fragment>
))}
</>
);

type ExperimentLinkProps = BaseLinkProps & { experimentId: string };
const ExperimentLink = ({ packetId, experimentId }: ExperimentLinkProps) => {
const url = usePhenopacketOverviewLink(packetId, 'experiments', { experiment: experimentId });
return packetId ? <Link to={url}>{experimentId}</Link> : experimentId;
};

export const PhenopacketExperimentResultLink = ({
packetId,
experimentResultId,
children,
}: {
packetId?: string;
experimentResultId: number;
children?: ReactNode;
}) => {
type ExperimentResultLinkProps = BaseLinkProps & { experimentResultId: number; children?: ReactNode };
const ExperimentResultLink = ({ packetId, experimentResultId, children }: ExperimentResultLinkProps) => {
const url = usePhenopacketOverviewLink(packetId, 'experimentResults', {
experimentResult: experimentResultId.toString(10),
});
children = children ?? experimentResultId;
return packetId ? <Link to={url}>{children}</Link> : children;
};

type ExperimentLinkListProps = BaseLinkProps & { current?: string; experiments: string[] };
export const ExperimentLinkList = ({ packetId, current, experiments }: ExperimentLinkListProps) => (
<>
{experiments.map((experimentId, i) => (
<Fragment key={i}>
<ExperimentLink packetId={current === experimentId ? undefined : packetId} experimentId={experimentId} />
{i < experiments.length - 1 ? ', ' : ''}
</Fragment>
))}
</>
);

export default {
Subject: PhenopacketSubjectLink,
Biosample: PhenopacketBiosampleLink,
Experiment: PhenopacketExperimentLink,
ExperimentResult: PhenopacketExperimentResultLink,
Subject: SubjectLink,
Biosample: BiosampleLink,
Biosamples: BiosampleLinkList,
Experiment: ExperimentLink,
Experiments: ExperimentLinkList,
ExperimentResult: ExperimentResultLink,
};
21 changes: 12 additions & 9 deletions src/js/components/Search/SearchResultsTablePage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type ReactNode, Fragment, useCallback, useEffect, useMemo, useState } from 'react';
import { type ReactNode, useCallback, useEffect, useMemo, useState } from 'react';

import { Button, Checkbox, Col, Flex, Modal, Space, type TablePaginationConfig, Tooltip, Typography } from 'antd';
import { ExportOutlined, LeftOutlined, TableOutlined } from '@ant-design/icons';
Expand Down Expand Up @@ -102,13 +102,9 @@ const PHENOPACKET_SEARCH_TABLE_COLUMNS = {
biosamples: {
title: 'entities.biosample_other',
dataIndex: 'biosamples',
render: (_ctx: SearchColRenderContext) => (b: DiscoveryMatchBiosample[], p: DiscoveryMatchPhenopacket) =>
b.map((bb, bbi) => (
<Fragment key={bb.id}>
<PhenopacketLink.Biosample packetId={p.id} sampleId={bb.id} />
{bbi < b.length - 1 ? ', ' : ''}
</Fragment>
)),
render: (_ctx: SearchColRenderContext) => (b: DiscoveryMatchBiosample[], p: DiscoveryMatchPhenopacket) => (
<PhenopacketLink.Biosamples packetId={p.id} biosamples={b.map((bb) => bb.id)} />
),
},
...commonSearchTableColumns<DiscoveryMatchPhenopacket>(),
} as Record<string, ResultsTableColumn<DiscoveryMatchPhenopacket>>;
Expand Down Expand Up @@ -137,6 +133,13 @@ const EXPERIMENT_RESULT_SEARCH_TABLE_COLUMNS = {
title: 'experiment_result.file_format',
dataIndex: 'file_format',
} as ResultsTableColumn<DiscoveryMatchExperimentResult>,
experiments: {
title: 'experiment_result.experiments',
dataIndex: 'experiments',
render: (_ctx) => (experiments: string[], er) => (
<PhenopacketLink.Experiments packetId={er?.phenopacket} experiments={experiments} />
),
} as ResultsTableColumn<DiscoveryMatchExperimentResult>,
...commonSearchTableColumns<DiscoveryMatchExperimentResult>(),
};

Expand Down Expand Up @@ -215,7 +218,7 @@ const TABLE_SPEC_EXPERIMENT_RESULT: ResultsTableSpec<DiscoveryMatchExperimentRes
} as ResultsTableFixedColumn<DiscoveryMatchExperimentResult>,
],
availableColumns: EXPERIMENT_RESULT_SEARCH_TABLE_COLUMNS,
defaultColumns: ['description', 'genome_assembly_id', 'file_format', 'project', 'dataset'],
defaultColumns: ['genome_assembly_id', 'file_format', 'experiments', 'project', 'dataset'],
expandedRowRender: (rec) => <ExperimentResultRowDetail id={rec.id} />,
};

Expand Down
1 change: 1 addition & 0 deletions src/js/features/search/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export type DiscoveryMatchExperimentResult = DiscoveryMatchObject & {
created_by?: string;
extra_properties?: JSONType;
// ---
experiments: string[];
phenopacket?: string;
};

Expand Down
2 changes: 1 addition & 1 deletion src/js/types/clinPhen/experiments/experimentResult.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,5 @@ export interface ExperimentResult extends ExtraPropertiesEntity {
creation_date?: string;
created_by?: string;
// Can be added in front end:
experiment_ids?: string[];
experiments?: string[];
}
2 changes: 1 addition & 1 deletion src/public/locales/en/default_translation_en.json
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,7 @@
"usage": "Usage",
"creation_date": "Creation Date",
"created_by": "Created By",
"experiment_ids": "Experiment IDs"
"experiments": "$t(entities.experiment_other)"
},
"auth": {
"unauthorized_message": "You are not authorized to access this resource."
Expand Down
1 change: 0 additions & 1 deletion src/public/locales/fr/default_translation_fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -455,7 +455,6 @@
"usage": "Utilisation",
"creation_date": "Date de création",
"created_by": "Créé par",
"experiment_ids": "Identifiants d'expériences"
},
"auth": {
"unauthorized_message": "Vous n'êtes pas autorisé à accéder à cette ressource."
Expand Down