diff --git a/__tests__/viewModelBuilders/dataset-export.test.ts b/__tests__/viewModelBuilders/dataset-export.test.ts new file mode 100644 index 000000000..0c268ec37 --- /dev/null +++ b/__tests__/viewModelBuilders/dataset-export.test.ts @@ -0,0 +1,396 @@ +import { Breadcrumb } from "@databiosphere/findable-ui/lib/components/common/Breadcrumbs/breadcrumbs"; +import { ViewContext } from "@databiosphere/findable-ui/lib/config/entities"; +import { FileFacet } from "@databiosphere/findable-ui/lib/hooks/useFileManifest/common/entities"; +import { FileManifestState } from "@databiosphere/findable-ui/lib/providers/fileManifestState"; +import { + buildDatasetExportToPlatform, + buildDatasetExportToPlatformHero, + buildDatasetExportToPlatformMethod, +} from "../../app/viewModelBuilders/azul/anvil-cmg/common/viewModelBuilders"; +import { DatasetsResponse } from "../../app/apis/azul/anvil-cmg/common/responses"; +import { ANVIL_CMG_CATEGORY_KEY } from "../../site-config/anvil-cmg/category"; +import { + EXPORTS, + EXPORT_METHODS, +} from "../../site-config/anvil-cmg/dev/export/constants"; + +/** + * Creates a mock FileManifestState for testing. + * @param overrides - Properties to override in the mock. + * @returns Mock FileManifestState. + */ +const createMockFileManifestState = ( + overrides: Partial = {} +): FileManifestState => ({ + fileCount: undefined, + fileSummary: undefined, + fileSummaryFacetName: undefined, + fileSummaryFilters: [], + filesFacets: [], + filters: [], + isEnabled: true, + isFacetsLoading: false, + isFacetsSuccess: true, + isFileSummaryLoading: false, + isLoading: false, + isSummaryLoading: false, + summary: undefined, + ...overrides, +}); + +/** + * Creates a mock FileFacet for testing. + * @param overrides - Properties to override in the mock. + * @returns Mock FileFacet. + */ +const createMockFileFacet = ( + overrides: Partial = {} +): FileFacet => ({ + name: "files.file_format", + selected: false, + selectedTermCount: 0, + selectedTerms: [], + termCount: 5, + terms: [{ count: 10, name: "bam", selected: false }], + termsByName: new Map(), + total: 10, + ...overrides, +}); + +/** + * Creates a mock DatasetsResponse for testing. + * @param overrides - Properties to override in the mock. + * @returns Mock DatasetsResponse. + */ +const createMockDatasetsResponse = ( + overrides: Partial = {} +): DatasetsResponse => + ({ + datasets: [ + { + accessible: true, + dataset_id: "test-dataset-id", + title: "Test Dataset Title", + }, + ], + donors: [{ organism_type: ["Homo sapiens"] }], + entryId: "test-dataset-id", + files: [{ file_format: ["bam", "fastq"] }], + ...overrides, + }) as DatasetsResponse; + +/** + * Creates a mock ViewContext for testing. + * @param overrides - Properties to override in the mock. + * @returns Mock ViewContext. + */ +const createMockViewContext = ( + overrides: Partial> = {} +): ViewContext => + ({ + cellContext: undefined, + entityConfig: undefined, + exploreState: { + categoryViews: [], + filterState: [{ categoryKey: "test.filter", value: ["value1"] }], + tabValue: "datasets", + }, + fileManifestState: createMockFileManifestState(), + ...overrides, + }) as ViewContext; + +describe("buildDatasetExportToPlatform", () => { + describe("returns correct props structure", () => { + it("spreads provided props into result", () => { + const props = EXPORTS.BIO_DATA_CATALYST; + const builder = buildDatasetExportToPlatform(props); + const response = createMockDatasetsResponse(); + const viewContext = createMockViewContext(); + + const result = builder(response, viewContext); + + expect(result.buttonLabel).toBe(props.buttonLabel); + expect(result.description).toBe(props.description); + expect(result.successTitle).toBe(props.successTitle); + expect(result.title).toBe(props.title); + }); + + it("includes fileManifestState from viewContext", () => { + const props = EXPORTS.CAVATICA; + const builder = buildDatasetExportToPlatform(props); + const fileManifestState = createMockFileManifestState({ + isFacetsSuccess: true, + }); + const response = createMockDatasetsResponse(); + const viewContext = createMockViewContext({ fileManifestState }); + + const result = builder(response, viewContext); + + expect(result.fileManifestState).toBe(fileManifestState); + }); + + it("includes correct fileSummaryFacetName", () => { + const props = EXPORTS.CANCER_GENOMICS_CLOUD; + const builder = buildDatasetExportToPlatform(props); + const response = createMockDatasetsResponse(); + const viewContext = createMockViewContext(); + + const result = builder(response, viewContext); + + expect(result.fileSummaryFacetName).toBe( + ANVIL_CMG_CATEGORY_KEY.FILE_FILE_FORMAT + ); + }); + + it("includes correct speciesFacetName", () => { + const props = EXPORTS.BIO_DATA_CATALYST; + const builder = buildDatasetExportToPlatform(props); + const response = createMockDatasetsResponse(); + const viewContext = createMockViewContext(); + + const result = builder(response, viewContext); + + expect(result.speciesFacetName).toBe( + ANVIL_CMG_CATEGORY_KEY.DONOR_ORGANISM_TYPE + ); + }); + + it("includes filters derived from response", () => { + const props = EXPORTS.CAVATICA; + const builder = buildDatasetExportToPlatform(props); + const response = createMockDatasetsResponse(); + const viewContext = createMockViewContext(); + + const result = builder(response, viewContext); + + expect(result.filters).toBeDefined(); + expect(Array.isArray(result.filters)).toBe(true); + }); + }); + + describe("works correctly for each platform", () => { + it("returns correct props for BioData Catalyst config", () => { + const builder = buildDatasetExportToPlatform(EXPORTS.BIO_DATA_CATALYST); + const response = createMockDatasetsResponse(); + const viewContext = createMockViewContext(); + + const result = builder(response, viewContext); + + expect(result.buttonLabel).toBe("Open BioData Catalyst"); + expect(result.title).toBe("Analyze in BioData Catalyst"); + }); + + it("returns correct props for CAVATICA config", () => { + const builder = buildDatasetExportToPlatform(EXPORTS.CAVATICA); + const response = createMockDatasetsResponse(); + const viewContext = createMockViewContext(); + + const result = builder(response, viewContext); + + expect(result.buttonLabel).toBe("Open CAVATICA"); + expect(result.title).toBe("Analyze in CAVATICA"); + }); + + it("returns correct props for Cancer Genomics Cloud config", () => { + const builder = buildDatasetExportToPlatform( + EXPORTS.CANCER_GENOMICS_CLOUD + ); + const response = createMockDatasetsResponse(); + const viewContext = createMockViewContext(); + + const result = builder(response, viewContext); + + expect(result.buttonLabel).toBe("Open Cancer Genomics Cloud"); + expect(result.title).toBe("Analyze in Cancer Genomics Cloud"); + }); + }); +}); + +describe("buildDatasetExportToPlatformHero", () => { + it("returns provided title as dataset title", () => { + const title = "Analyze in BioData Catalyst"; + const builder = buildDatasetExportToPlatformHero(title); + const response = createMockDatasetsResponse(); + const viewContext = createMockViewContext(); + + const result = builder(response, viewContext); + + expect(result.title).toBe("Test Dataset Title"); + }); + + it("builds correct breadcrumb structure", () => { + const title = "Analyze in CAVATICA"; + const builder = buildDatasetExportToPlatformHero(title); + const response = createMockDatasetsResponse(); + const viewContext = createMockViewContext(); + + const result = builder(response, viewContext); + const breadcrumbs = result.breadcrumbs as Breadcrumb[]; + + expect(result.breadcrumbs).toBeDefined(); + expect(Array.isArray(result.breadcrumbs)).toBe(true); + expect(breadcrumbs.length).toBe(4); + + // First breadcrumb should link to datasets list + expect(breadcrumbs[0].text).toBe("Datasets"); + expect(breadcrumbs[0].path).toBe("/datasets"); + + // Second breadcrumb should link to the dataset + expect(breadcrumbs[1].text).toBe("Test Dataset Title"); + expect(breadcrumbs[1].path).toBe("/datasets/test-dataset-id"); + + // Third breadcrumb should link to export choice + expect(breadcrumbs[2].text).toBe("Choose Export Method"); + expect(breadcrumbs[2].path).toBe("/datasets/test-dataset-id/export"); + + // Fourth breadcrumb should be the current page title + expect(breadcrumbs[3].text).toBe(title); + }); + + it("uses dataset ID from response for paths", () => { + const title = "Export to Cancer Genomics Cloud"; + const builder = buildDatasetExportToPlatformHero(title); + const response = createMockDatasetsResponse({ + datasets: [ + { + accessible: true, + dataset_id: "custom-dataset-123", + title: "Custom Dataset", + }, + ], + entryId: "custom-dataset-123", + } as Partial); + const viewContext = createMockViewContext(); + + const result = builder(response, viewContext); + const breadcrumbs = result.breadcrumbs as Breadcrumb[]; + + expect(breadcrumbs[1].path).toBe("/datasets/custom-dataset-123"); + expect(breadcrumbs[2].path).toBe("/datasets/custom-dataset-123/export"); + }); +}); + +describe("buildDatasetExportToPlatformMethod", () => { + it("returns route with dataset path prefix", () => { + const props = EXPORT_METHODS.BIO_DATA_CATALYST; + const builder = buildDatasetExportToPlatformMethod(props); + const response = createMockDatasetsResponse(); + const viewContext = createMockViewContext(); + + const result = builder(response, viewContext); + + expect(result.route).toBe( + "/datasets/test-dataset-id/export/biodata-catalyst" + ); + }); + + it("returns buttonLabel from props", () => { + const props = EXPORT_METHODS.CAVATICA; + const builder = buildDatasetExportToPlatformMethod(props); + const response = createMockDatasetsResponse(); + const viewContext = createMockViewContext(); + + const result = builder(response, viewContext); + + expect(result.buttonLabel).toBe(props.buttonLabel); + }); + + it("returns description from props", () => { + const props = EXPORT_METHODS.CANCER_GENOMICS_CLOUD; + const builder = buildDatasetExportToPlatformMethod(props); + const response = createMockDatasetsResponse(); + const viewContext = createMockViewContext(); + + const result = builder(response, viewContext); + + expect(result.description).toBe(props.description); + }); + + it("returns title from props", () => { + const props = EXPORT_METHODS.BIO_DATA_CATALYST; + const builder = buildDatasetExportToPlatformMethod(props); + const response = createMockDatasetsResponse(); + const viewContext = createMockViewContext(); + + const result = builder(response, viewContext); + + expect(result.title).toBe(props.title); + }); + + describe("accessibility", () => { + it("returns isAccessible true when facets loaded and files available", () => { + const props = EXPORT_METHODS.CAVATICA; + const builder = buildDatasetExportToPlatformMethod(props); + const response = createMockDatasetsResponse(); + const fileManifestState = createMockFileManifestState({ + filesFacets: [createMockFileFacet()], + isFacetsSuccess: true, + summary: { fileCount: 10 }, + }); + const viewContext = createMockViewContext({ fileManifestState }); + + const result = builder(response, viewContext); + + expect(result.isAccessible).toBe(true); + }); + + it("returns isAccessible false when facets not yet loaded", () => { + const props = EXPORT_METHODS.CANCER_GENOMICS_CLOUD; + const builder = buildDatasetExportToPlatformMethod(props); + const response = createMockDatasetsResponse(); + const fileManifestState = createMockFileManifestState({ + isFacetsSuccess: false, + }); + const viewContext = createMockViewContext({ fileManifestState }); + + const result = builder(response, viewContext); + + expect(result.isAccessible).toBe(false); + }); + }); + + describe("works correctly for each platform method", () => { + it("returns correct props for BioData Catalyst method", () => { + const builder = buildDatasetExportToPlatformMethod( + EXPORT_METHODS.BIO_DATA_CATALYST + ); + const response = createMockDatasetsResponse(); + const viewContext = createMockViewContext(); + + const result = builder(response, viewContext); + + expect(result.buttonLabel).toBe("Analyze in NHLBI BioData Catalyst"); + expect(result.route).toBe( + "/datasets/test-dataset-id/export/biodata-catalyst" + ); + }); + + it("returns correct props for CAVATICA method", () => { + const builder = buildDatasetExportToPlatformMethod( + EXPORT_METHODS.CAVATICA + ); + const response = createMockDatasetsResponse(); + const viewContext = createMockViewContext(); + + const result = builder(response, viewContext); + + expect(result.buttonLabel).toBe("Analyze in CAVATICA"); + expect(result.route).toBe("/datasets/test-dataset-id/export/cavatica"); + }); + + it("returns correct props for Cancer Genomics Cloud method", () => { + const builder = buildDatasetExportToPlatformMethod( + EXPORT_METHODS.CANCER_GENOMICS_CLOUD + ); + const response = createMockDatasetsResponse(); + const viewContext = createMockViewContext(); + + const result = builder(response, viewContext); + + expect(result.buttonLabel).toBe("Analyze in Cancer Genomics Cloud"); + expect(result.route).toBe( + "/datasets/test-dataset-id/export/cancer-genomics-cloud" + ); + }); + }); +}); diff --git a/app/viewModelBuilders/azul/anvil-cmg/common/viewModelBuilders.ts b/app/viewModelBuilders/azul/anvil-cmg/common/viewModelBuilders.ts index 241409f50..586cf3948 100644 --- a/app/viewModelBuilders/azul/anvil-cmg/common/viewModelBuilders.ts +++ b/app/viewModelBuilders/azul/anvil-cmg/common/viewModelBuilders.ts @@ -488,6 +488,75 @@ export const buildDatasetExportMethodTerra = ( }; }; +/** + * Build props for the dataset ExportToPlatform component. + * @param props - Props to pass to the ExportToPlatform component. + * @returns model to be used as props for the ExportToPlatform component. + */ +export const buildDatasetExportToPlatform = ( + props: Pick< + ComponentProps, + "buttonLabel" | "description" | "successTitle" | "title" + > +): (( + response: DatasetsResponse, + viewContext: ViewContext +) => ComponentProps) => { + return (response: DatasetsResponse, viewContext: ViewContext) => { + const { fileManifestState } = viewContext; + return { + ...props, + fileManifestState, + fileSummaryFacetName: ANVIL_CMG_CATEGORY_KEY.FILE_FILE_FORMAT, + filters: getExportTerraEntityFilters(response), + formFacet: getFormFacets(fileManifestState), + speciesFacetName: ANVIL_CMG_CATEGORY_KEY.DONOR_ORGANISM_TYPE, + }; + }; +}; + +/** + * Build props for dataset ExportToPlatform BackPageHero component. + * @param title - Title of the export method. + * @returns model to be used as props for the BackPageHero component. + */ +export const buildDatasetExportToPlatformHero = ( + title: string +): (( + response: DatasetsResponse, + viewContext: ViewContext +) => React.ComponentProps) => { + return (response: DatasetsResponse) => { + return getDatasetExportMethodHero(response, title); + }; +}; + +/** + * Build props for dataset ExportMethod component for display of the export to [platform] metadata section. + * @param props - Props to pass to the ExportMethod component. + * @param props.route - Route to the export method. + * @returns model to be used as props for the dataset ExportMethod component. + */ +export const buildDatasetExportToPlatformMethod = ({ + route, + ...props +}: Pick< + ComponentProps, + "buttonLabel" | "description" | "route" | "title" +>): (( + response: DatasetsResponse, + viewContext: ViewContext +) => ComponentProps) => { + return (response: DatasetsResponse, viewContext: ViewContext) => { + const datasetPath = buildDatasetPath(response); + return { + ...props, + ...getExportMethodAccessibility(viewContext), + route: `${datasetPath}${route}`, + }; + }; +}; + /** * Build props for BackPageHero component from the given datasets response. * @param datasetsResponse - Response model return from datasets API. diff --git a/pages/[entityListType]/[...params].tsx b/pages/[entityListType]/[...params].tsx index 03e09b185..c8680c4da 100644 --- a/pages/[entityListType]/[...params].tsx +++ b/pages/[entityListType]/[...params].tsx @@ -29,9 +29,19 @@ import { ParsedUrlQuery } from "querystring"; import { EntityGuard } from "../../app/components/Detail/components/EntityGuard/entityGuard"; import { readFile } from "../../app/utils/tsvParser"; import { useRouter } from "next/router"; +import { useFeatureFlag } from "@databiosphere/findable-ui/lib/hooks/useFeatureFlag/useFeatureFlag"; +import { FEATURES } from "../../app/shared/entities"; +import NextError from "next/error"; +import { ROUTES } from "../../site-config/anvil-cmg/dev/export/routes"; const setOfProcessedIds = new Set(); +const NCPI_EXPORT_PATHS = [ + ROUTES.BIO_DATA_CATALYST, + ROUTES.CANCER_GENOMICS_CLOUD, + ROUTES.CAVATICA, +]; + interface StaticPath { params: PageUrl; } @@ -53,9 +63,12 @@ export interface EntityDetailPageProps extends AzulEntityStaticResponse { * @returns Entity detail view component. */ const EntityDetailPage = (props: EntityDetailPageProps): JSX.Element => { + const isNCPIExportEnabled = useFeatureFlag(FEATURES.NCPI_EXPORT); const { query } = useRouter(); if (!props.entityListType) return <>; if (props.override) return ; + if (!isNCPIExportEnabled && isNCPIExportRoute(query)) + return ; if (isChooseExportView(query)) return ; if (isExportMethodView(query)) return ; return ; @@ -77,6 +90,19 @@ function findOverride( return overrides.find(({ entryId }) => entryId === entityId); } +/** + * Returns true if the current route is an NCPI export route. + * @param query - Parsed URL query. + * @returns True if the route matches an NCPI export path. + */ +function isNCPIExportRoute(query: ParsedUrlQuery): boolean { + const params = query.params as string[] | undefined; + const lastParam = params?.[params.length - 1] || ""; + return NCPI_EXPORT_PATHS.map((path) => path.replace("/export/", "")).includes( + lastParam + ); +} + /** * Returns true if the entity is a special case e.g. an "override". * @param override - Override. diff --git a/site-config/anvil-cmg/dev/detail/dataset/export/export.ts b/site-config/anvil-cmg/dev/detail/dataset/export/export.ts index 98729284b..d0ee2b45b 100644 --- a/site-config/anvil-cmg/dev/detail/dataset/export/export.ts +++ b/site-config/anvil-cmg/dev/detail/dataset/export/export.ts @@ -8,6 +8,9 @@ import * as C from "../../../../../../app/components"; import { DatasetsResponse } from "app/apis/azul/anvil-cmg/common/responses"; import { ROUTES } from "../../../export/routes"; import * as MDX from "../../../../../../app/components/common/MDXContent/anvil-cmg"; +import { ExportMethod } from "../../../../../../app/components/Export/components/AnVILExplorer/platform/ExportMethod/exportMethod"; +import { EXPORT_METHODS, EXPORTS } from "../../../export/constants"; +import { ExportToPlatform } from "../../../../../../app/components/Export/components/AnVILExplorer/platform/ExportToPlatform/exportToPlatform"; /** * Badge indicating dataset accessibility. @@ -72,6 +75,60 @@ export const exportConfig: ExportConfig = { } as ComponentConfig, ], }, + { + mainColumn: [ + /* --------- */ + /* Dataset is not accessible; render warning */ + /* --------- */ + { + children: [ + { + children: [ + { + component: MDX.Alert, + viewBuilder: V.buildAlertDatasetExportWarning, + } as ComponentConfig, + ], + component: C.BackPageContentSingleColumn, + } as ComponentConfig, + ], + component: C.ConditionalComponent, + viewBuilder: V.renderDatasetExportWarning, + } as ComponentConfig, + /* ------ */ + /* Dataset is accessible; render BioData Catalyst export method */ + /* ------ */ + { + children: [ + { + children: [ + { + component: ExportToPlatform, + viewBuilder: V.buildDatasetExportToPlatform( + EXPORTS.BIO_DATA_CATALYST + ), + } as ComponentConfig, + ], + component: C.BackPageContentMainColumn, + } as ComponentConfig, + /* sideColumn */ + ...exportSideColumn, + ], + component: C.ConditionalComponent, + viewBuilder: V.renderDatasetExport, + } as ComponentConfig, + ], + route: ROUTES.BIO_DATA_CATALYST, + top: [ + { + children: [DATASET_ACCESSIBILITY_BADGE], + component: C.BackPageHero, + viewBuilder: V.buildDatasetExportToPlatformHero( + EXPORTS.BIO_DATA_CATALYST.title + ), + } as ComponentConfig, + ], + }, { mainColumn: [ /* --------- */ @@ -165,6 +222,12 @@ export const exportConfig: ExportConfig = { component: C.ExportMethod, viewBuilder: V.buildDatasetExportMethodTerra, } as ComponentConfig, + { + component: ExportMethod, + viewBuilder: V.buildDatasetExportToPlatformMethod( + EXPORT_METHODS.BIO_DATA_CATALYST + ), + } as ComponentConfig, { component: C.ExportMethod, viewBuilder: V.buildDatasetExportMethodManifestDownload,