diff --git a/CHANGELOG-cat-706.md b/CHANGELOG-cat-706.md
new file mode 100644
index 0000000000..d7a9770a39
--- /dev/null
+++ b/CHANGELOG-cat-706.md
@@ -0,0 +1 @@
+- Add title to changelog page.
diff --git a/CHANGELOG-entity-header-styling.md b/CHANGELOG-entity-header-styling.md
new file mode 100644
index 0000000000..d7f8bde1a3
--- /dev/null
+++ b/CHANGELOG-entity-header-styling.md
@@ -0,0 +1 @@
+- Fix Webkit-specific bugs that caused the entity header to become misaligned.
\ No newline at end of file
diff --git a/CHANGELOG-fix-metadata-table.md b/CHANGELOG-fix-metadata-table.md
new file mode 100644
index 0000000000..fe89cfa966
--- /dev/null
+++ b/CHANGELOG-fix-metadata-table.md
@@ -0,0 +1,3 @@
+- Switch to using metadata table with tabs component in the Sample and Donor pages.
+- In the metadata sections of Dataset, Sample, and Donor pages, add tabs for any entities in the Provenance section with metadata.
+- Update the metadata table component to show unique labels for each tab and to be scrollable when many tabs are present.
\ No newline at end of file
diff --git a/CHANGELOG-remove-qa-workspace-option.md b/CHANGELOG-remove-qa-workspace-option.md
new file mode 100644
index 0000000000..72d5b720f4
--- /dev/null
+++ b/CHANGELOG-remove-qa-workspace-option.md
@@ -0,0 +1 @@
+- Fix issue of Workspaces button being available for protected datasets in the Visualization section.
\ No newline at end of file
diff --git a/context/app/markdown/CHANGELOG.md b/context/app/markdown/CHANGELOG.md
index 7277456f00..d56ad9622c 100644
--- a/context/app/markdown/CHANGELOG.md
+++ b/context/app/markdown/CHANGELOG.md
@@ -1,3 +1,5 @@
+# Changelog
+
## v1.12.1 - 2024-10-10
- Prevent collections without a DOI from being counted on the homepage.
diff --git a/context/app/static/js/components/Header/Header/Header.tsx b/context/app/static/js/components/Header/Header/Header.tsx
index fd20fa4ce0..f041e7754e 100644
--- a/context/app/static/js/components/Header/Header/Header.tsx
+++ b/context/app/static/js/components/Header/Header/Header.tsx
@@ -1,5 +1,6 @@
import React from 'react';
+import Box from '@mui/material/Box';
import EntityHeader from 'js/components/detailPage/entityHeader/EntityHeader';
import HeaderAppBar from '../HeaderAppBar';
import HeaderContent from '../HeaderContent';
@@ -9,12 +10,12 @@ function Header() {
const { shouldDisplayHeader, ...props } = useEntityHeaderVisibility();
return (
- <>
+ ({ position: 'fixed', width: '100%', zIndex: theme.zIndex.header })}>
{shouldDisplayHeader && }
- >
+
);
}
diff --git a/context/app/static/js/components/detailPage/MetadataSection/MetadataSection.tsx b/context/app/static/js/components/detailPage/MetadataSection/MetadataSection.tsx
index 09cd0fdb72..1ec2ce4e3b 100644
--- a/context/app/static/js/components/detailPage/MetadataSection/MetadataSection.tsx
+++ b/context/app/static/js/components/detailPage/MetadataSection/MetadataSection.tsx
@@ -6,66 +6,14 @@ import { CollapsibleDetailPageSection } from 'js/components/detailPage/DetailPag
import { useTrackEntityPageEvent } from 'js/components/detailPage/useTrackEntityPageEvent';
import { tableToDelimitedString, createDownloadUrl } from 'js/helpers/functions';
import { useMetadataFieldDescriptions } from 'js/hooks/useUBKG';
-import { getMetadata, hasMetadata } from 'js/helpers/metadata';
-import { ESEntityType, isDataset } from 'js/components/types';
-import { useProcessedDatasets } from 'js/pages/Dataset/hooks';
-import { entityIconMap } from 'js/shared-styles/icons/entityIconMap';
+import { Dataset, Donor, Sample, isDataset } from 'js/components/types';
import withShouldDisplay from 'js/helpers/withShouldDisplay';
import { sectionIconMap } from 'js/shared-styles/icons/sectionIconMap';
+import { getTableEntities } from 'js/components/detailPage/MetadataSection/utils';
import { DownloadIcon, StyledWhiteBackgroundIconButton } from '../MetadataTable/style';
import MetadataTabs from '../multi-assay/MultiAssayMetadataTabs';
-import { Columns, defaultTSVColumns } from './columns';
import { SectionDescription } from '../ProcessedData/ProcessedDataset/SectionDescription';
-import MetadataTable from '../MetadataTable';
-import { nodeIcons } from '../DatasetRelationships/nodeTypes';
-
-export function getDescription(
- field: string,
- metadataFieldDescriptions: Record | Record,
-) {
- const [prefix, stem] = field.split('.');
- if (!stem) {
- return metadataFieldDescriptions?.[field];
- }
- const description = metadataFieldDescriptions?.[stem];
- if (!description) {
- return undefined;
- }
- if (prefix === 'donor') {
- return `For the original donor: ${metadataFieldDescriptions?.[stem]}`;
- }
- if (prefix === 'sample') {
- return `For the original sample: ${metadataFieldDescriptions?.[stem]}`;
- }
- throw new Error(`Unrecognized metadata field prefix: ${prefix}`);
-}
-
-export function buildTableData(
- tableData: Record,
- metadataFieldDescriptions: Record | Record,
- extraValues: Record = {},
-) {
- return (
- Object.entries(tableData)
- // Filter out nested objects, like nested "metadata" for Samples...
- // but allow arrays. Remember, in JS: typeof [] === 'object'
- .filter((entry) => typeof entry[1] !== 'object' || Array.isArray(entry[1]))
- // Filter out fields from TSV that aren't really metadata:
- .filter((entry) => !['contributors_path', 'antibodies_path', 'version'].includes(entry[0]))
- .map((entry) => ({
- ...extraValues,
- key: entry[0],
- // eslint-disable-next-line @typescript-eslint/no-base-to-string
- value: Array.isArray(entry[1]) ? entry[1].join(', ') : entry[1].toString(),
- description: getDescription(entry[0], metadataFieldDescriptions),
- }))
- );
-}
-
-function useTableData(tableData: Record) {
- const { data: fieldDescriptions } = useMetadataFieldDescriptions();
- return buildTableData(tableData, fieldDescriptions);
-}
+import { Columns, defaultTSVColumns } from './columns';
interface TableRow {
key: string;
@@ -124,76 +72,25 @@ function MetadataWrapper({ allTableRows, tsvColumns = defaultTSVColumns, childre
);
}
-function SingleMetadata({ metadata }: { metadata: Record }) {
- const tableRows = useTableData(metadata);
-
- return (
-
-
-
- );
-}
-
-function getEntityIcon(entity: { entity_type: ESEntityType; is_component?: boolean; processing?: string }) {
- if (isDataset(entity)) {
- if (entity.is_component) {
- return nodeIcons.componentDataset;
- }
- if (entity.processing === 'processed') {
- return nodeIcons.processedDataset;
- }
- return nodeIcons.primaryDataset;
- }
- return entityIconMap[entity.entity_type];
-}
-
interface MetadataProps {
- metadata?: Record;
+ entities: (Donor | Dataset | Sample)[];
}
-function Metadata({ metadata }: MetadataProps) {
- const { searchHits: datasetsWithMetadata, isLoading } = useProcessedDatasets(true);
+function Metadata({ entities }: MetadataProps) {
const { data: fieldDescriptions } = useMetadataFieldDescriptions();
+ const {
+ entity: { uuid },
+ } = useFlaskDataContext();
- const { entity } = useFlaskDataContext();
-
- if (!isDataset(entity)) {
- return ;
- }
-
- if (isLoading || !datasetsWithMetadata) {
- return null;
- }
-
- const { donor, source_samples } = entity;
-
- const entities = [entity, ...datasetsWithMetadata.map((d) => d._source), ...source_samples, donor]
- .filter((e) => hasMetadata({ targetEntityType: e.entity_type, currentEntity: e }))
- .map((e) => {
- const label = isDataset(e) ? e.assay_display_name : e.entity_type;
- return {
- uuid: e.uuid,
- label,
- icon: getEntityIcon(e),
- tableRows: buildTableData(
- getMetadata({
- targetEntityType: e.entity_type,
- currentEntity: e,
- }),
- fieldDescriptions,
- { hubmap_id: e.hubmap_id, label },
- ),
- };
- });
-
- const allTableRows = entities.map((d) => d.tableRows).flat();
+ const tableEntities = getTableEntities({ entities, uuid, fieldDescriptions });
+ const allTableRows = tableEntities.map((d) => d.tableRows).flat();
return (
-
+
);
}
diff --git a/context/app/static/js/components/detailPage/MetadataSection/MetadataTable.spec.ts b/context/app/static/js/components/detailPage/MetadataSection/MetadataTable.spec.ts
deleted file mode 100644
index cad1593421..0000000000
--- a/context/app/static/js/components/detailPage/MetadataSection/MetadataTable.spec.ts
+++ /dev/null
@@ -1,79 +0,0 @@
-import { getDescription, buildTableData } from './MetadataSection';
-
-test('should handle plain fields', () => {
- // Wouldn't actually expect to see age_value anywhere except for donor,
- // but shouldn't make a difference... and if it does, we want to know.
- expect(getDescription('age_value', { age_value: 'The time elapsed since birth.' })).toEqual(
- 'The time elapsed since birth.',
- );
-});
-
-test('should handle donor fields', () => {
- expect(getDescription('donor.age_value', { age_value: 'The time elapsed since birth.' })).toEqual(
- 'For the original donor: The time elapsed since birth.',
- );
-});
-
-test('should handle sample fields', () => {
- expect(getDescription('sample.age_value', { age_value: 'The time elapsed since birth.' })).toEqual(
- 'For the original sample: The time elapsed since birth.',
- );
-});
-
-test('should return undefined if there is not a definition', () => {
- expect(getDescription('sample.no_such_stem', {})).toEqual(undefined);
-});
-
-test('should error if stem is known, but prefix is not', () => {
- expect(() => getDescription('no_such_prefix.age_value', { age_value: 'The time elapsed since birth.' })).toThrow();
-});
-
-test('should look up field descriptions', () => {
- expect(
- buildTableData(
- {
- assay_type: 'FAKE-seq',
- },
- {
- assay_type: 'The specific type of assay being executed.',
- },
- ),
- ).toEqual([
- {
- description: 'The specific type of assay being executed.',
- key: 'assay_type',
- value: 'FAKE-seq',
- },
- ]);
-});
-
-test('should remove nested objects, but concat nested lists', () => {
- expect(
- buildTableData(
- {
- object: { foo: 'bar' },
- list: ['foo', 'bar'],
- },
- {},
- ),
- ).toEqual([
- {
- description: undefined,
- key: 'list',
- value: 'foo, bar',
- },
- ]);
-});
-
-test('should remove keys that are not metadata', () => {
- expect(
- buildTableData(
- {
- contributors_path: '/local/path',
- antibodies_path: '/local/path',
- version: '42',
- },
- {},
- ),
- ).toEqual([]);
-});
diff --git a/context/app/static/js/components/detailPage/MetadataSection/utils.spec.ts b/context/app/static/js/components/detailPage/MetadataSection/utils.spec.ts
new file mode 100644
index 0000000000..ffd24d4584
--- /dev/null
+++ b/context/app/static/js/components/detailPage/MetadataSection/utils.spec.ts
@@ -0,0 +1,145 @@
+import { DonorIcon } from 'js/shared-styles/icons';
+import { buildTableData, sortEntities, TableEntity } from './utils';
+
+/**
+ * =============================================================================
+ * buildTableData
+ * =============================================================================
+ */
+
+test('should look up field descriptions', () => {
+ expect(
+ buildTableData(
+ {
+ assay_type: 'FAKE-seq',
+ },
+ {
+ assay_type: 'The specific type of assay being executed.',
+ },
+ ),
+ ).toEqual([
+ {
+ description: 'The specific type of assay being executed.',
+ key: 'assay_type',
+ value: 'FAKE-seq',
+ },
+ ]);
+});
+
+test('should remove nested objects, but concat nested lists', () => {
+ expect(
+ buildTableData(
+ {
+ object: { foo: 'bar' },
+ list: ['foo', 'bar'],
+ },
+ {},
+ ),
+ ).toEqual([
+ {
+ description: undefined,
+ key: 'list',
+ value: 'foo, bar',
+ },
+ ]);
+});
+
+test('should remove keys that are not metadata', () => {
+ expect(
+ buildTableData(
+ {
+ contributors_path: '/local/path',
+ antibodies_path: '/local/path',
+ version: '42',
+ },
+ {},
+ ),
+ ).toEqual([]);
+});
+
+/**
+ * =============================================================================
+ * sortEntities
+ * =============================================================================
+ */
+
+const baseEntity = {
+ icon: DonorIcon,
+ tableRows: [],
+};
+
+const current: TableEntity = {
+ ...baseEntity,
+ uuid: 'current',
+ label: 'Current',
+ entity_type: 'Dataset',
+ hubmap_id: 'current',
+};
+
+const donor: TableEntity = {
+ ...baseEntity,
+ uuid: 'donor',
+ label: 'Donor',
+ entity_type: 'Donor',
+ hubmap_id: 'donor',
+};
+
+const uniqueSample1: TableEntity = {
+ ...baseEntity,
+ uuid: 'sample1',
+ label: 'Unique Category',
+ entity_type: 'Sample',
+ hubmap_id: 'sample1',
+};
+
+const duplicateSample2: TableEntity = {
+ ...baseEntity,
+ uuid: 'sample2',
+ label: 'Duplicate Category (sample2)',
+ entity_type: 'Sample',
+ hubmap_id: 'sample2',
+};
+
+const duplicateSample3: TableEntity = {
+ ...baseEntity,
+ uuid: 'sample3',
+ label: 'Duplicate Category (sample3)',
+ entity_type: 'Sample',
+ hubmap_id: 'sample3',
+};
+
+test('should sort by current -> donor -> samples without IDs in their label -> samples with IDs in their label', () => {
+ expect(
+ sortEntities({
+ tableEntities: [duplicateSample2, duplicateSample3, donor, current, uniqueSample1],
+ uuid: 'current',
+ }),
+ ).toEqual([current, donor, uniqueSample1, duplicateSample2, duplicateSample3]);
+});
+
+test('should sort by current -> samples without IDs in their label -> samples with IDs in their label when donor is absent', () => {
+ expect(
+ sortEntities({
+ tableEntities: [duplicateSample2, current, uniqueSample1, duplicateSample3],
+ uuid: 'current',
+ }),
+ ).toEqual([current, uniqueSample1, duplicateSample2, duplicateSample3]);
+});
+
+test('should sort by donor -> samples without IDs -> samples with IDs when current is absent', () => {
+ expect(
+ sortEntities({
+ tableEntities: [duplicateSample3, duplicateSample2, donor, uniqueSample1],
+ uuid: 'current',
+ }),
+ ).toEqual([donor, uniqueSample1, duplicateSample2, duplicateSample3]);
+});
+
+test('should return only the current entity if no other entities are present', () => {
+ expect(
+ sortEntities({
+ tableEntities: [current],
+ uuid: 'current',
+ }),
+ ).toEqual([current]);
+});
diff --git a/context/app/static/js/components/detailPage/MetadataSection/utils.ts b/context/app/static/js/components/detailPage/MetadataSection/utils.ts
new file mode 100644
index 0000000000..639bb791f8
--- /dev/null
+++ b/context/app/static/js/components/detailPage/MetadataSection/utils.ts
@@ -0,0 +1,121 @@
+import { TableRows } from 'js/components/detailPage/MetadataSection/MetadataSection';
+import { Dataset, Donor, ESEntityType, Sample, isDataset, isSample } from 'js/components/types';
+import { getEntityIcon } from 'js/helpers/functions';
+import { getMetadata } from 'js/helpers/metadata';
+import { ProcessedDatasetInfo } from 'js/pages/Dataset/hooks';
+import { MUIIcon } from 'js/shared-styles/icons/entityIconMap';
+
+function buildTableData(
+ tableData: Record,
+ metadataFieldDescriptions: Record | Record,
+ extraValues: Record = {},
+) {
+ return (
+ Object.entries(tableData)
+ // Filter out nested objects, like nested "metadata" for Samples...
+ // but allow arrays. Remember, in JS: typeof [] === 'object'
+ .filter((entry) => typeof entry[1] !== 'object' || Array.isArray(entry[1]))
+ // Filter out fields from TSV that aren't really metadata:
+ .filter((entry) => !['contributors_path', 'antibodies_path', 'version'].includes(entry[0]))
+ .map((entry) => ({
+ ...extraValues,
+ key: entry[0],
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string
+ value: Array.isArray(entry[1]) ? entry[1].join(', ') : entry[1].toString(),
+ description: metadataFieldDescriptions?.[entry[0]],
+ }))
+ );
+}
+
+export interface TableEntity {
+ uuid: string;
+ label: string;
+ icon: MUIIcon;
+ tableRows: TableRows;
+ entity_type: ESEntityType;
+ hubmap_id: string;
+}
+
+interface sortEntitiesProps {
+ tableEntities: TableEntity[];
+ uuid: string;
+}
+
+function sortEntities({ tableEntities, uuid }: sortEntitiesProps) {
+ return [...tableEntities].sort((a, b) => {
+ // Current entity at the front
+ if (a.uuid === uuid) return -1;
+ if (b.uuid === uuid) return 1;
+
+ // Then donors
+ if (a.entity_type === 'Donor' && b.entity_type !== 'Donor') return -1;
+ if (b.entity_type === 'Donor' && a.entity_type !== 'Donor') return 1;
+
+ // Then samples, with unique categories first
+ const aIsSampleWithoutHubmapId = a.entity_type === 'Sample' && !a.label.includes(a.hubmap_id);
+ const bIsSampleWithoutHubmapId = b.entity_type === 'Sample' && !b.label.includes(b.hubmap_id);
+ if (aIsSampleWithoutHubmapId && !bIsSampleWithoutHubmapId) return -1;
+ if (bIsSampleWithoutHubmapId && !aIsSampleWithoutHubmapId) return 1;
+
+ return a.label.localeCompare(b.label);
+ });
+}
+
+interface getEntityLabelProps {
+ entity: ProcessedDatasetInfo | Donor | Sample;
+ sampleCategoryCounts: Record;
+}
+
+function getEntityLabel({ entity, sampleCategoryCounts }: getEntityLabelProps) {
+ if (isSample(entity)) {
+ // If samples have the same category, add the HuBMAP ID to the label
+ if (sampleCategoryCounts[entity.sample_category] > 1) {
+ return `${entity.sample_category} (${entity.hubmap_id})`;
+ }
+ return entity.sample_category;
+ }
+ if (isDataset(entity)) {
+ return entity.assay_display_name;
+ }
+ return entity.entity_type;
+}
+
+interface getTableEntitiesProps {
+ entities: (Donor | Dataset | Sample)[];
+ uuid: string;
+ fieldDescriptions: Record;
+}
+
+function getTableEntities({ entities, uuid, fieldDescriptions }: getTableEntitiesProps) {
+ // Keep track of whether there are multiple samples with the same category
+ const sampleCategoryCounts: Record = {};
+ entities.forEach((e) => {
+ if (isSample(e)) {
+ sampleCategoryCounts[e.sample_category] = (sampleCategoryCounts[e.sample_category] || 0) + 1;
+ }
+ });
+
+ const tableEntities = entities.map((entity) => {
+ // Generate a label with the HuBMAP ID if there are multiple samples with the same category
+ const label = getEntityLabel({ entity, sampleCategoryCounts });
+ return {
+ uuid: entity.uuid,
+ label: label ?? '',
+ icon: getEntityIcon(entity),
+ tableRows: buildTableData(
+ getMetadata({
+ targetEntityType: entity.entity_type,
+ currentEntity: entity,
+ }),
+ fieldDescriptions,
+ { hubmap_id: entity.hubmap_id, label },
+ ),
+ entity_type: entity.entity_type,
+ hubmap_id: entity.hubmap_id,
+ };
+ });
+
+ return sortEntities({ tableEntities, uuid });
+}
+
+export { getTableEntities, buildTableData, sortEntities };
diff --git a/context/app/static/js/components/detailPage/entityHeader/EntityHeader/style.ts b/context/app/static/js/components/detailPage/entityHeader/EntityHeader/style.ts
index 8cd749ed3d..0c8d0fc0a4 100644
--- a/context/app/static/js/components/detailPage/entityHeader/EntityHeader/style.ts
+++ b/context/app/static/js/components/detailPage/entityHeader/EntityHeader/style.ts
@@ -7,7 +7,7 @@ const StyledPaper = styled(Paper)(({ theme }) => ({
position: 'sticky',
width: '100%',
top: headerHeight,
- zIndex: theme.zIndex.entityHeader,
+ zIndex: theme.zIndex.header,
}));
export { StyledPaper };
diff --git a/context/app/static/js/components/detailPage/entityHeader/EntityHeaderContent/EntityHeaderContent.tsx b/context/app/static/js/components/detailPage/entityHeader/EntityHeaderContent/EntityHeaderContent.tsx
index 9d38915dc7..26911cd1b2 100644
--- a/context/app/static/js/components/detailPage/entityHeader/EntityHeaderContent/EntityHeaderContent.tsx
+++ b/context/app/static/js/components/detailPage/entityHeader/EntityHeaderContent/EntityHeaderContent.tsx
@@ -217,6 +217,7 @@ function EntityHeaderContent({ view, setView }: { view: SummaryViewsType; setVie
({ ...(view !== 'narrow' && { borderBottom: `1px solid ${theme.palette.primary.lowEmphasis}` }) })}
diff --git a/context/app/static/js/components/detailPage/multi-assay/MultiAssayMetadataTabs/MultiAssayMetadataTabs.tsx b/context/app/static/js/components/detailPage/multi-assay/MultiAssayMetadataTabs/MultiAssayMetadataTabs.tsx
index e0af51e4a7..8148676cb8 100644
--- a/context/app/static/js/components/detailPage/multi-assay/MultiAssayMetadataTabs/MultiAssayMetadataTabs.tsx
+++ b/context/app/static/js/components/detailPage/multi-assay/MultiAssayMetadataTabs/MultiAssayMetadataTabs.tsx
@@ -38,7 +38,7 @@ function MetadataTabs({ entities }: { entities: MultiAssayEntityWithTableRows[]
return (
-
+ 4 ? 'scrollable' : 'fullWidth'}>
{entities.map(({ label, uuid, icon }, index) => (
))}
diff --git a/context/app/static/js/components/detailPage/visualization/Visualization/Visualization.tsx b/context/app/static/js/components/detailPage/visualization/Visualization/Visualization.tsx
index 1b8a73cb60..bd8ff0c846 100644
--- a/context/app/static/js/components/detailPage/visualization/Visualization/Visualization.tsx
+++ b/context/app/static/js/components/detailPage/visualization/Visualization/Visualization.tsx
@@ -43,8 +43,6 @@ const visualizationStoreSelector = (state: VisualizationStore) => ({
interface VisualizationProps {
vitData: object | object[];
uuid?: string;
- hubmap_id?: string;
- mapped_data_access_level?: string;
hasNotebook: boolean;
shouldDisplayHeader: boolean;
shouldMountVitessce?: boolean;
@@ -54,8 +52,6 @@ interface VisualizationProps {
function Visualization({
vitData,
uuid,
- hubmap_id,
- mapped_data_access_level,
hasNotebook,
shouldDisplayHeader,
shouldMountVitessce = true,
@@ -122,12 +118,7 @@ function Visualization({
leftText={shouldDisplayHeader ? Visualization : undefined}
buttons={
-
+ {hasNotebook && }
diff --git a/context/app/static/js/components/detailPage/visualization/VisualizationWorkspaceButton/VisualizationWorkspaceButton.tsx b/context/app/static/js/components/detailPage/visualization/VisualizationWorkspaceButton/VisualizationWorkspaceButton.tsx
index dde0ef9f18..303f41de6f 100644
--- a/context/app/static/js/components/detailPage/visualization/VisualizationWorkspaceButton/VisualizationWorkspaceButton.tsx
+++ b/context/app/static/js/components/detailPage/visualization/VisualizationWorkspaceButton/VisualizationWorkspaceButton.tsx
@@ -7,30 +7,29 @@ import NewWorkspaceDialog from 'js/components/workspaces/NewWorkspaceDialog';
import { useCreateWorkspaceForm } from 'js/components/workspaces/NewWorkspaceDialog/useCreateWorkspaceForm';
import { WhiteBackgroundIconTooltipButton } from 'js/shared-styles/buttons';
import { useAppContext } from 'js/components/Contexts';
+import { useDetailContext } from 'js/components/detailPage/DetailContext';
+import { useProcessedDatasetDetails } from 'js/components/detailPage/ProcessedData/ProcessedDataset/hooks';
const tooltip = 'Launch New Workspace';
interface VisualizationWorkspaceButtonProps {
uuid?: string;
- hubmap_id?: string;
- hasNotebook?: boolean;
- mapped_data_access_level?: string;
}
-function VisualizationWorkspaceButton({
- uuid = '',
- hubmap_id,
- hasNotebook,
- mapped_data_access_level,
-}: VisualizationWorkspaceButtonProps) {
+function VisualizationWorkspaceButton({ uuid = '' }: VisualizationWorkspaceButtonProps) {
+ const { mapped_data_access_level } = useDetailContext();
+ const {
+ datasetDetails: { hubmap_id },
+ } = useProcessedDatasetDetails(uuid);
const { isWorkspacesUser } = useAppContext();
+
const { setDialogIsOpen, removeDatasets, ...rest } = useCreateWorkspaceForm({
defaultName: hubmap_id,
defaultTemplate: 'visualization',
initialSelectedDatasets: [uuid],
});
- if (!isWorkspacesUser ?? !uuid ?? !hubmap_id ?? !hasNotebook ?? mapped_data_access_level === 'Protected') {
+ if (!isWorkspacesUser || !hubmap_id || mapped_data_access_level !== 'Public') {
return null;
}
diff --git a/context/app/static/js/components/detailPage/visualization/VisualizationWrapper/VisualizationWrapper.tsx b/context/app/static/js/components/detailPage/visualization/VisualizationWrapper/VisualizationWrapper.tsx
index 94da48470b..bd58c89b3f 100644
--- a/context/app/static/js/components/detailPage/visualization/VisualizationWrapper/VisualizationWrapper.tsx
+++ b/context/app/static/js/components/detailPage/visualization/VisualizationWrapper/VisualizationWrapper.tsx
@@ -10,8 +10,6 @@ const Visualization = React.lazy(() => import('../Visualization'));
interface VisualizationWrapperProps {
vitData: object | object[];
uuid?: string;
- hubmap_id?: string;
- mapped_data_access_level?: string;
hasNotebook?: boolean;
shouldDisplayHeader?: boolean;
hasBeenMounted?: boolean;
@@ -22,8 +20,6 @@ interface VisualizationWrapperProps {
function VisualizationWrapper({
vitData,
uuid,
- hubmap_id,
- mapped_data_access_level,
hasNotebook = false,
shouldDisplayHeader = true,
hasBeenMounted,
@@ -45,8 +41,6 @@ function VisualizationWrapper({
(uuids: string[], source?: string[]): [T[], boolean] {
const query = useEntityQuery(uuids, source);
- const { searchHits, isLoading } = useSearchHits(query);
+ const { searchHits, isLoading } = useSearchHits(query);
return [searchHits.map((hit) => hit._source), isLoading];
}
diff --git a/context/app/static/js/pages/Dataset/Dataset.tsx b/context/app/static/js/pages/Dataset/Dataset.tsx
index 37a83738a2..4f7c3be0fd 100644
--- a/context/app/static/js/pages/Dataset/Dataset.tsx
+++ b/context/app/static/js/pages/Dataset/Dataset.tsx
@@ -17,7 +17,7 @@ import { getCombinedDatasetStatus } from 'js/components/detailPage/utils';
import ComponentAlert from 'js/components/detailPage/multi-assay/ComponentAlert';
import MultiAssayRelationship from 'js/components/detailPage/multi-assay/MultiAssayRelationship';
import MetadataSection from 'js/components/detailPage/MetadataSection';
-import { Dataset, Entity, isDataset } from 'js/components/types';
+import { Dataset, Donor, Entity, Sample, isDataset } from 'js/components/types';
import DatasetRelationships from 'js/components/detailPage/DatasetRelationships';
import ProcessedDataSection from 'js/components/detailPage/ProcessedData';
import { SelectedVersionStoreProvider } from 'js/components/detailPage/VersionSelect/SelectedVersionStore';
@@ -28,7 +28,8 @@ import { useDatasetsCollections } from 'js/hooks/useDatasetsCollections';
import useTrackID from 'js/hooks/useTrackID';
import { InternalLink } from 'js/shared-styles/Links';
import OrganIcon from 'js/shared-styles/icons/OrganIcon';
-
+import { useEntitiesData } from 'js/hooks/useEntityData';
+import { hasMetadata } from 'js/helpers/metadata';
import { useProcessedDatasets, useProcessedDatasetsSections, useRedirectAlert } from './hooks';
interface SummaryDataChildrenProps {
@@ -94,8 +95,14 @@ function DatasetDetail({ assayMetadata }: EntityDetailProps) {
is_component,
assay_modality,
processing,
+ ancestor_ids,
} = assayMetadata;
+ const [entities, loadingEntities] = useEntitiesData([uuid, ...ancestor_ids]);
+ const entitiesWithMetadata = entities.filter((e) =>
+ hasMetadata({ targetEntityType: e.entity_type, currentEntity: e }),
+ );
+
useRedirectAlert();
const origin_sample = origin_samples[0];
@@ -122,6 +129,10 @@ function DatasetDetail({ assayMetadata }: EntityDetailProps) {
const { shouldDisplay: shouldDisplayRelationships } = useDatasetRelationships(uuid, processing);
+ if (loadingEntities) {
+ return null;
+ }
+
return (
ds._id) ?? []}>
@@ -146,7 +157,7 @@ function DatasetDetail({ assayMetadata }: EntityDetailProps) {
>
-
+
diff --git a/context/app/static/js/pages/Donor/Donor.tsx b/context/app/static/js/pages/Donor/Donor.tsx
index 6a0907ea2d..abd830a10b 100644
--- a/context/app/static/js/pages/Donor/Donor.tsx
+++ b/context/app/static/js/pages/Donor/Donor.tsx
@@ -39,10 +39,7 @@ function DonorDetail() {
- }
- shouldDisplay={shouldDisplaySection.metadata}
- />
+
diff --git a/context/app/static/js/pages/Sample/Sample.jsx b/context/app/static/js/pages/Sample/Sample.jsx
index 914ba98f94..17926c1dc5 100644
--- a/context/app/static/js/pages/Sample/Sample.jsx
+++ b/context/app/static/js/pages/Sample/Sample.jsx
@@ -11,41 +11,43 @@ import SummaryItem from 'js/components/detailPage/summary/SummaryItem';
import DetailLayout from 'js/components/detailPage/DetailLayout';
import SampleTissue from 'js/components/detailPage/SampleTissue';
import { DetailContext } from 'js/components/detailPage/DetailContext';
+import { hasMetadata } from 'js/helpers/metadata';
import DerivedDatasetsSection from 'js/components/detailPage/derivedEntities/DerivedDatasetsSection';
-import { combineMetadata } from 'js/pages/utils/entity-utils';
import useTrackID from 'js/hooks/useTrackID';
import MetadataSection from 'js/components/detailPage/MetadataSection';
+import { useEntitiesData } from 'js/hooks/useEntityData';
function SampleDetail() {
+ const { entity } = useFlaskDataContext();
const {
- entity: {
- uuid,
- donor,
- protocol_url,
- sample_category,
- origin_samples,
- hubmap_id,
- entity_type,
- metadata,
- descendant_counts,
- },
- } = useFlaskDataContext();
+ uuid,
+ protocol_url,
+ sample_category,
+ origin_samples,
+ hubmap_id,
+ entity_type,
+ descendant_counts,
+ ancestor_ids,
+ } = entity;
+
+ const [entities, loadingEntities] = useEntitiesData([uuid, ...ancestor_ids]);
+ const entitiesWithMetadata = entities.filter((e) =>
+ hasMetadata({ targetEntityType: e.entity_type, currentEntity: e }),
+ );
// TODO: Update design to reflect samples and datasets which have multiple origin samples with different organs.
const origin_sample = origin_samples[0];
const { mapped_organ } = origin_sample;
- const combinedMetadata = combineMetadata(donor, undefined, metadata);
-
const shouldDisplaySection = {
summary: true,
'derived-data': Boolean(descendant_counts?.entity_type?.Dataset > 0),
tissue: true,
provenance: true,
protocols: Boolean(protocol_url),
- metadata: Boolean(Object.keys(combinedMetadata).length),
+ metadata: Boolean(entitiesWithMetadata.length),
attribution: true,
};
@@ -53,6 +55,10 @@ function SampleDetail() {
const detailContext = useMemo(() => ({ hubmap_id, uuid }), [hubmap_id, uuid]);
+ if (loadingEntities) {
+ return null;
+ }
+
return (
@@ -70,7 +76,7 @@ function SampleDetail() {
{shouldDisplaySection.protocols && }
-
+
diff --git a/context/app/static/js/pages/utils/entity-utils.spec.ts b/context/app/static/js/pages/utils/entity-utils.spec.ts
deleted file mode 100644
index b51eb42418..0000000000
--- a/context/app/static/js/pages/utils/entity-utils.spec.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-import { Donor, Sample } from 'js/components/types';
-import { combineMetadata } from './entity-utils';
-
-test('robust against undefined data', () => {
- const donor = undefined;
- const source_samples = undefined;
- const metadata = undefined;
- // @ts-expect-error - This is a test case for bad data.
- expect(combineMetadata(donor, source_samples, metadata)).toEqual({});
-});
-
-test('robust against empty objects', () => {
- const donor = {};
- const source_samples: Sample[] = [];
- const metadata = {};
- // @ts-expect-error - This is a test case for bad data.
- expect(combineMetadata(donor, source_samples, metadata)).toEqual({});
-});
-
-test('combines appropriately structured metadata', () => {
- // This information is also available in the "ancestors" list,
- // but metadata is structured differently between Samples and Donors,
- // so it wouldn't simplify things to use that.
- const donor = {
- created_by_user_displayname: 'John Doe',
- mapped_metadata: {
- age_unit: ['years'],
- age_value: [40],
- },
- metadata: {
- // This is the source of the mapped_metadata.
- living_donor_data: [],
- },
- } as unknown as Donor;
- const source_samples = [
- {
- // mapped_metadata seems to be empty.
- mapped_metadata: {},
- metadata: {
- cold_ischemia_time_unit: 'minutes',
- cold_ischemia_time_value: '100',
- },
- },
- ] as unknown as Sample[];
- const metadata = {
- dag_provenance_list: [],
- extra_metadata: {},
- metadata: {
- analyte_class: 'polysaccharides',
- assay_category: 'imaging',
- assay_type: 'PAS microscopy',
- },
- };
- expect(combineMetadata(donor, source_samples, metadata)).toEqual({
- analyte_class: 'polysaccharides',
- assay_category: 'imaging',
- assay_type: 'PAS microscopy',
- 'donor.age_unit': ['years'],
- 'donor.age_value': [40],
- 'sample.cold_ischemia_time_unit': 'minutes',
- 'sample.cold_ischemia_time_value': '100',
- });
-});
diff --git a/context/app/static/js/pages/utils/entity-utils.ts b/context/app/static/js/pages/utils/entity-utils.ts
deleted file mode 100644
index 1161cfa2af..0000000000
--- a/context/app/static/js/pages/utils/entity-utils.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import { Donor, Sample } from 'js/components/types';
-
-function addPrefix(prefix: string, object: object) {
- return Object.fromEntries(Object.entries(object).map(([key, value]) => [prefix + key, value]));
-}
-
-function prefixDonorMetadata(donor: Donor | null | undefined) {
- const donorMetadata = donor?.mapped_metadata ?? {};
- return addPrefix('donor.', donorMetadata);
-}
-
-function prefixSampleMetadata(source_samples: Sample[] | null | undefined) {
- const sampleMetadatas = (source_samples ?? []).filter((sample) => sample?.metadata).map((sample) => sample.metadata);
- return sampleMetadatas.map((sampleMetadata) => addPrefix('sample.', sampleMetadata));
-}
-
-function combineMetadata(
- donor: Donor,
- source_samples: Sample[] = [],
- metadata: Record | null | undefined = {},
-) {
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
- const combinedMetadata: Record = {
- ...(metadata?.metadata ?? {}),
- ...prefixDonorMetadata(donor),
- };
- prefixSampleMetadata(source_samples).forEach((sampleMetadata) => {
- Object.assign(combinedMetadata, sampleMetadata);
- });
-
- return combinedMetadata;
-}
-export { combineMetadata };
diff --git a/context/app/static/js/theme/theme.tsx b/context/app/static/js/theme/theme.tsx
index f79df3dfc4..eee6ac1c56 100644
--- a/context/app/static/js/theme/theme.tsx
+++ b/context/app/static/js/theme/theme.tsx
@@ -42,7 +42,7 @@ declare module '@mui/material/styles' {
export interface ZIndex {
tutorial: number;
dropdownOffset: number;
- entityHeader: number;
+ header: number;
dropdown: number;
visualization: number;
fileBrowserHeader: number;
@@ -264,7 +264,7 @@ const theme = createTheme({
zIndex: {
tutorial: 1101, // one higher than AppBar zIndex provided by MUI
dropdownOffset: 1001,
- entityHeader: 1000,
+ header: 1000,
dropdown: 50,
visualization: 3,
fileBrowserHeader: 1,
diff --git a/etc/build/push.sh b/etc/build/push.sh
index 29e86a25b5..27e56bc8fd 100755
--- a/etc/build/push.sh
+++ b/etc/build/push.sh
@@ -44,6 +44,7 @@ else
VERSION=`cd context && npm version major`
fi
+
echo "Version: $VERSION"
./grab-dependencies.sh
@@ -54,13 +55,16 @@ git commit -m "Version bump to $VERSION"
if ls CHANGELOG-*.md; then
(
+ echo '# Changelog'
+ echo
echo '##' $VERSION - `date +"%F"`
echo
# "-l" chomps and adds newline.
perl -lpe '' CHANGELOG-*.md
echo
echo
- cat CHANGELOG.md
+ # Skip the first line of the existing changelog (the header).
+ tail -n +2 context/app/markdown/CHANGELOG.md
) > CHANGELOG.md.new
mv CHANGELOG.md.new context/app/markdown/CHANGELOG.md
git rm CHANGELOG-*.md