From b724a5015f9dfb61dd89e4697188ff11eecaa06c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Mon, 25 Aug 2025 11:57:13 -0300 Subject: [PATCH 01/51] feat: use cases, repositories, etc --- package-lock.json | 8 ++--- package.json | 2 +- .../domain/models/DatasetExternalToolUrl.ts | 6 ++++ .../domain/models/ExternalTool.ts | 19 ++++++++++ .../domain/models/FileExternalToolUrl.ts | 6 ++++ .../repositories/ExternalToolsRepository.ts | 18 ++++++++++ .../useCases/DTOs/GetExternalToolUrlDTO.ts | 9 +++++ .../useCases/GetDatasetExternalToolUrl.ts | 12 +++++++ .../domain/useCases/GetExternalTools.ts | 8 +++++ .../domain/useCases/GetFileExternalToolUrl.ts | 12 +++++++ .../ExternalToolsJSDataverseRepository.ts | 36 +++++++++++++++++++ 11 files changed, 131 insertions(+), 5 deletions(-) create mode 100644 src/externalTools/domain/models/DatasetExternalToolUrl.ts create mode 100644 src/externalTools/domain/models/ExternalTool.ts create mode 100644 src/externalTools/domain/models/FileExternalToolUrl.ts create mode 100644 src/externalTools/domain/repositories/ExternalToolsRepository.ts create mode 100644 src/externalTools/domain/useCases/DTOs/GetExternalToolUrlDTO.ts create mode 100644 src/externalTools/domain/useCases/GetDatasetExternalToolUrl.ts create mode 100644 src/externalTools/domain/useCases/GetExternalTools.ts create mode 100644 src/externalTools/domain/useCases/GetFileExternalToolUrl.ts create mode 100644 src/externalTools/infrastructure/repositories/ExternalToolsJSDataverseRepository.ts diff --git a/package-lock.json b/package-lock.json index 007c3c4ac..345f9fbf9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@dnd-kit/sortable": "8.0.0", "@dnd-kit/utilities": "3.2.2", "@faker-js/faker": "7.6.0", - "@iqss/dataverse-client-javascript": "2.0.0-alpha.61", + "@iqss/dataverse-client-javascript": "2.0.0-pr353.fc750c9", "@iqss/dataverse-design-system": "*", "@istanbuljs/nyc-config-typescript": "1.0.2", "@tanstack/react-table": "8.9.2", @@ -3561,9 +3561,9 @@ }, "node_modules/@iqss/dataverse-client-javascript": { "name": "@IQSS/dataverse-client-javascript", - "version": "2.0.0-alpha.61", - "resolved": "https://npm.pkg.github.com/download/@IQSS/dataverse-client-javascript/2.0.0-alpha.61/1be6ec895a4d6dbc875b4a10727c4741c852e6b0", - "integrity": "sha512-225/ihgNRBxcivu8tYfbB+21QT5eSEYDqKSLrUQh61aZa74w407axjsWp1sVEPZcLqZpr+CgI9JppoGTeS4qnA==", + "version": "2.0.0-pr353.fc750c9", + "resolved": "https://npm.pkg.github.com/download/@IQSS/dataverse-client-javascript/2.0.0-pr353.fc750c9/3547ae61dcaa0e27c4ad3c03b23a9c3720543488", + "integrity": "sha512-ARsTrMRclcKYAU3mWJleoi+KPsg2UdjyI7RFgTXSnTdRu+y7VjwW5Tjb1tEoui41PvHg5m6dseshSSEZj/2QHA==", "license": "MIT", "dependencies": { "@types/node": "^18.15.11", diff --git a/package.json b/package.json index abf8590f5..d14fd9fe5 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "@dnd-kit/sortable": "8.0.0", "@dnd-kit/utilities": "3.2.2", "@faker-js/faker": "7.6.0", - "@iqss/dataverse-client-javascript": "2.0.0-alpha.61", + "@iqss/dataverse-client-javascript": "2.0.0-pr353.fc750c9", "@iqss/dataverse-design-system": "*", "@istanbuljs/nyc-config-typescript": "1.0.2", "@tanstack/react-table": "8.9.2", diff --git a/src/externalTools/domain/models/DatasetExternalToolUrl.ts b/src/externalTools/domain/models/DatasetExternalToolUrl.ts new file mode 100644 index 000000000..a7d1a6039 --- /dev/null +++ b/src/externalTools/domain/models/DatasetExternalToolUrl.ts @@ -0,0 +1,6 @@ +export interface DatasetExternalToolUrl { + toolUrlResolved: string + displayName: string + datasetId: number + preview: boolean +} diff --git a/src/externalTools/domain/models/ExternalTool.ts b/src/externalTools/domain/models/ExternalTool.ts new file mode 100644 index 000000000..beb9f4b6e --- /dev/null +++ b/src/externalTools/domain/models/ExternalTool.ts @@ -0,0 +1,19 @@ +export interface ExternalTool { + id: number + displayName: string + description: string + types: ToolType[] + scope: ToolScope +} + +export enum ToolType { + Explore = 'explore', + Configure = 'configure', + Preview = 'preview', + Query = 'query' +} + +export enum ToolScope { + Dataset = 'dataset', + File = 'file' +} diff --git a/src/externalTools/domain/models/FileExternalToolUrl.ts b/src/externalTools/domain/models/FileExternalToolUrl.ts new file mode 100644 index 000000000..bcdd64859 --- /dev/null +++ b/src/externalTools/domain/models/FileExternalToolUrl.ts @@ -0,0 +1,6 @@ +export interface FileExternalToolUrl { + toolUrlResolved: string + displayName: string + fileId: number + preview: boolean +} diff --git a/src/externalTools/domain/repositories/ExternalToolsRepository.ts b/src/externalTools/domain/repositories/ExternalToolsRepository.ts new file mode 100644 index 000000000..ed6eab542 --- /dev/null +++ b/src/externalTools/domain/repositories/ExternalToolsRepository.ts @@ -0,0 +1,18 @@ +import { DatasetExternalToolUrl } from '../models/DatasetExternalToolUrl' +import { ExternalTool } from '../models/ExternalTool' +import { FileExternalToolUrl } from '../models/FileExternalToolUrl' +import { GetExternalToolDTO } from '../useCases/DTOs/GetExternalToolUrlDTO' + +export interface ExternalToolsRepository { + getExternalTools(): Promise + getDatasetExternalToolUrl( + datasetId: number | string, + toolId: number, + getExternalToolDTO: GetExternalToolDTO + ): Promise + getFileExternalToolUrl( + fileId: number | string, + toolId: number, + getExternalToolDTO: GetExternalToolDTO + ): Promise +} diff --git a/src/externalTools/domain/useCases/DTOs/GetExternalToolUrlDTO.ts b/src/externalTools/domain/useCases/DTOs/GetExternalToolUrlDTO.ts new file mode 100644 index 000000000..7e415d253 --- /dev/null +++ b/src/externalTools/domain/useCases/DTOs/GetExternalToolUrlDTO.ts @@ -0,0 +1,9 @@ +/** + * @property {boolean} preview - boolean flag to indicate if the request is for previewing the tool or not. + * @property {string} locale - string specifying the locale for internationalization + */ + +export interface GetExternalToolDTO { + preview: boolean + locale: string +} diff --git a/src/externalTools/domain/useCases/GetDatasetExternalToolUrl.ts b/src/externalTools/domain/useCases/GetDatasetExternalToolUrl.ts new file mode 100644 index 000000000..f40fbb71a --- /dev/null +++ b/src/externalTools/domain/useCases/GetDatasetExternalToolUrl.ts @@ -0,0 +1,12 @@ +import { DatasetExternalToolUrl } from '../models/DatasetExternalToolUrl' +import { ExternalToolsRepository } from '../repositories/ExternalToolsRepository' +import { GetExternalToolDTO } from './DTOs/GetExternalToolUrlDTO' + +export function getDatasetExternalToolUrl( + externalToolsRepository: ExternalToolsRepository, + datasetId: number | string, + toolId: number, + getExternalToolDTO: GetExternalToolDTO +): Promise { + return externalToolsRepository.getDatasetExternalToolUrl(datasetId, toolId, getExternalToolDTO) +} diff --git a/src/externalTools/domain/useCases/GetExternalTools.ts b/src/externalTools/domain/useCases/GetExternalTools.ts new file mode 100644 index 000000000..889e2a165 --- /dev/null +++ b/src/externalTools/domain/useCases/GetExternalTools.ts @@ -0,0 +1,8 @@ +import { ExternalTool } from '../models/ExternalTool' +import { ExternalToolsRepository } from '../repositories/ExternalToolsRepository' + +export function getExternalTools( + externalToolsRepository: ExternalToolsRepository +): Promise { + return externalToolsRepository.getExternalTools() +} diff --git a/src/externalTools/domain/useCases/GetFileExternalToolUrl.ts b/src/externalTools/domain/useCases/GetFileExternalToolUrl.ts new file mode 100644 index 000000000..d896d6e96 --- /dev/null +++ b/src/externalTools/domain/useCases/GetFileExternalToolUrl.ts @@ -0,0 +1,12 @@ +import { FileExternalToolUrl } from '../models/FileExternalToolUrl' +import { ExternalToolsRepository } from '../repositories/ExternalToolsRepository' +import { GetExternalToolDTO } from './DTOs/GetExternalToolUrlDTO' + +export function getFileExternalToolUrl( + externalToolsRepository: ExternalToolsRepository, + fileId: number | string, + toolId: number, + getExternalToolDTO: GetExternalToolDTO +): Promise { + return externalToolsRepository.getFileExternalToolUrl(fileId, toolId, getExternalToolDTO) +} diff --git a/src/externalTools/infrastructure/repositories/ExternalToolsJSDataverseRepository.ts b/src/externalTools/infrastructure/repositories/ExternalToolsJSDataverseRepository.ts new file mode 100644 index 000000000..3e3c25ee1 --- /dev/null +++ b/src/externalTools/infrastructure/repositories/ExternalToolsJSDataverseRepository.ts @@ -0,0 +1,36 @@ +import { + getDatasetExternalToolUrl, + getExternalTools, + getFileExternalToolUrl +} from '@iqss/dataverse-client-javascript' +import { DatasetExternalToolUrl } from '@/externalTools/domain/models/DatasetExternalToolUrl' +import { ExternalTool } from '@/externalTools/domain/models/ExternalTool' +import { FileExternalToolUrl } from '@/externalTools/domain/models/FileExternalToolUrl' +import { ExternalToolsRepository } from '@/externalTools/domain/repositories/ExternalToolsRepository' +import { GetExternalToolDTO } from '@/externalTools/domain/useCases/DTOs/GetExternalToolUrlDTO' + +export class ExternalToolsJSDataverseRepository implements ExternalToolsRepository { + getExternalTools(): Promise { + return getExternalTools.execute().then((jsExternalTools) => jsExternalTools) + } + + getDatasetExternalToolUrl( + datasetId: number | string, + toolId: number, + getExternalToolDTO: GetExternalToolDTO + ): Promise { + return getDatasetExternalToolUrl + .execute(datasetId, toolId, getExternalToolDTO) + .then((jsDatasetExternalToolUrl) => jsDatasetExternalToolUrl) + } + + getFileExternalToolUrl( + fileId: number | string, + toolId: number, + getExternalToolDTO: GetExternalToolDTO + ): Promise { + return getFileExternalToolUrl + .execute(fileId, toolId, getExternalToolDTO) + .then((jsFileExternalToolUrl) => jsFileExternalToolUrl) + } +} From 92aa2dc38336daedf849773bd66f1b963504bd9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Mon, 25 Aug 2025 13:48:17 -0300 Subject: [PATCH 02/51] refactor: move LoadingContext and LoadingProvider to shared/contexts --- src/index.app.tsx | 2 +- src/sections/account/Account.tsx | 2 +- src/sections/advanced-search/AdvancedSearch.tsx | 2 +- .../collection/collection-items-panel/CollectionItemsPanel.tsx | 2 +- src/sections/create-collection/CreateCollection.tsx | 2 +- src/sections/create-dataset/CreateDataset.tsx | 2 +- src/sections/dataset/Dataset.tsx | 2 +- .../edit-collection-featured-items/EditFeaturedItems.tsx | 2 +- src/sections/edit-collection/EditCollection.tsx | 2 +- src/sections/edit-dataset-metadata/EditDatasetMetadata.tsx | 2 +- src/sections/edit-file-metadata/EditFileMetadata.tsx | 2 +- src/sections/featured-item/FeaturedItem.tsx | 2 +- src/sections/file/File.tsx | 2 +- src/sections/homepage/Homepage.tsx | 2 +- .../topbar-progress-indicator/TopbarProgressIndicator.tsx | 2 +- src/sections/not-found-page/NotFoundPage.tsx | 2 +- src/sections/replace-file/ReplaceFile.tsx | 2 +- src/sections/shared/form/DatasetMetadataForm/index.tsx | 2 +- .../form/EditCreateCollectionForm/EditCreateCollectionForm.tsx | 2 +- src/sections/sign-up/SignUp.tsx | 2 +- src/sections/upload-dataset-files/UploadDatasetFiles.tsx | 2 +- src/{sections => shared/contexts}/loading/LoadingContext.ts | 0 src/{sections => shared/contexts}/loading/LoadingProvider.tsx | 0 src/stories/WithLayout.tsx | 2 +- 24 files changed, 22 insertions(+), 22 deletions(-) rename src/{sections => shared/contexts}/loading/LoadingContext.ts (100%) rename src/{sections => shared/contexts}/loading/LoadingProvider.tsx (100%) diff --git a/src/index.app.tsx b/src/index.app.tsx index 633aca4d1..470b580d6 100644 --- a/src/index.app.tsx +++ b/src/index.app.tsx @@ -1,7 +1,7 @@ import React from 'react' import App from './App' import './i18n' -import { LoadingProvider } from './sections/loading/LoadingProvider' +import { LoadingProvider } from './shared/contexts/loading/LoadingProvider' import { ThemeProvider } from '@iqss/dataverse-design-system' import { AppLoader } from './sections/shared/layout/app-loader/AppLoader' diff --git a/src/sections/account/Account.tsx b/src/sections/account/Account.tsx index 082b5b0e6..26fc6cb79 100644 --- a/src/sections/account/Account.tsx +++ b/src/sections/account/Account.tsx @@ -7,7 +7,7 @@ import { UserJSDataverseRepository } from '@/users/infrastructure/repositories/U import { CollectionRepository } from '@/collection/domain/repositories/CollectionRepository' import { ApiTokenSection } from './api-token-section/ApiTokenSection' import { AccountInfoSection } from './account-info-section/AccountInfoSection' -import { useLoading } from '../loading/LoadingContext' +import { useLoading } from '../../shared/contexts/loading/LoadingContext' import { MyDataItemsPanel } from '@/sections/account/my-data-section/MyDataItemsPanel' import { RoleJSDataverseRepository } from '@/roles/infrastructure/repositories/RoleJSDataverseRepository' import styles from './Account.module.scss' diff --git a/src/sections/advanced-search/AdvancedSearch.tsx b/src/sections/advanced-search/AdvancedSearch.tsx index 27c35dbd1..15ab438e3 100644 --- a/src/sections/advanced-search/AdvancedSearch.tsx +++ b/src/sections/advanced-search/AdvancedSearch.tsx @@ -5,7 +5,7 @@ import { CollectionRepository } from '@/collection/domain/repositories/Collectio import { MetadataBlockInfoRepository } from '@/metadata-block-info/domain/repositories/MetadataBlockInfoRepository' import { useGetCollectionMetadataBlocksInfo } from '@/shared/hooks/useGetCollectionMetadataBlocksInfo' import { useCollection } from '../collection/useCollection' -import { useLoading } from '../loading/LoadingContext' +import { useLoading } from '../../shared/contexts/loading/LoadingContext' import { NotFoundPage } from '../not-found-page/NotFoundPage' import { AppLoader } from '../shared/layout/app-loader/AppLoader' import { BreadcrumbsGenerator } from '../shared/hierarchy/BreadcrumbsGenerator' diff --git a/src/sections/collection/collection-items-panel/CollectionItemsPanel.tsx b/src/sections/collection/collection-items-panel/CollectionItemsPanel.tsx index 492801de4..61fb652cb 100644 --- a/src/sections/collection/collection-items-panel/CollectionItemsPanel.tsx +++ b/src/sections/collection/collection-items-panel/CollectionItemsPanel.tsx @@ -15,7 +15,7 @@ import { CollectionItemsQueryParams } from '@/collection/domain/models/Collectio import { useGetAccumulatedItems } from './useGetAccumulatedItems' import { UseCollectionQueryParamsReturnType } from '../useGetCollectionQueryParams' import { useLoadMoreOnPopStateEvent } from './useLoadMoreOnPopStateEvent' -import { useLoading } from '@/sections/loading/LoadingContext' +import { useLoading } from '@/shared/contexts/loading/LoadingContext' import { CollectionHelper } from '../CollectionHelper' import { FilterPanel } from '@/sections/collection/collection-items-panel/filter-panel/FilterPanel' import { RemoveAddFacetFilter } from '@/sections/collection/collection-items-panel/filter-panel/facets-filters/FacetFilterGroup' diff --git a/src/sections/create-collection/CreateCollection.tsx b/src/sections/create-collection/CreateCollection.tsx index 8a04ddc90..1b7778960 100644 --- a/src/sections/create-collection/CreateCollection.tsx +++ b/src/sections/create-collection/CreateCollection.tsx @@ -4,7 +4,7 @@ import { Alert } from '@iqss/dataverse-design-system' import { useCollection } from '@/sections/collection/useCollection' import { CollectionRepository } from '@/collection/domain/repositories/CollectionRepository' import { MetadataBlockInfoRepository } from '@/metadata-block-info/domain/repositories/MetadataBlockInfoRepository' -import { useLoading } from '../loading/LoadingContext' +import { useLoading } from '../../shared/contexts/loading/LoadingContext' import { useSession } from '../session/SessionContext' import { useGetCollectionUserPermissions } from '@/shared/hooks/useGetCollectionUserPermissions' import { User } from '@/users/domain/models/User' diff --git a/src/sections/create-dataset/CreateDataset.tsx b/src/sections/create-dataset/CreateDataset.tsx index b262140b2..d2d01f42a 100644 --- a/src/sections/create-dataset/CreateDataset.tsx +++ b/src/sections/create-dataset/CreateDataset.tsx @@ -10,7 +10,7 @@ import { useNotImplementedModal } from '../not-implemented/NotImplementedModalCo import { DatasetMetadataForm } from '../shared/form/DatasetMetadataForm' import { useGetCollectionUserPermissions } from '../../shared/hooks/useGetCollectionUserPermissions' import { CollectionRepository } from '../../collection/domain/repositories/CollectionRepository' -import { useLoading } from '../loading/LoadingContext' +import { useLoading } from '../../shared/contexts/loading/LoadingContext' import { BreadcrumbsGenerator } from '../shared/hierarchy/BreadcrumbsGenerator' import { useCollection } from '../collection/useCollection' diff --git a/src/sections/dataset/Dataset.tsx b/src/sections/dataset/Dataset.tsx index 4a767111c..151c2da49 100644 --- a/src/sections/dataset/Dataset.tsx +++ b/src/sections/dataset/Dataset.tsx @@ -3,7 +3,7 @@ import { Col, Row, Tabs } from '@iqss/dataverse-design-system' import styles from './Dataset.module.scss' import { useNavigate, useSearchParams } from 'react-router-dom' import { DatasetLabels } from './dataset-labels/DatasetLabels' -import { useLoading } from '../loading/LoadingContext' +import { useLoading } from '../../shared/contexts/loading/LoadingContext' import { DatasetSkeleton, TabsSkeleton } from './DatasetSkeleton' import { NotFoundPage } from '../not-found-page/NotFoundPage' import { useTranslation } from 'react-i18next' diff --git a/src/sections/edit-collection-featured-items/EditFeaturedItems.tsx b/src/sections/edit-collection-featured-items/EditFeaturedItems.tsx index f3cc56875..3b3900bd7 100644 --- a/src/sections/edit-collection-featured-items/EditFeaturedItems.tsx +++ b/src/sections/edit-collection-featured-items/EditFeaturedItems.tsx @@ -4,7 +4,7 @@ import { Alert } from '@iqss/dataverse-design-system' import { CollectionRepository } from '@/collection/domain/repositories/CollectionRepository' import { useGetFeaturedItems } from '../collection/useGetFeaturedItems' import { useCollection } from '../collection/useCollection' -import { useLoading } from '../loading/LoadingContext' +import { useLoading } from '../../shared/contexts/loading/LoadingContext' import { BreadcrumbsGenerator } from '../shared/hierarchy/BreadcrumbsGenerator' import { SeparationLine } from '../shared/layout/SeparationLine/SeparationLine' import { NotFoundPage } from '../not-found-page/NotFoundPage' diff --git a/src/sections/edit-collection/EditCollection.tsx b/src/sections/edit-collection/EditCollection.tsx index 72ade9a15..0ee6c71a2 100644 --- a/src/sections/edit-collection/EditCollection.tsx +++ b/src/sections/edit-collection/EditCollection.tsx @@ -4,7 +4,7 @@ import { Alert } from '@iqss/dataverse-design-system' import { CollectionRepository } from '@/collection/domain/repositories/CollectionRepository' import { MetadataBlockInfoRepository } from '@/metadata-block-info/domain/repositories/MetadataBlockInfoRepository' import { useGetCollectionUserPermissions } from '@/shared/hooks/useGetCollectionUserPermissions' -import { useLoading } from '../loading/LoadingContext' +import { useLoading } from '../../shared/contexts/loading/LoadingContext' import { useSession } from '../session/SessionContext' import { useCollection } from '../collection/useCollection' import { User } from '@/users/domain/models/User' diff --git a/src/sections/edit-dataset-metadata/EditDatasetMetadata.tsx b/src/sections/edit-dataset-metadata/EditDatasetMetadata.tsx index 6b926d2fd..4ba85e69a 100644 --- a/src/sections/edit-dataset-metadata/EditDatasetMetadata.tsx +++ b/src/sections/edit-dataset-metadata/EditDatasetMetadata.tsx @@ -4,7 +4,7 @@ import { Alert, Tabs } from '@iqss/dataverse-design-system' import { DatasetRepository } from '../../dataset/domain/repositories/DatasetRepository' import { MetadataBlockInfoRepository } from '../../metadata-block-info/domain/repositories/MetadataBlockInfoRepository' import { useDataset } from '../dataset/DatasetContext' -import { useLoading } from '../loading/LoadingContext' +import { useLoading } from '../../shared/contexts/loading/LoadingContext' import { HostCollection } from './HostCollection' import { BreadcrumbsGenerator } from '../shared/hierarchy/BreadcrumbsGenerator' import { SeparationLine } from '../shared/layout/SeparationLine/SeparationLine' diff --git a/src/sections/edit-file-metadata/EditFileMetadata.tsx b/src/sections/edit-file-metadata/EditFileMetadata.tsx index fb8696490..c72819a0e 100644 --- a/src/sections/edit-file-metadata/EditFileMetadata.tsx +++ b/src/sections/edit-file-metadata/EditFileMetadata.tsx @@ -10,7 +10,7 @@ import { EditFileMetadataFormData, EditFilesList } from '@/sections/edit-file-metadata/EditFilesList' -import { useLoading } from '../loading/LoadingContext' +import { useLoading } from '../../shared/contexts/loading/LoadingContext' import { useFile } from '@/sections/file/useFile' import styles from './EditFileMetadata.module.scss' diff --git a/src/sections/featured-item/FeaturedItem.tsx b/src/sections/featured-item/FeaturedItem.tsx index 25c380a71..0403a96f5 100644 --- a/src/sections/featured-item/FeaturedItem.tsx +++ b/src/sections/featured-item/FeaturedItem.tsx @@ -5,7 +5,7 @@ import { CollectionRepository } from '@/collection/domain/repositories/Collectio import { CustomFeaturedItem } from '@/collection/domain/models/FeaturedItem' import { useGetFeaturedItems } from '../collection/useGetFeaturedItems' import { AppLoader } from '../shared/layout/app-loader/AppLoader' -import { useLoading } from '../loading/LoadingContext' +import { useLoading } from '../../shared/contexts/loading/LoadingContext' import { FeaturedItemView } from './featured-item-view/FeaturedItemView' import { useCollection } from '../collection/useCollection' diff --git a/src/sections/file/File.tsx b/src/sections/file/File.tsx index 101f7864a..c6c046fcf 100644 --- a/src/sections/file/File.tsx +++ b/src/sections/file/File.tsx @@ -4,7 +4,7 @@ import { ButtonGroup, Col, Row, Tabs } from '@iqss/dataverse-design-system' import { FileRepository } from '../../files/domain/repositories/FileRepository' import { useFile } from './useFile' import { useEffect, useState } from 'react' -import { useLoading } from '../loading/LoadingContext' +import { useLoading } from '../../shared/contexts/loading/LoadingContext' import { FileSkeleton } from './FileSkeleton' import { DatasetCitation } from '../dataset/dataset-citation/DatasetCitation' import { FileCitation } from './file-citation/FileCitation' diff --git a/src/sections/homepage/Homepage.tsx b/src/sections/homepage/Homepage.tsx index c36d3de20..e8a4ec96e 100644 --- a/src/sections/homepage/Homepage.tsx +++ b/src/sections/homepage/Homepage.tsx @@ -5,7 +5,7 @@ import { CollectionRepository } from '@/collection/domain/repositories/Collectio import { DataverseHubRepository } from '@/dataverse-hub/domain/repositories/DataverseHubRepository' import { useCollection } from '../collection/useCollection' import { FeaturedItems } from '../collection/featured-items/FeaturedItems' -import { useLoading } from '../loading/LoadingContext' +import { useLoading } from '../../shared/contexts/loading/LoadingContext' import { AppLoader } from '../shared/layout/app-loader/AppLoader' import { SearchInput } from './search-input/SearchInput' import { Metrics } from './metrics/Metrics' diff --git a/src/sections/layout/topbar-progress-indicator/TopbarProgressIndicator.tsx b/src/sections/layout/topbar-progress-indicator/TopbarProgressIndicator.tsx index 1c3b9d6ca..4d065e6c2 100644 --- a/src/sections/layout/topbar-progress-indicator/TopbarProgressIndicator.tsx +++ b/src/sections/layout/topbar-progress-indicator/TopbarProgressIndicator.tsx @@ -1,7 +1,7 @@ import TopBarProgress from 'react-topbar-progress-indicator' import { useTheme } from '@iqss/dataverse-design-system' import { useEffect, useState } from 'react' -import { useLoading } from '../../loading/LoadingContext' +import { useLoading } from '../../../shared/contexts/loading/LoadingContext' import isChromatic from 'chromatic/isChromatic' const TopBarProgressIndicator = () => { diff --git a/src/sections/not-found-page/NotFoundPage.tsx b/src/sections/not-found-page/NotFoundPage.tsx index b84cf7753..a259751ab 100644 --- a/src/sections/not-found-page/NotFoundPage.tsx +++ b/src/sections/not-found-page/NotFoundPage.tsx @@ -1,7 +1,7 @@ import { useEffect } from 'react' import { Link } from 'react-router-dom' import { Trans, useTranslation } from 'react-i18next' -import { useLoading } from '../loading/LoadingContext' +import { useLoading } from '../../shared/contexts/loading/LoadingContext' import { Route } from '../Route.enum' import styles from './NotFoundPage.module.scss' diff --git a/src/sections/replace-file/ReplaceFile.tsx b/src/sections/replace-file/ReplaceFile.tsx index 8bbf2d009..ea8382456 100644 --- a/src/sections/replace-file/ReplaceFile.tsx +++ b/src/sections/replace-file/ReplaceFile.tsx @@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next' import { Col, Row, Tabs } from '@iqss/dataverse-design-system' import { FileRepository } from '@/files/domain/repositories/FileRepository' import { useFile } from '../file/useFile' -import { useLoading } from '../loading/LoadingContext' +import { useLoading } from '../../shared/contexts/loading/LoadingContext' import { FileInfo } from './file-info/FileInfo' import { BreadcrumbsGenerator } from '../shared/hierarchy/BreadcrumbsGenerator' import { AppLoader } from '../shared/layout/app-loader/AppLoader' diff --git a/src/sections/shared/form/DatasetMetadataForm/index.tsx b/src/sections/shared/form/DatasetMetadataForm/index.tsx index 4ebd7aaab..10034a157 100644 --- a/src/sections/shared/form/DatasetMetadataForm/index.tsx +++ b/src/sections/shared/form/DatasetMetadataForm/index.tsx @@ -1,5 +1,5 @@ import { useEffect, useMemo } from 'react' -import { useLoading } from '../../../loading/LoadingContext' +import { useLoading } from '../../../../shared/contexts/loading/LoadingContext' import { useGetMetadataBlocksInfo } from './useGetMetadataBlocksInfo' import { DatasetRepository } from '../../../../dataset/domain/repositories/DatasetRepository' import { MetadataBlockInfoRepository } from '../../../../metadata-block-info/domain/repositories/MetadataBlockInfoRepository' diff --git a/src/sections/shared/form/EditCreateCollectionForm/EditCreateCollectionForm.tsx b/src/sections/shared/form/EditCreateCollectionForm/EditCreateCollectionForm.tsx index 801dd8e18..fc3204d53 100644 --- a/src/sections/shared/form/EditCreateCollectionForm/EditCreateCollectionForm.tsx +++ b/src/sections/shared/form/EditCreateCollectionForm/EditCreateCollectionForm.tsx @@ -5,7 +5,7 @@ import { useGetCollectionMetadataBlocksInfo } from '@/shared/hooks/useGetCollect import { useGetAllMetadataBlocksInfo } from '@/shared/hooks/useGetAllMetadataBlocksInfo' import { useGetCollectionFacets } from '@/shared/hooks/useGetCollectionFacets' import { useGetAllFacetableMetadataFields } from '@/shared/hooks/useGetAllFacetableMetadataFields' -import { useLoading } from '@/sections/loading/LoadingContext' +import { useLoading } from '@/shared/contexts/loading/LoadingContext' import { useDeepCompareMemo } from 'use-deep-compare' import { CollectionFormHelper } from './CollectionFormHelper' import { diff --git a/src/sections/sign-up/SignUp.tsx b/src/sections/sign-up/SignUp.tsx index 342f3989c..4d7e60f46 100644 --- a/src/sections/sign-up/SignUp.tsx +++ b/src/sections/sign-up/SignUp.tsx @@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next' import { Alert, Tabs } from '@iqss/dataverse-design-system' import { UserRepository } from '@/users/domain/repositories/UserRepository' import { DataverseInfoRepository } from '@/info/domain/repositories/DataverseInfoRepository' -import { useLoading } from '../loading/LoadingContext' +import { useLoading } from '../../shared/contexts/loading/LoadingContext' import { ValidTokenNotLinkedAccountForm } from './valid-token-not-linked-account-form/ValidTokenNotLinkedAccountForm' import styles from './SignUp.module.scss' diff --git a/src/sections/upload-dataset-files/UploadDatasetFiles.tsx b/src/sections/upload-dataset-files/UploadDatasetFiles.tsx index 2c3b45bb4..0f828b99d 100644 --- a/src/sections/upload-dataset-files/UploadDatasetFiles.tsx +++ b/src/sections/upload-dataset-files/UploadDatasetFiles.tsx @@ -2,7 +2,7 @@ import { useEffect } from 'react' import { useTranslation } from 'react-i18next' import { Tabs } from '@iqss/dataverse-design-system' import { FileRepository } from '../../files/domain/repositories/FileRepository' -import { useLoading } from '../loading/LoadingContext' +import { useLoading } from '../../shared/contexts/loading/LoadingContext' import { useDataset } from '../dataset/DatasetContext' import { NotFoundPage } from '../not-found-page/NotFoundPage' import { BreadcrumbsGenerator } from '../shared/hierarchy/BreadcrumbsGenerator' diff --git a/src/sections/loading/LoadingContext.ts b/src/shared/contexts/loading/LoadingContext.ts similarity index 100% rename from src/sections/loading/LoadingContext.ts rename to src/shared/contexts/loading/LoadingContext.ts diff --git a/src/sections/loading/LoadingProvider.tsx b/src/shared/contexts/loading/LoadingProvider.tsx similarity index 100% rename from src/sections/loading/LoadingProvider.tsx rename to src/shared/contexts/loading/LoadingProvider.tsx diff --git a/src/stories/WithLayout.tsx b/src/stories/WithLayout.tsx index 34a5af36d..cc7b89175 100644 --- a/src/stories/WithLayout.tsx +++ b/src/stories/WithLayout.tsx @@ -1,7 +1,7 @@ import { StoryFn } from '@storybook/react' import { Routes, Route } from 'react-router-dom' import { Layout } from '../sections/layout/Layout' -import { LoadingProvider } from '../sections/loading/LoadingProvider' +import { LoadingProvider } from '../shared/contexts/loading/LoadingProvider' export const WithLayout = (Story: StoryFn) => ( From 07685ccfe0040b781337e13d23efeaa29998929d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Mon, 25 Aug 2025 16:45:24 -0300 Subject: [PATCH 03/51] feat: dataset explore tools --- public/locales/en/dataset.json | 5 +- public/locales/en/shared.json | 4 +- src/App.tsx | 8 +- ...lUrl.ts => DatasetExternalToolResolved.ts} | 2 +- ...ToolUrl.ts => FileExternalToolResolved.ts} | 2 +- .../repositories/ExternalToolsRepository.ts | 8 +- .../useCases/GetDatasetExternalToolUrl.ts | 4 +- .../domain/useCases/GetFileExternalToolUrl.ts | 4 +- .../ExternalToolsJSDataverseRepository.ts | 8 +- src/sections/dataset/Dataset.tsx | 4 + src/sections/dataset/DatasetFactory.tsx | 4 + .../DatasetActionButtons.tsx | 7 +- .../access-dataset-menu/AccessDatasetMenu.tsx | 19 +++- .../DatasetExploreOptions.tsx | 100 ++++++++++++++++ .../external-tools/ExternalToolsProvider.tsx | 107 ++++++++++++++++++ 15 files changed, 262 insertions(+), 24 deletions(-) rename src/externalTools/domain/models/{DatasetExternalToolUrl.ts => DatasetExternalToolResolved.ts} (65%) rename src/externalTools/domain/models/{FileExternalToolUrl.ts => FileExternalToolResolved.ts} (66%) create mode 100644 src/sections/dataset/dataset-action-buttons/access-dataset-menu/DatasetExploreOptions.tsx create mode 100644 src/shared/contexts/external-tools/ExternalToolsProvider.tsx diff --git a/public/locales/en/dataset.json b/public/locales/en/dataset.json index d2bf605c1..cd629de8e 100644 --- a/public/locales/en/dataset.json +++ b/public/locales/en/dataset.json @@ -69,7 +69,8 @@ "zip": "Download ZIP", "originalZip": "Original Format ZIP", "archivalZip": "Archival Format (.tab) ZIP" - } + }, + "exploreOptions": "Explore Options" }, "uploadFiles": "Upload Files", "share": { @@ -318,4 +319,4 @@ "contact": { "tip": "Use email button above to contact." } -} +} \ No newline at end of file diff --git a/public/locales/en/shared.json b/public/locales/en/shared.json index 77a93d6b7..69f1e816a 100644 --- a/public/locales/en/shared.json +++ b/public/locales/en/shared.json @@ -27,6 +27,8 @@ "dragHandleLabel": "press space to select and keys to drag", "unknown": "Unknown", "find": "Find", + "allowPopups": "You must enable popups in your browser to open external tools in a new window or tab.", + "externalToolOpeningFailed": "There was a problem opening the external tool. Please try again.", "pageNumberNotFound": { "heading": "Page Number Not Found", "message": "The page number you requested does not exist. Please try a different page number." @@ -289,4 +291,4 @@ "truncateMoreTip": "Click to read the full {{contentName}}", "truncateLessTip": "Click to collapse the {{contentName}}" } -} +} \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 0344a7124..2cec4e501 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,6 +5,8 @@ import { DataverseApiAuthMechanism } from '@iqss/dataverse-client-javascript/dis import { Router } from './router' import { Route } from './sections/Route.enum' import { OIDC_AUTH_CONFIG, DATAVERSE_BACKEND_URL } from './config' +import { ExternalToolsProvider } from './shared/contexts/external-tools/ExternalToolsProvider' +import { ExternalToolsJSDataverseRepository } from './externalTools/infrastructure/repositories/ExternalToolsJSDataverseRepository' import 'react-loading-skeleton/dist/skeleton.css' import './assets/global.scss' import './assets/react-toastify-custom.scss' @@ -46,11 +48,15 @@ const authConfig: TAuthConfig = { clearURL: false } +const externalToolsRepository = new ExternalToolsJSDataverseRepository() + function App() { return ( <> - + + + diff --git a/src/externalTools/domain/models/DatasetExternalToolUrl.ts b/src/externalTools/domain/models/DatasetExternalToolResolved.ts similarity index 65% rename from src/externalTools/domain/models/DatasetExternalToolUrl.ts rename to src/externalTools/domain/models/DatasetExternalToolResolved.ts index a7d1a6039..fabf2f698 100644 --- a/src/externalTools/domain/models/DatasetExternalToolUrl.ts +++ b/src/externalTools/domain/models/DatasetExternalToolResolved.ts @@ -1,4 +1,4 @@ -export interface DatasetExternalToolUrl { +export interface DatasetExternalToolResolved { toolUrlResolved: string displayName: string datasetId: number diff --git a/src/externalTools/domain/models/FileExternalToolUrl.ts b/src/externalTools/domain/models/FileExternalToolResolved.ts similarity index 66% rename from src/externalTools/domain/models/FileExternalToolUrl.ts rename to src/externalTools/domain/models/FileExternalToolResolved.ts index bcdd64859..2fe2303d7 100644 --- a/src/externalTools/domain/models/FileExternalToolUrl.ts +++ b/src/externalTools/domain/models/FileExternalToolResolved.ts @@ -1,4 +1,4 @@ -export interface FileExternalToolUrl { +export interface FileExternalToolResolved { toolUrlResolved: string displayName: string fileId: number diff --git a/src/externalTools/domain/repositories/ExternalToolsRepository.ts b/src/externalTools/domain/repositories/ExternalToolsRepository.ts index ed6eab542..0309ee367 100644 --- a/src/externalTools/domain/repositories/ExternalToolsRepository.ts +++ b/src/externalTools/domain/repositories/ExternalToolsRepository.ts @@ -1,6 +1,6 @@ -import { DatasetExternalToolUrl } from '../models/DatasetExternalToolUrl' +import { DatasetExternalToolResolved } from '../models/DatasetExternalToolResolved' import { ExternalTool } from '../models/ExternalTool' -import { FileExternalToolUrl } from '../models/FileExternalToolUrl' +import { FileExternalToolResolved } from '../models/FileExternalToolResolved' import { GetExternalToolDTO } from '../useCases/DTOs/GetExternalToolUrlDTO' export interface ExternalToolsRepository { @@ -9,10 +9,10 @@ export interface ExternalToolsRepository { datasetId: number | string, toolId: number, getExternalToolDTO: GetExternalToolDTO - ): Promise + ): Promise getFileExternalToolUrl( fileId: number | string, toolId: number, getExternalToolDTO: GetExternalToolDTO - ): Promise + ): Promise } diff --git a/src/externalTools/domain/useCases/GetDatasetExternalToolUrl.ts b/src/externalTools/domain/useCases/GetDatasetExternalToolUrl.ts index f40fbb71a..406df6ded 100644 --- a/src/externalTools/domain/useCases/GetDatasetExternalToolUrl.ts +++ b/src/externalTools/domain/useCases/GetDatasetExternalToolUrl.ts @@ -1,4 +1,4 @@ -import { DatasetExternalToolUrl } from '../models/DatasetExternalToolUrl' +import { DatasetExternalToolResolved } from '../models/DatasetExternalToolResolved' import { ExternalToolsRepository } from '../repositories/ExternalToolsRepository' import { GetExternalToolDTO } from './DTOs/GetExternalToolUrlDTO' @@ -7,6 +7,6 @@ export function getDatasetExternalToolUrl( datasetId: number | string, toolId: number, getExternalToolDTO: GetExternalToolDTO -): Promise { +): Promise { return externalToolsRepository.getDatasetExternalToolUrl(datasetId, toolId, getExternalToolDTO) } diff --git a/src/externalTools/domain/useCases/GetFileExternalToolUrl.ts b/src/externalTools/domain/useCases/GetFileExternalToolUrl.ts index d896d6e96..47965d4e9 100644 --- a/src/externalTools/domain/useCases/GetFileExternalToolUrl.ts +++ b/src/externalTools/domain/useCases/GetFileExternalToolUrl.ts @@ -1,4 +1,4 @@ -import { FileExternalToolUrl } from '../models/FileExternalToolUrl' +import { FileExternalToolResolved } from '../models/FileExternalToolResolved' import { ExternalToolsRepository } from '../repositories/ExternalToolsRepository' import { GetExternalToolDTO } from './DTOs/GetExternalToolUrlDTO' @@ -7,6 +7,6 @@ export function getFileExternalToolUrl( fileId: number | string, toolId: number, getExternalToolDTO: GetExternalToolDTO -): Promise { +): Promise { return externalToolsRepository.getFileExternalToolUrl(fileId, toolId, getExternalToolDTO) } diff --git a/src/externalTools/infrastructure/repositories/ExternalToolsJSDataverseRepository.ts b/src/externalTools/infrastructure/repositories/ExternalToolsJSDataverseRepository.ts index 3e3c25ee1..78eb5e4ed 100644 --- a/src/externalTools/infrastructure/repositories/ExternalToolsJSDataverseRepository.ts +++ b/src/externalTools/infrastructure/repositories/ExternalToolsJSDataverseRepository.ts @@ -3,9 +3,9 @@ import { getExternalTools, getFileExternalToolUrl } from '@iqss/dataverse-client-javascript' -import { DatasetExternalToolUrl } from '@/externalTools/domain/models/DatasetExternalToolUrl' +import { DatasetExternalToolResolved } from '@/externalTools/domain/models/DatasetExternalToolResolved' import { ExternalTool } from '@/externalTools/domain/models/ExternalTool' -import { FileExternalToolUrl } from '@/externalTools/domain/models/FileExternalToolUrl' +import { FileExternalToolResolved } from '@/externalTools/domain/models/FileExternalToolResolved' import { ExternalToolsRepository } from '@/externalTools/domain/repositories/ExternalToolsRepository' import { GetExternalToolDTO } from '@/externalTools/domain/useCases/DTOs/GetExternalToolUrlDTO' @@ -18,7 +18,7 @@ export class ExternalToolsJSDataverseRepository implements ExternalToolsReposito datasetId: number | string, toolId: number, getExternalToolDTO: GetExternalToolDTO - ): Promise { + ): Promise { return getDatasetExternalToolUrl .execute(datasetId, toolId, getExternalToolDTO) .then((jsDatasetExternalToolUrl) => jsDatasetExternalToolUrl) @@ -28,7 +28,7 @@ export class ExternalToolsJSDataverseRepository implements ExternalToolsReposito fileId: number | string, toolId: number, getExternalToolDTO: GetExternalToolDTO - ): Promise { + ): Promise { return getFileExternalToolUrl .execute(fileId, toolId, getExternalToolDTO) .then((jsFileExternalToolUrl) => jsFileExternalToolUrl) diff --git a/src/sections/dataset/Dataset.tsx b/src/sections/dataset/Dataset.tsx index 151c2da49..a882043d8 100644 --- a/src/sections/dataset/Dataset.tsx +++ b/src/sections/dataset/Dataset.tsx @@ -31,6 +31,7 @@ import { DatasetVersions } from './dataset-versions/DatasetVersions' import { ContactRepository } from '@/contact/domain/repositories/ContactRepository' import { DatasetMetrics } from './dataset-metrics/DatasetMetrics' import { DatasetPublishingStatus } from '@/dataset/domain/models/Dataset' +import { ExternalToolsRepository } from '@/externalTools/domain/repositories/ExternalToolsRepository' interface DatasetProps { datasetRepository: DatasetRepository @@ -38,6 +39,7 @@ interface DatasetProps { metadataBlockInfoRepository: MetadataBlockInfoRepository collectionRepository: CollectionRepository contactRepository: ContactRepository + externalToolsRepository: ExternalToolsRepository filesTabInfiniteScrollEnabled?: boolean publishInProgress?: boolean tab?: string @@ -49,6 +51,7 @@ export function Dataset({ metadataBlockInfoRepository, collectionRepository, contactRepository, + externalToolsRepository, filesTabInfiniteScrollEnabled, publishInProgress, tab = 'files' @@ -148,6 +151,7 @@ export function Dataset({ collectionRepository={collectionRepository} dataset={dataset} contactRepository={contactRepository} + externalToolsRepository={externalToolsRepository} /> @@ -97,6 +100,7 @@ function DatasetWithSearchParams() { fileRepository={fileRepository} metadataBlockInfoRepository={metadataBlockInfoRepository} contactRepository={contactRepository} + externalToolsRepository={externalToolsRepository} publishInProgress={publishInProgress} filesTabInfiniteScrollEnabled={FILES_TAB_INFINITE_SCROLL_ENABLED} tab={tab} diff --git a/src/sections/dataset/dataset-action-buttons/DatasetActionButtons.tsx b/src/sections/dataset/dataset-action-buttons/DatasetActionButtons.tsx index b34fccbc8..2207d5261 100644 --- a/src/sections/dataset/dataset-action-buttons/DatasetActionButtons.tsx +++ b/src/sections/dataset/dataset-action-buttons/DatasetActionButtons.tsx @@ -11,6 +11,7 @@ import { LinkDatasetButton } from './link-dataset-button/LinkDatasetButton' import { ShareDatasetButton } from './share-dataset-button/ShareDatasetButton' import { ContactButton } from '@/sections/shared/contact/ContactButton' import { ContactRepository } from '@/contact/domain/repositories/ContactRepository' +import { ExternalToolsRepository } from '@/externalTools/domain/repositories/ExternalToolsRepository' import styles from './DatasetActionButtons.module.scss' interface DatasetActionButtonsProps { @@ -18,13 +19,15 @@ interface DatasetActionButtonsProps { datasetRepository: DatasetRepository collectionRepository: CollectionRepository contactRepository: ContactRepository + externalToolsRepository: ExternalToolsRepository } export function DatasetActionButtons({ dataset, datasetRepository, collectionRepository, - contactRepository + contactRepository, + externalToolsRepository }: DatasetActionButtonsProps) { const { t } = useTranslation('dataset') @@ -37,6 +40,8 @@ export function DatasetActionButtons({ fileDownloadSizes={dataset.fileDownloadSizes} downloadUrls={dataset.downloadUrls} fileStore={dataset.fileStore} + persistentId={dataset.persistentId} + externalToolsRepository={externalToolsRepository} /> - - {t('datasetActionButtons.accessDataset.downloadOptions.header')} + + {t('datasetActionButtons.accessDataset.downloadOptions.header')} + ) } @@ -105,5 +115,4 @@ const DatasetDownloadOptions = ({ } // TODO: add download feature https://github.com/IQSS/dataverse-frontend/issues/63 -// TODO: add explore feature // TODO: add compute feature diff --git a/src/sections/dataset/dataset-action-buttons/access-dataset-menu/DatasetExploreOptions.tsx b/src/sections/dataset/dataset-action-buttons/access-dataset-menu/DatasetExploreOptions.tsx new file mode 100644 index 000000000..e25a5b204 --- /dev/null +++ b/src/sections/dataset/dataset-action-buttons/access-dataset-menu/DatasetExploreOptions.tsx @@ -0,0 +1,100 @@ +import { useState } from 'react' +import { toast } from 'react-toastify' +import { useTranslation } from 'react-i18next' +import { BarChartFill as BarChartFillIcon } from 'react-bootstrap-icons' +import { DropdownButtonItem, DropdownHeader } from '@iqss/dataverse-design-system' +import { useExternalTools } from '@/shared/contexts/external-tools/ExternalToolsProvider' +import { getDatasetExternalToolUrl } from '@/externalTools/domain/useCases/GetDatasetExternalToolUrl' +import { ExternalToolsRepository } from '@/externalTools/domain/repositories/ExternalToolsRepository' + +interface DatasetExploreOptionsProps { + externalToolsRepository: ExternalToolsRepository + persistentId: string +} + +export const DatasetExploreOptions = ({ + externalToolsRepository, + persistentId +}: DatasetExploreOptionsProps) => { + const { t } = useTranslation('dataset') + const { datasetExploreTools } = useExternalTools() + + if (datasetExploreTools.length === 0) return null + + return ( + <> + + {t('datasetActionButtons.accessDataset.exploreOptions')} + + + {datasetExploreTools.map((tool) => ( + + ))} + + ) +} + +interface ExploreOptionProps { + toolId: number + toolDisplayName: string + persistentId: string + externalToolsRepository: ExternalToolsRepository +} + +const ExploreOption = ({ + toolId, + toolDisplayName, + persistentId, + externalToolsRepository +}: ExploreOptionProps) => { + const [isOpening, setIsOpening] = useState(false) + const { t: tShared } = useTranslation('shared') + + const handleClick = async () => { + // If already opening, do nothing + if (isOpening) return + setIsOpening(true) + + const newWindow = window.open('', '_blank') + + // If the window didn't open, likely due to a popup blocker + if (!newWindow || newWindow.closed || typeof newWindow.closed === 'undefined') { + toast.info(tShared('allowPopups')) + setIsOpening(false) + newWindow?.close() + return + } + + try { + // Set a temporary title on the new window while fetching the tool URL + newWindow.document.title = `Loading ${toolDisplayName}...` + + const datasetExternalTool = await getDatasetExternalToolUrl( + externalToolsRepository, + persistentId, + toolId, + { preview: false, locale: 'en' } + ) + // Change the location of the new window to the tool URL + newWindow.location.href = datasetExternalTool.toolUrlResolved + } catch (error) { + // If there's an error, notify the user and close the new window + toast.error(tShared('externalToolOpeningFailed')) + if (!newWindow?.closed) newWindow.close() + } finally { + setIsOpening(false) + } + } + + return ( + + {toolDisplayName} + + ) +} diff --git a/src/shared/contexts/external-tools/ExternalToolsProvider.tsx b/src/shared/contexts/external-tools/ExternalToolsProvider.tsx new file mode 100644 index 000000000..778d47369 --- /dev/null +++ b/src/shared/contexts/external-tools/ExternalToolsProvider.tsx @@ -0,0 +1,107 @@ +import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react' +import { ExternalTool, ToolScope, ToolType } from '@/externalTools/domain/models/ExternalTool' +import { ExternalToolsRepository } from '@/externalTools/domain/repositories/ExternalToolsRepository' +import { getExternalTools } from '@/externalTools/domain/useCases/GetExternalTools' +import { ReadError } from '@iqss/dataverse-client-javascript' +import { JSDataverseReadErrorHandler } from '@/shared/helpers/JSDataverseReadErrorHandler' + +type ExternalToolsContextValue = { + externalTools: ExternalTool[] + loading: boolean + error: unknown + refreshExternalTools: () => Promise + datasetExploreTools: ExternalTool[] + fileExploreTools: ExternalTool[] + filePreviewTools: ExternalTool[] +} + +const ExternalToolsContext = createContext(undefined) + +type ExternalToolsProviderProps = { + externalToolsRepository: ExternalToolsRepository + children: React.ReactNode +} + +export function ExternalToolsProvider({ + externalToolsRepository, + children +}: ExternalToolsProviderProps) { + const [externalTools, setExternalTools] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(undefined) + + const fetchExternalTools = useCallback(async () => { + setLoading(true) + setError(undefined) + + try { + const data = await getExternalTools(externalToolsRepository) + setExternalTools(data) + } catch (err: ReadError | unknown) { + if (err instanceof ReadError) { + const error = new JSDataverseReadErrorHandler(err) + const formattedError = + error.getReasonWithoutStatusCode() ?? /* istanbul ignore next */ error.getErrorMessage() + + setError(formattedError) + } else { + setError('An unexpected error occurred while fetching external tools.') + } + } finally { + setLoading(false) + } + }, [externalToolsRepository]) + + useEffect(() => { + void fetchExternalTools() + }, [fetchExternalTools]) + + const datasetExploreTools = useMemo(() => { + return externalTools.filter( + (tool) => tool.scope === ToolScope.Dataset && tool.types.includes(ToolType.Explore) + ) + }, [externalTools]) + + const fileExploreTools = useMemo(() => { + return externalTools.filter( + (tool) => tool.scope === ToolScope.File && tool.types.includes(ToolType.Explore) + ) + }, [externalTools]) + + const filePreviewTools = useMemo(() => { + return externalTools.filter( + (tool) => tool.scope === ToolScope.File && tool.types.includes(ToolType.Preview) + ) + }, [externalTools]) + + const value = useMemo( + () => ({ + externalTools, + loading, + error, + datasetExploreTools, + fileExploreTools, + filePreviewTools, + refreshExternalTools: fetchExternalTools + }), + [ + externalTools, + loading, + error, + datasetExploreTools, + fileExploreTools, + filePreviewTools, + fetchExternalTools + ] + ) + + return {children} +} + +export function useExternalTools() { + const ctx = useContext(ExternalToolsContext) + if (!ctx) { + throw new Error('useExternalTools must be used within a ExternalToolsProvider') + } + return ctx +} From 1d76f0c0e0211369a8c614b9cb11321293d757eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Mon, 25 Aug 2025 17:21:23 -0300 Subject: [PATCH 04/51] refactor: rename external tool URL methods and interfaces for clarity --- package-lock.json | 8 ++++---- package.json | 2 +- .../repositories/ExternalToolsRepository.ts | 6 +++--- ...rnalToolUrlDTO.ts => GetExternalToolDTO.ts} | 0 ...rl.ts => GetDatasetExternalToolResolved.ts} | 10 +++++++--- ...olUrl.ts => GetFileExternalToolResolved.ts} | 6 +++--- .../ExternalToolsJSDataverseRepository.ts | 18 +++++++++--------- .../DatasetExploreOptions.tsx | 4 ++-- 8 files changed, 29 insertions(+), 25 deletions(-) rename src/externalTools/domain/useCases/DTOs/{GetExternalToolUrlDTO.ts => GetExternalToolDTO.ts} (100%) rename src/externalTools/domain/useCases/{GetDatasetExternalToolUrl.ts => GetDatasetExternalToolResolved.ts} (60%) rename src/externalTools/domain/useCases/{GetFileExternalToolUrl.ts => GetFileExternalToolResolved.ts} (62%) diff --git a/package-lock.json b/package-lock.json index 345f9fbf9..a7cf7db51 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@dnd-kit/sortable": "8.0.0", "@dnd-kit/utilities": "3.2.2", "@faker-js/faker": "7.6.0", - "@iqss/dataverse-client-javascript": "2.0.0-pr353.fc750c9", + "@iqss/dataverse-client-javascript": "2.0.0-pr353.5ed2e8a", "@iqss/dataverse-design-system": "*", "@istanbuljs/nyc-config-typescript": "1.0.2", "@tanstack/react-table": "8.9.2", @@ -3561,9 +3561,9 @@ }, "node_modules/@iqss/dataverse-client-javascript": { "name": "@IQSS/dataverse-client-javascript", - "version": "2.0.0-pr353.fc750c9", - "resolved": "https://npm.pkg.github.com/download/@IQSS/dataverse-client-javascript/2.0.0-pr353.fc750c9/3547ae61dcaa0e27c4ad3c03b23a9c3720543488", - "integrity": "sha512-ARsTrMRclcKYAU3mWJleoi+KPsg2UdjyI7RFgTXSnTdRu+y7VjwW5Tjb1tEoui41PvHg5m6dseshSSEZj/2QHA==", + "version": "2.0.0-pr353.5ed2e8a", + "resolved": "https://npm.pkg.github.com/download/@IQSS/dataverse-client-javascript/2.0.0-pr353.5ed2e8a/61b6036f0c33d8dedcadd9873a7b745a41bb9f31", + "integrity": "sha512-rm1YsyDnG1rZUD2c2XUUIPxN5RikvAbY3UkdsEvBtTWIUcb2UhzHgwZGpxTLQiA2OVS6Hqhy2+J6VVWt6PXOOQ==", "license": "MIT", "dependencies": { "@types/node": "^18.15.11", diff --git a/package.json b/package.json index d14fd9fe5..22f0ea598 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "@dnd-kit/sortable": "8.0.0", "@dnd-kit/utilities": "3.2.2", "@faker-js/faker": "7.6.0", - "@iqss/dataverse-client-javascript": "2.0.0-pr353.fc750c9", + "@iqss/dataverse-client-javascript": "2.0.0-pr353.5ed2e8a", "@iqss/dataverse-design-system": "*", "@istanbuljs/nyc-config-typescript": "1.0.2", "@tanstack/react-table": "8.9.2", diff --git a/src/externalTools/domain/repositories/ExternalToolsRepository.ts b/src/externalTools/domain/repositories/ExternalToolsRepository.ts index 0309ee367..ee8d5a7e7 100644 --- a/src/externalTools/domain/repositories/ExternalToolsRepository.ts +++ b/src/externalTools/domain/repositories/ExternalToolsRepository.ts @@ -1,16 +1,16 @@ import { DatasetExternalToolResolved } from '../models/DatasetExternalToolResolved' import { ExternalTool } from '../models/ExternalTool' import { FileExternalToolResolved } from '../models/FileExternalToolResolved' -import { GetExternalToolDTO } from '../useCases/DTOs/GetExternalToolUrlDTO' +import { GetExternalToolDTO } from '../useCases/DTOs/GetExternalToolDTO' export interface ExternalToolsRepository { getExternalTools(): Promise - getDatasetExternalToolUrl( + getDatasetExternalToolResolved( datasetId: number | string, toolId: number, getExternalToolDTO: GetExternalToolDTO ): Promise - getFileExternalToolUrl( + getFileExternalToolResolved( fileId: number | string, toolId: number, getExternalToolDTO: GetExternalToolDTO diff --git a/src/externalTools/domain/useCases/DTOs/GetExternalToolUrlDTO.ts b/src/externalTools/domain/useCases/DTOs/GetExternalToolDTO.ts similarity index 100% rename from src/externalTools/domain/useCases/DTOs/GetExternalToolUrlDTO.ts rename to src/externalTools/domain/useCases/DTOs/GetExternalToolDTO.ts diff --git a/src/externalTools/domain/useCases/GetDatasetExternalToolUrl.ts b/src/externalTools/domain/useCases/GetDatasetExternalToolResolved.ts similarity index 60% rename from src/externalTools/domain/useCases/GetDatasetExternalToolUrl.ts rename to src/externalTools/domain/useCases/GetDatasetExternalToolResolved.ts index 406df6ded..6401f008c 100644 --- a/src/externalTools/domain/useCases/GetDatasetExternalToolUrl.ts +++ b/src/externalTools/domain/useCases/GetDatasetExternalToolResolved.ts @@ -1,12 +1,16 @@ import { DatasetExternalToolResolved } from '../models/DatasetExternalToolResolved' import { ExternalToolsRepository } from '../repositories/ExternalToolsRepository' -import { GetExternalToolDTO } from './DTOs/GetExternalToolUrlDTO' +import { GetExternalToolDTO } from './DTOs/GetExternalToolDTO' -export function getDatasetExternalToolUrl( +export function getDatasetExternalToolResolved( externalToolsRepository: ExternalToolsRepository, datasetId: number | string, toolId: number, getExternalToolDTO: GetExternalToolDTO ): Promise { - return externalToolsRepository.getDatasetExternalToolUrl(datasetId, toolId, getExternalToolDTO) + return externalToolsRepository.getDatasetExternalToolResolved( + datasetId, + toolId, + getExternalToolDTO + ) } diff --git a/src/externalTools/domain/useCases/GetFileExternalToolUrl.ts b/src/externalTools/domain/useCases/GetFileExternalToolResolved.ts similarity index 62% rename from src/externalTools/domain/useCases/GetFileExternalToolUrl.ts rename to src/externalTools/domain/useCases/GetFileExternalToolResolved.ts index 47965d4e9..942c9837c 100644 --- a/src/externalTools/domain/useCases/GetFileExternalToolUrl.ts +++ b/src/externalTools/domain/useCases/GetFileExternalToolResolved.ts @@ -1,12 +1,12 @@ import { FileExternalToolResolved } from '../models/FileExternalToolResolved' import { ExternalToolsRepository } from '../repositories/ExternalToolsRepository' -import { GetExternalToolDTO } from './DTOs/GetExternalToolUrlDTO' +import { GetExternalToolDTO } from './DTOs/GetExternalToolDTO' -export function getFileExternalToolUrl( +export function getFileExternalToolResolved( externalToolsRepository: ExternalToolsRepository, fileId: number | string, toolId: number, getExternalToolDTO: GetExternalToolDTO ): Promise { - return externalToolsRepository.getFileExternalToolUrl(fileId, toolId, getExternalToolDTO) + return externalToolsRepository.getFileExternalToolResolved(fileId, toolId, getExternalToolDTO) } diff --git a/src/externalTools/infrastructure/repositories/ExternalToolsJSDataverseRepository.ts b/src/externalTools/infrastructure/repositories/ExternalToolsJSDataverseRepository.ts index 78eb5e4ed..125ffca6e 100644 --- a/src/externalTools/infrastructure/repositories/ExternalToolsJSDataverseRepository.ts +++ b/src/externalTools/infrastructure/repositories/ExternalToolsJSDataverseRepository.ts @@ -1,36 +1,36 @@ import { - getDatasetExternalToolUrl, + getDatasetExternalToolResolved, getExternalTools, - getFileExternalToolUrl + getFileExternalToolResolved } from '@iqss/dataverse-client-javascript' import { DatasetExternalToolResolved } from '@/externalTools/domain/models/DatasetExternalToolResolved' import { ExternalTool } from '@/externalTools/domain/models/ExternalTool' import { FileExternalToolResolved } from '@/externalTools/domain/models/FileExternalToolResolved' import { ExternalToolsRepository } from '@/externalTools/domain/repositories/ExternalToolsRepository' -import { GetExternalToolDTO } from '@/externalTools/domain/useCases/DTOs/GetExternalToolUrlDTO' +import { GetExternalToolDTO } from '@/externalTools/domain/useCases/DTOs/GetExternalToolDTO' export class ExternalToolsJSDataverseRepository implements ExternalToolsRepository { getExternalTools(): Promise { return getExternalTools.execute().then((jsExternalTools) => jsExternalTools) } - getDatasetExternalToolUrl( + getDatasetExternalToolResolved( datasetId: number | string, toolId: number, getExternalToolDTO: GetExternalToolDTO ): Promise { - return getDatasetExternalToolUrl + return getDatasetExternalToolResolved .execute(datasetId, toolId, getExternalToolDTO) - .then((jsDatasetExternalToolUrl) => jsDatasetExternalToolUrl) + .then((jsDatasetExternalToolResolved) => jsDatasetExternalToolResolved) } - getFileExternalToolUrl( + getFileExternalToolResolved( fileId: number | string, toolId: number, getExternalToolDTO: GetExternalToolDTO ): Promise { - return getFileExternalToolUrl + return getFileExternalToolResolved .execute(fileId, toolId, getExternalToolDTO) - .then((jsFileExternalToolUrl) => jsFileExternalToolUrl) + .then((jsFileExternalToolResolved) => jsFileExternalToolResolved) } } diff --git a/src/sections/dataset/dataset-action-buttons/access-dataset-menu/DatasetExploreOptions.tsx b/src/sections/dataset/dataset-action-buttons/access-dataset-menu/DatasetExploreOptions.tsx index e25a5b204..9a26aab44 100644 --- a/src/sections/dataset/dataset-action-buttons/access-dataset-menu/DatasetExploreOptions.tsx +++ b/src/sections/dataset/dataset-action-buttons/access-dataset-menu/DatasetExploreOptions.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next' import { BarChartFill as BarChartFillIcon } from 'react-bootstrap-icons' import { DropdownButtonItem, DropdownHeader } from '@iqss/dataverse-design-system' import { useExternalTools } from '@/shared/contexts/external-tools/ExternalToolsProvider' -import { getDatasetExternalToolUrl } from '@/externalTools/domain/useCases/GetDatasetExternalToolUrl' +import { getDatasetExternalToolResolved } from '@/externalTools/domain/useCases/GetDatasetExternalToolResolved' import { ExternalToolsRepository } from '@/externalTools/domain/repositories/ExternalToolsRepository' interface DatasetExploreOptionsProps { @@ -75,7 +75,7 @@ const ExploreOption = ({ // Set a temporary title on the new window while fetching the tool URL newWindow.document.title = `Loading ${toolDisplayName}...` - const datasetExternalTool = await getDatasetExternalToolUrl( + const datasetExternalTool = await getDatasetExternalToolResolved( externalToolsRepository, persistentId, toolId, From 3576772c8ea373cc4d8a29a850c8391f0995619c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Tue, 26 Aug 2025 10:01:08 -0300 Subject: [PATCH 05/51] feat: add external tools mock repository and update related tests and stories --- .storybook/preview.tsx | 6 ++- .../access-dataset-menu/AccessDatasetMenu.tsx | 1 - .../DatasetExploreOptions.tsx | 7 ++- src/stories/collection/Collection.stories.tsx | 2 +- src/stories/dataset/Dataset.stories.tsx | 12 ++++- .../DatasetActionButtons.stories.tsx | 6 ++- .../AccessDatasetMenu.stories.tsx | 7 +++ .../contact/ContactMockRepository.ts | 0 .../ExternalToolsMockRepository.ts | 43 ++++++++++++++++ .../contact-modal/ContactModal.stories.tsx | 2 +- .../DatasetExternalToolResolvedMother.ts | 13 +++++ .../domain/models/ExternalToolsMother.ts | 42 +++++++++++++++ .../models/FileExternalToolResolvedMother.ts | 13 +++++ .../sections/dataset/Dataset.spec.tsx | 20 +++++++- .../sections/dataset/DatasetProvider.spec.tsx | 2 +- .../DatasetActionButtons.spec.tsx | 4 ++ .../AccessDatasetMenu.spec.tsx | 19 +++++++ .../DatasetExploreOptions.spec.tsx | 51 +++++++++++++++++++ .../EditDatasetMetadata.spec.tsx | 2 +- .../EditFileMetadata.spec.tsx | 2 +- .../sections/loading/LoadingProvider.spec.tsx | 4 +- .../replace-file/ReplaceFile.spec.tsx | 2 +- .../UploadDatasetFiles.spec.tsx | 2 +- tests/support/commands.tsx | 6 ++- 24 files changed, 251 insertions(+), 17 deletions(-) rename src/stories/{shared => shared-mock-repositories}/contact/ContactMockRepository.ts (100%) create mode 100644 src/stories/shared-mock-repositories/externalTools/ExternalToolsMockRepository.ts rename src/stories/shared/{contact => }/contact-modal/ContactModal.stories.tsx (94%) create mode 100644 tests/component/externalTools/domain/models/DatasetExternalToolResolvedMother.ts create mode 100644 tests/component/externalTools/domain/models/ExternalToolsMother.ts create mode 100644 tests/component/externalTools/domain/models/FileExternalToolResolvedMother.ts create mode 100644 tests/component/sections/dataset/dataset-action-buttons/access-dataset-menu/DatasetExploreOptions.spec.tsx diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 00c579906..faee4990c 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -3,6 +3,8 @@ import type { Preview } from '@storybook/react' import { ThemeProvider } from '@iqss/dataverse-design-system' import { createBrowserRouter, RouteObject, RouterProvider } from 'react-router-dom' import { FakerHelper } from '../tests/component/shared/FakerHelper' +import { ExternalToolsProvider } from '../src/shared/contexts/external-tools/ExternalToolsProvider' +import { ExternalToolsMockRepository } from '../src/stories/shared-mock-repositories/externalTools/ExternalToolsMockRepository' import 'react-loading-skeleton/dist/skeleton.css' import '../src/assets/global.scss' import '../src/assets/swal-custom.scss' @@ -32,7 +34,9 @@ const preview: Preview = { return ( - + + + ) } diff --git a/src/sections/dataset/dataset-action-buttons/access-dataset-menu/AccessDatasetMenu.tsx b/src/sections/dataset/dataset-action-buttons/access-dataset-menu/AccessDatasetMenu.tsx index e0aa8ea79..8a8eeb8e1 100644 --- a/src/sections/dataset/dataset-action-buttons/access-dataset-menu/AccessDatasetMenu.tsx +++ b/src/sections/dataset/dataset-action-buttons/access-dataset-menu/AccessDatasetMenu.tsx @@ -114,5 +114,4 @@ const DatasetDownloadOptions = ({ ) } -// TODO: add download feature https://github.com/IQSS/dataverse-frontend/issues/63 // TODO: add compute feature diff --git a/src/sections/dataset/dataset-action-buttons/access-dataset-menu/DatasetExploreOptions.tsx b/src/sections/dataset/dataset-action-buttons/access-dataset-menu/DatasetExploreOptions.tsx index 9a26aab44..699f7867a 100644 --- a/src/sections/dataset/dataset-action-buttons/access-dataset-menu/DatasetExploreOptions.tsx +++ b/src/sections/dataset/dataset-action-buttons/access-dataset-menu/DatasetExploreOptions.tsx @@ -7,6 +7,9 @@ import { useExternalTools } from '@/shared/contexts/external-tools/ExternalTools import { getDatasetExternalToolResolved } from '@/externalTools/domain/useCases/GetDatasetExternalToolResolved' import { ExternalToolsRepository } from '@/externalTools/domain/repositories/ExternalToolsRepository' +// TODO:ME - Add File Preview icon on dataset files. +// TODO:ME - Add File Explore options. + interface DatasetExploreOptionsProps { externalToolsRepository: ExternalToolsRepository persistentId: string @@ -54,7 +57,7 @@ const ExploreOption = ({ externalToolsRepository }: ExploreOptionProps) => { const [isOpening, setIsOpening] = useState(false) - const { t: tShared } = useTranslation('shared') + const { t: tShared, i18n } = useTranslation('shared') const handleClick = async () => { // If already opening, do nothing @@ -79,7 +82,7 @@ const ExploreOption = ({ externalToolsRepository, persistentId, toolId, - { preview: false, locale: 'en' } + { preview: false, locale: i18n.language } ) // Change the location of the new window to the tool URL newWindow.location.href = datasetExternalTool.toolUrlResolved diff --git a/src/stories/collection/Collection.stories.tsx b/src/stories/collection/Collection.stories.tsx index 76bbb1c07..019c0fd02 100644 --- a/src/stories/collection/Collection.stories.tsx +++ b/src/stories/collection/Collection.stories.tsx @@ -8,7 +8,7 @@ import { CollectionLoadingMockRepository } from './CollectionLoadingMockReposito import { UnpublishedCollectionMockRepository } from '@/stories/collection/UnpublishedCollectionMockRepository' import { FeaturedItemMother } from '@tests/component/collection/domain/models/FeaturedItemMother' import { FakerHelper } from '@tests/component/shared/FakerHelper' -import { ContactMockRepository } from '../shared/contact/ContactMockRepository' +import { ContactMockRepository } from '../shared-mock-repositories/contact/ContactMockRepository' const meta: Meta = { title: 'Pages/Collection', diff --git a/src/stories/dataset/Dataset.stories.tsx b/src/stories/dataset/Dataset.stories.tsx index c61b16fb2..8b369e738 100644 --- a/src/stories/dataset/Dataset.stories.tsx +++ b/src/stories/dataset/Dataset.stories.tsx @@ -18,7 +18,8 @@ import { WithNotImplementedModal } from '../WithNotImplementedModal' import { MetadataBlockInfoMockRepository } from '../shared-mock-repositories/metadata-block-info/MetadataBlockInfoMockRepository' import { DatasetMockRepository } from './DatasetMockRepository' import { CollectionMockRepository } from '@/stories/collection/CollectionMockRepository' -import { ContactMockRepository } from '../shared/contact/ContactMockRepository' +import { ContactMockRepository } from '../shared-mock-repositories/contact/ContactMockRepository' +import { ExternalToolsMockRepository } from '../shared-mock-repositories/externalTools/ExternalToolsMockRepository' const meta: Meta = { title: 'Pages/Dataset', @@ -42,6 +43,7 @@ export const Default: Story = { fileRepository={new FileMockRepository()} metadataBlockInfoRepository={new MetadataBlockInfoMockRepository()} contactRepository={new ContactMockRepository()} + externalToolsRepository={new ExternalToolsMockRepository()} filesTabInfiniteScrollEnabled /> ) @@ -55,6 +57,7 @@ export const WithNormalPagination: Story = { fileRepository={new FileMockRepository()} metadataBlockInfoRepository={new MetadataBlockInfoMockRepository()} contactRepository={new ContactMockRepository()} + externalToolsRepository={new ExternalToolsMockRepository()} /> ) } @@ -68,6 +71,7 @@ export const DraftWithAllDatasetPermissions: Story = { fileRepository={new FileMockRepository()} metadataBlockInfoRepository={new MetadataBlockInfoMockRepository()} contactRepository={new ContactMockRepository()} + externalToolsRepository={new ExternalToolsMockRepository()} filesTabInfiniteScrollEnabled /> ) @@ -81,6 +85,7 @@ export const Deaccessioned: Story = { fileRepository={new FileMockRepository()} metadataBlockInfoRepository={new MetadataBlockInfoMockRepository()} contactRepository={new ContactMockRepository()} + externalToolsRepository={new ExternalToolsMockRepository()} filesTabInfiniteScrollEnabled /> ) @@ -94,6 +99,7 @@ export const LoggedInAsOwner: Story = { fileRepository={new FileMockRepository()} metadataBlockInfoRepository={new MetadataBlockInfoMockRepository()} contactRepository={new ContactMockRepository()} + externalToolsRepository={new ExternalToolsMockRepository()} filesTabInfiniteScrollEnabled /> ) @@ -108,6 +114,7 @@ export const Loading: Story = { fileRepository={new FileMockRepository()} metadataBlockInfoRepository={new MetadataBlockInfoMockRepository()} contactRepository={new ContactMockRepository()} + externalToolsRepository={new ExternalToolsMockRepository()} filesTabInfiniteScrollEnabled /> ) @@ -122,6 +129,7 @@ export const DatasetNotFound: Story = { fileRepository={new FileMockRepository()} metadataBlockInfoRepository={new MetadataBlockInfoMockRepository()} contactRepository={new ContactMockRepository()} + externalToolsRepository={new ExternalToolsMockRepository()} filesTabInfiniteScrollEnabled /> ) @@ -136,6 +144,7 @@ export const DatasetAnonymizedView: Story = { fileRepository={new FileMockRepository()} metadataBlockInfoRepository={new MetadataBlockInfoMockRepository()} contactRepository={new ContactMockRepository()} + externalToolsRepository={new ExternalToolsMockRepository()} filesTabInfiniteScrollEnabled /> ) @@ -150,6 +159,7 @@ export const DatasetWithNoFiles: Story = { fileRepository={new FileMockNoDataRepository()} metadataBlockInfoRepository={new MetadataBlockInfoMockRepository()} contactRepository={new ContactMockRepository()} + externalToolsRepository={new ExternalToolsMockRepository()} filesTabInfiniteScrollEnabled /> ) diff --git a/src/stories/dataset/dataset-action-buttons/DatasetActionButtons.stories.tsx b/src/stories/dataset/dataset-action-buttons/DatasetActionButtons.stories.tsx index 8d20e4a55..9bf11d545 100644 --- a/src/stories/dataset/dataset-action-buttons/DatasetActionButtons.stories.tsx +++ b/src/stories/dataset/dataset-action-buttons/DatasetActionButtons.stories.tsx @@ -11,7 +11,8 @@ import { import { WithLoggedInUser } from '../../WithLoggedInUser' import { DatasetMockRepository } from '../DatasetMockRepository' import { CollectionMockRepository } from '@/stories/collection/CollectionMockRepository' -import { ContactMockRepository } from '@/stories/shared/contact/ContactMockRepository' +import { ContactMockRepository } from '@/stories/shared-mock-repositories/contact/ContactMockRepository' +import { ExternalToolsMockRepository } from '@/stories/shared-mock-repositories/externalTools/ExternalToolsMockRepository' const meta: Meta = { title: 'Sections/Dataset Page/DatasetActionButtons', @@ -43,6 +44,7 @@ export const WithPublishPermissions: Story = { datasetRepository={new DatasetMockRepository()} collectionRepository={new CollectionMockRepository()} contactRepository={new ContactMockRepository()} + externalToolsRepository={new ExternalToolsMockRepository()} /> ) } @@ -63,6 +65,7 @@ export const WithNoDatasetPermissions: Story = { datasetRepository={new DatasetMockRepository()} collectionRepository={new CollectionMockRepository()} contactRepository={new ContactMockRepository()} + externalToolsRepository={new ExternalToolsMockRepository()} /> ) } @@ -87,6 +90,7 @@ export const WithUpdateAndNoPublishDatasetPermissions: Story = { datasetRepository={new DatasetMockRepository()} collectionRepository={new CollectionMockRepository()} contactRepository={new ContactMockRepository()} + externalToolsRepository={new ExternalToolsMockRepository()} /> ) } diff --git a/src/stories/dataset/dataset-action-buttons/access-dataset-menu/AccessDatasetMenu.stories.tsx b/src/stories/dataset/dataset-action-buttons/access-dataset-menu/AccessDatasetMenu.stories.tsx index 0721b6276..7b2b13769 100644 --- a/src/stories/dataset/dataset-action-buttons/access-dataset-menu/AccessDatasetMenu.stories.tsx +++ b/src/stories/dataset/dataset-action-buttons/access-dataset-menu/AccessDatasetMenu.stories.tsx @@ -8,6 +8,7 @@ import { DatasetVersionMother } from '../../../../../tests/component/dataset/domain/models/DatasetMother' import { AccessDatasetMenu } from '../../../../sections/dataset/dataset-action-buttons/access-dataset-menu/AccessDatasetMenu' +import { ExternalToolsMockRepository } from '@/stories/shared-mock-repositories/externalTools/ExternalToolsMockRepository' const meta: Meta = { title: 'Sections/Dataset Page/DatasetActionButtons/AccessDatasetMenu', @@ -31,6 +32,8 @@ export const WithDownloadNotAllowed: Story = { fileDownloadSizes={[DatasetFileDownloadSizeMother.createOriginal()]} downloadUrls={DatasetDownloadUrlsMother.create()} fileStore="s3" + externalToolsRepository={new ExternalToolsMockRepository()} + persistentId="doi:10.5072/FK2/ABCDEFGH" /> ) } @@ -43,6 +46,8 @@ export const WithoutTabularFiles: Story = { fileDownloadSizes={[DatasetFileDownloadSizeMother.createOriginal()]} downloadUrls={DatasetDownloadUrlsMother.create()} fileStore="s3" + externalToolsRepository={new ExternalToolsMockRepository()} + persistentId="doi:10.5072/FK2/ABCDEFGH" /> ) } @@ -58,6 +63,8 @@ export const WithTabularFiles: Story = { ]} downloadUrls={DatasetDownloadUrlsMother.create()} fileStore="s3" + externalToolsRepository={new ExternalToolsMockRepository()} + persistentId="doi:10.5072/FK2/ABCDEFGH" /> ) } diff --git a/src/stories/shared/contact/ContactMockRepository.ts b/src/stories/shared-mock-repositories/contact/ContactMockRepository.ts similarity index 100% rename from src/stories/shared/contact/ContactMockRepository.ts rename to src/stories/shared-mock-repositories/contact/ContactMockRepository.ts diff --git a/src/stories/shared-mock-repositories/externalTools/ExternalToolsMockRepository.ts b/src/stories/shared-mock-repositories/externalTools/ExternalToolsMockRepository.ts new file mode 100644 index 000000000..8b7ac3308 --- /dev/null +++ b/src/stories/shared-mock-repositories/externalTools/ExternalToolsMockRepository.ts @@ -0,0 +1,43 @@ +import { DatasetExternalToolResolved } from '@/externalTools/domain/models/DatasetExternalToolResolved' +import { ExternalTool } from '@/externalTools/domain/models/ExternalTool' +import { FileExternalToolResolved } from '@/externalTools/domain/models/FileExternalToolResolved' +import { ExternalToolsRepository } from '@/externalTools/domain/repositories/ExternalToolsRepository' +import { GetExternalToolDTO } from '@/externalTools/domain/useCases/DTOs/GetExternalToolDTO' +import { DatasetExternalToolResolvedMother } from '@tests/component/externalTools/domain/models/DatasetExternalToolResolvedMother' +import { ExternalToolsMother } from '@tests/component/externalTools/domain/models/ExternalToolsMother' +import { FileExternalToolResolvedMother } from '@tests/component/externalTools/domain/models/FileExternalToolResolvedMother' +import { FakerHelper } from '@tests/component/shared/FakerHelper' + +export class ExternalToolsMockRepository implements ExternalToolsRepository { + getExternalTools(): Promise { + return new Promise((resolve) => { + setTimeout(() => { + resolve(ExternalToolsMother.createList()) + }, FakerHelper.loadingTimout()) + }) + } + + getDatasetExternalToolResolved( + _datasetId: number | string, + _toolId: number, + _getExternalToolDTO: GetExternalToolDTO + ): Promise { + return new Promise((resolve) => { + setTimeout(() => { + resolve(DatasetExternalToolResolvedMother.create()) + }, FakerHelper.loadingTimout()) + }) + } + + getFileExternalToolResolved( + _fileId: number | string, + _toolId: number, + _getExternalToolDTO: GetExternalToolDTO + ): Promise { + return new Promise((resolve) => { + setTimeout(() => { + resolve(FileExternalToolResolvedMother.create()) + }, FakerHelper.loadingTimout()) + }) + } +} diff --git a/src/stories/shared/contact/contact-modal/ContactModal.stories.tsx b/src/stories/shared/contact-modal/ContactModal.stories.tsx similarity index 94% rename from src/stories/shared/contact/contact-modal/ContactModal.stories.tsx rename to src/stories/shared/contact-modal/ContactModal.stories.tsx index e323854ae..f08e6f03d 100644 --- a/src/stories/shared/contact/contact-modal/ContactModal.stories.tsx +++ b/src/stories/shared/contact-modal/ContactModal.stories.tsx @@ -1,5 +1,5 @@ import { Meta, StoryObj } from '@storybook/react' -import { WithI18next } from '../../../WithI18next' +import { WithI18next } from '../../WithI18next' import { ContactModal } from '@/sections/shared/contact/contact-modal/contact-modal' import { ContactRepository } from '@/contact/domain/repositories/ContactRepository' diff --git a/tests/component/externalTools/domain/models/DatasetExternalToolResolvedMother.ts b/tests/component/externalTools/domain/models/DatasetExternalToolResolvedMother.ts new file mode 100644 index 000000000..6162f1e56 --- /dev/null +++ b/tests/component/externalTools/domain/models/DatasetExternalToolResolvedMother.ts @@ -0,0 +1,13 @@ +import { DatasetExternalToolResolved } from '@/externalTools/domain/models/DatasetExternalToolResolved' + +export class DatasetExternalToolResolvedMother { + static create(props?: Partial): DatasetExternalToolResolved { + return { + displayName: 'Dataset Explore Tool', + datasetId: 1, + preview: false, + toolUrlResolved: 'http://localhost:3000/external-tool', + ...props + } + } +} diff --git a/tests/component/externalTools/domain/models/ExternalToolsMother.ts b/tests/component/externalTools/domain/models/ExternalToolsMother.ts new file mode 100644 index 000000000..669071bf8 --- /dev/null +++ b/tests/component/externalTools/domain/models/ExternalToolsMother.ts @@ -0,0 +1,42 @@ +import { ToolScope, ToolType } from '@/externalTools/domain/models/ExternalTool' +import { ExternalTool } from '@iqss/dataverse-client-javascript' + +export class ExternalToolsMother { + static createList(props?: Partial): ExternalTool[] { + return [ + this.createDatasetExploreTool(), + this.createFilePreviewTool(), + this.createFileExploreTool() + ].map((tool) => ({ ...tool, ...props })) + } + + static createDatasetExploreTool(): ExternalTool { + return { + id: 1, + displayName: 'Dataset Explore Tool', + description: 'Description for Dataset Explore Tool', + scope: ToolScope.Dataset, + types: [ToolType.Explore] + } + } + + static createFilePreviewTool(): ExternalTool { + return { + id: 2, + displayName: 'File Preview Tool', + description: 'Description for File Preview Tool', + scope: ToolScope.File, + types: [ToolType.Preview] + } + } + + static createFileExploreTool(): ExternalTool { + return { + id: 3, + displayName: 'File Explore Tool', + description: 'Description for File Explore Tool', + scope: ToolScope.File, + types: [ToolType.Explore] + } + } +} diff --git a/tests/component/externalTools/domain/models/FileExternalToolResolvedMother.ts b/tests/component/externalTools/domain/models/FileExternalToolResolvedMother.ts new file mode 100644 index 000000000..1ef610a9e --- /dev/null +++ b/tests/component/externalTools/domain/models/FileExternalToolResolvedMother.ts @@ -0,0 +1,13 @@ +import { FileExternalToolResolved } from '@/externalTools/domain/models/FileExternalToolResolved' + +export class FileExternalToolResolvedMother { + static create(props?: Partial): FileExternalToolResolved { + return { + displayName: 'File Explore Tool', + fileId: 1, + preview: false, + toolUrlResolved: 'http://localhost:3000/external-tool', + ...props + } + } +} diff --git a/tests/component/sections/dataset/Dataset.spec.tsx b/tests/component/sections/dataset/Dataset.spec.tsx index 4bcc74e49..f9a8795c1 100644 --- a/tests/component/sections/dataset/Dataset.spec.tsx +++ b/tests/component/sections/dataset/Dataset.spec.tsx @@ -1,7 +1,7 @@ import { DatasetRepository } from '../../../../src/dataset/domain/repositories/DatasetRepository' import { Dataset } from '../../../../src/sections/dataset/Dataset' import { DatasetMother } from '../../dataset/domain/models/DatasetMother' -import { LoadingProvider } from '../../../../src/sections/loading/LoadingProvider' +import { LoadingProvider } from '../../../../src/shared/contexts/loading/LoadingProvider' import { ANONYMIZED_FIELD_VALUE, MetadataBlockName @@ -25,6 +25,7 @@ import { DatasetVersionSummaryStringValues } from '@/dataset/domain/models/DatasetVersionSummaryInfo' import { ContactRepository } from '@/contact/domain/repositories/ContactRepository' +import { ExternalToolsRepository } from '@/externalTools/domain/repositories/ExternalToolsRepository' const setAnonymizedView = () => {} const fileRepository: FileRepository = {} as FileRepository @@ -32,6 +33,7 @@ const datasetRepository: DatasetRepository = {} as DatasetRepository const metadataBlockInfoRepository: MetadataBlockInfoRepository = {} as MetadataBlockInfoRepository const collectionRepository: CollectionRepository = {} as CollectionRepository const contactRepository = {} as ContactRepository +const externalToolsRepository = {} as ExternalToolsRepository const TOTAL_FILES_COUNT = 200 const allFiles = FilePreviewMother.createMany(TOTAL_FILES_COUNT) @@ -315,6 +317,7 @@ describe('Dataset', () => { metadataBlockInfoRepository={metadataBlockInfoRepository} collectionRepository={collectionRepository} contactRepository={contactRepository} + externalToolsRepository={externalToolsRepository} />, testDataset ) @@ -333,6 +336,7 @@ describe('Dataset', () => { metadataBlockInfoRepository={metadataBlockInfoRepository} collectionRepository={collectionRepository} contactRepository={contactRepository} + externalToolsRepository={externalToolsRepository} />, emptyDataset ) @@ -351,6 +355,7 @@ describe('Dataset', () => { metadataBlockInfoRepository={metadataBlockInfoRepository} collectionRepository={collectionRepository} contactRepository={contactRepository} + externalToolsRepository={externalToolsRepository} />, dataset ) @@ -366,6 +371,7 @@ describe('Dataset', () => { metadataBlockInfoRepository={metadataBlockInfoRepository} collectionRepository={collectionRepository} contactRepository={contactRepository} + externalToolsRepository={externalToolsRepository} />, testDataset ) @@ -382,6 +388,7 @@ describe('Dataset', () => { metadataBlockInfoRepository={metadataBlockInfoRepository} collectionRepository={collectionRepository} contactRepository={contactRepository} + externalToolsRepository={externalToolsRepository} />, testDataset ) @@ -401,6 +408,7 @@ describe('Dataset', () => { metadataBlockInfoRepository={metadataBlockInfoRepository} collectionRepository={collectionRepository} contactRepository={contactRepository} + externalToolsRepository={externalToolsRepository} />, testDataset ) @@ -423,6 +431,7 @@ describe('Dataset', () => { metadataBlockInfoRepository={metadataBlockInfoRepository} collectionRepository={collectionRepository} contactRepository={contactRepository} + externalToolsRepository={externalToolsRepository} />, testDataset ) @@ -445,6 +454,7 @@ describe('Dataset', () => { metadataBlockInfoRepository={metadataBlockInfoRepository} collectionRepository={collectionRepository} contactRepository={contactRepository} + externalToolsRepository={externalToolsRepository} />, testDataset ) @@ -468,6 +478,7 @@ describe('Dataset', () => { metadataBlockInfoRepository={metadataBlockInfoRepository} collectionRepository={collectionRepository} contactRepository={contactRepository} + externalToolsRepository={externalToolsRepository} />, testDataset ) @@ -489,6 +500,7 @@ describe('Dataset', () => { metadataBlockInfoRepository={metadataBlockInfoRepository} collectionRepository={collectionRepository} contactRepository={contactRepository} + externalToolsRepository={externalToolsRepository} />, testDataset ) @@ -510,6 +522,7 @@ describe('Dataset', () => { metadataBlockInfoRepository={metadataBlockInfoRepository} collectionRepository={collectionRepository} contactRepository={contactRepository} + externalToolsRepository={externalToolsRepository} />, testDatasetAnonymized ) @@ -527,6 +540,7 @@ describe('Dataset', () => { metadataBlockInfoRepository={metadataBlockInfoRepository} collectionRepository={collectionRepository} contactRepository={contactRepository} + externalToolsRepository={externalToolsRepository} />, testDataset ) @@ -543,6 +557,7 @@ describe('Dataset', () => { filesTabInfiniteScrollEnabled={true} collectionRepository={collectionRepository} contactRepository={contactRepository} + externalToolsRepository={externalToolsRepository} />, testDataset ) @@ -560,6 +575,7 @@ describe('Dataset', () => { metadataBlockInfoRepository={metadataBlockInfoRepository} collectionRepository={collectionRepository} contactRepository={contactRepository} + externalToolsRepository={externalToolsRepository} />, testDataset ) @@ -594,6 +610,7 @@ describe('Dataset', () => { metadataBlockInfoRepository={metadataBlockInfoRepository} collectionRepository={collectionRepository} contactRepository={contactRepository} + externalToolsRepository={externalToolsRepository} />, testDataset ) @@ -615,6 +632,7 @@ describe('Dataset', () => { metadataBlockInfoRepository={metadataBlockInfoRepository} collectionRepository={collectionRepository} contactRepository={contactRepository} + externalToolsRepository={externalToolsRepository} />, testDataset ) diff --git a/tests/component/sections/dataset/DatasetProvider.spec.tsx b/tests/component/sections/dataset/DatasetProvider.spec.tsx index 2e2cf2420..9e7d4962b 100644 --- a/tests/component/sections/dataset/DatasetProvider.spec.tsx +++ b/tests/component/sections/dataset/DatasetProvider.spec.tsx @@ -2,7 +2,7 @@ import { DatasetProvider } from '../../../../src/sections/dataset/DatasetProvide import { DatasetRepository } from '../../../../src/dataset/domain/repositories/DatasetRepository' import { DatasetMother } from '../../dataset/domain/models/DatasetMother' import { useDataset } from '../../../../src/sections/dataset/DatasetContext' -import { LoadingProvider } from '../../../../src/sections/loading/LoadingProvider' +import { LoadingProvider } from '../../../../src/shared/contexts/loading/LoadingProvider' function TestComponent() { const { dataset, isLoading } = useDataset() diff --git a/tests/component/sections/dataset/dataset-action-buttons/DatasetActionButtons.spec.tsx b/tests/component/sections/dataset/dataset-action-buttons/DatasetActionButtons.spec.tsx index daac142f0..c5689f8e5 100644 --- a/tests/component/sections/dataset/dataset-action-buttons/DatasetActionButtons.spec.tsx +++ b/tests/component/sections/dataset/dataset-action-buttons/DatasetActionButtons.spec.tsx @@ -9,10 +9,12 @@ import { DatasetVersionMother } from '../../../dataset/domain/models/DatasetMother' import { ContactRepository } from '@/contact/domain/repositories/ContactRepository' +import { ExternalToolsRepository } from '@/externalTools/domain/repositories/ExternalToolsRepository' const datasetRepository: DatasetRepository = {} as DatasetRepository const collectionRepository: CollectionRepository = {} as CollectionRepository const contactRepository: ContactRepository = {} as ContactRepository +const externalToolsRepository: ExternalToolsRepository = {} as ExternalToolsRepository describe('DatasetActionButtons', () => { it('renders the DatasetActionButtons with the Publish button', () => { @@ -30,6 +32,7 @@ describe('DatasetActionButtons', () => { datasetRepository={datasetRepository} collectionRepository={collectionRepository} contactRepository={contactRepository} + externalToolsRepository={externalToolsRepository} /> ) @@ -61,6 +64,7 @@ describe('DatasetActionButtons', () => { datasetRepository={datasetRepository} collectionRepository={collectionRepository} contactRepository={contactRepository} + externalToolsRepository={externalToolsRepository} /> ) diff --git a/tests/component/sections/dataset/dataset-action-buttons/access-dataset-menu/AccessDatasetMenu.spec.tsx b/tests/component/sections/dataset/dataset-action-buttons/access-dataset-menu/AccessDatasetMenu.spec.tsx index a05a1454d..6fd214117 100644 --- a/tests/component/sections/dataset/dataset-action-buttons/access-dataset-menu/AccessDatasetMenu.spec.tsx +++ b/tests/component/sections/dataset/dataset-action-buttons/access-dataset-menu/AccessDatasetMenu.spec.tsx @@ -6,8 +6,11 @@ import { DatasetVersionMother } from '../../../../dataset/domain/models/DatasetMother' import { FileSizeUnit } from '../../../../../../src/files/domain/models/FileMetadata' +import { ExternalToolsRepository } from '@/externalTools/domain/repositories/ExternalToolsRepository' const downloadUrls = DatasetDownloadUrlsMother.create() +const externalToolsRepository: ExternalToolsRepository = {} as ExternalToolsRepository + describe('AccessDatasetMenu', () => { it('renders the AccessDatasetMenu if the user has download files permissions and the dataset is not deaccessioned', () => { const version = DatasetVersionMother.createReleased() @@ -24,6 +27,8 @@ describe('AccessDatasetMenu', () => { permissions={permissions} downloadUrls={downloadUrls} fileStore="s3" + externalToolsRepository={externalToolsRepository} + persistentId="doi:10.5072/FK2/ABCDEFGH" /> ) @@ -48,6 +53,8 @@ describe('AccessDatasetMenu', () => { permissions={permissions} downloadUrls={downloadUrls} fileStore="s3" + externalToolsRepository={externalToolsRepository} + persistentId="doi:10.5072/FK2/ABCDEFGH" /> ) cy.findByRole('button', { name: 'Access Dataset' }).should('exist') @@ -68,6 +75,8 @@ describe('AccessDatasetMenu', () => { permissions={permissions} downloadUrls={downloadUrls} fileStore="s3" + externalToolsRepository={externalToolsRepository} + persistentId="doi:10.5072/FK2/ABCDEFGH" /> ) cy.findByRole('button', { name: 'Access Dataset' }).should('not.exist') @@ -88,6 +97,8 @@ describe('AccessDatasetMenu', () => { permissions={permissions} downloadUrls={downloadUrls} fileStore="s3" + externalToolsRepository={externalToolsRepository} + persistentId="doi:10.5072/FK2/ABCDEFGH" /> ) cy.findByRole('button', { name: 'Access Dataset' }).should('not.exist') @@ -107,6 +118,8 @@ describe('AccessDatasetMenu', () => { permissions={permissions} downloadUrls={downloadUrls} fileStore="s3" + externalToolsRepository={externalToolsRepository} + persistentId="doi:10.5072/FK2/ABCDEFGH" /> ) cy.findByRole('button', { name: 'Access Dataset' }).should('exist') @@ -134,6 +147,8 @@ describe('AccessDatasetMenu', () => { permissions={permissions} downloadUrls={downloadUrls} fileStore="s3" + externalToolsRepository={externalToolsRepository} + persistentId="doi:10.5072/FK2/ABCDEFGH" /> ) cy.findByRole('button', { name: 'Access Dataset' }).should('exist') @@ -164,6 +179,8 @@ describe('AccessDatasetMenu', () => { permissions={permissions} downloadUrls={downloadUrls} fileStore="s3" + externalToolsRepository={externalToolsRepository} + persistentId="doi:10.5072/FK2/ABCDEFGH" /> ) cy.findByRole('button', { name: 'Access Dataset' }).should('not.exist') @@ -184,6 +201,8 @@ describe('AccessDatasetMenu', () => { permissions={permissions} downloadUrls={downloadUrls} fileStore="not-s3" + externalToolsRepository={externalToolsRepository} + persistentId="doi:10.5072/FK2/ABCDEFGH" /> ) cy.findByRole('button', { name: 'Access Dataset' }).should('not.exist') diff --git a/tests/component/sections/dataset/dataset-action-buttons/access-dataset-menu/DatasetExploreOptions.spec.tsx b/tests/component/sections/dataset/dataset-action-buttons/access-dataset-menu/DatasetExploreOptions.spec.tsx new file mode 100644 index 000000000..5caf66643 --- /dev/null +++ b/tests/component/sections/dataset/dataset-action-buttons/access-dataset-menu/DatasetExploreOptions.spec.tsx @@ -0,0 +1,51 @@ +import { ExternalToolsRepository } from '@/externalTools/domain/repositories/ExternalToolsRepository' +import { DatasetExploreOptions } from '@/sections/dataset/dataset-action-buttons/access-dataset-menu/DatasetExploreOptions' +import { ExternalToolsProvider } from '@/shared/contexts/external-tools/ExternalToolsProvider' +import { DatasetExternalToolResolvedMother } from '@tests/component/externalTools/domain/models/DatasetExternalToolResolvedMother' + +const externalToolsRepository: ExternalToolsRepository = {} as ExternalToolsRepository + +describe('DatasetExploreOptions', () => { + it('renders nothing if there are no dataset explore tools', () => { + externalToolsRepository.getExternalTools = cy.stub().resolves([]) + cy.customMount( + + + + ) + cy.findByText('Explore Options').should('not.exist') + }) + + it('renders the explore options if there are dataset explore tools', () => { + cy.customMount( + + ) + + cy.contains('Explore Options').should('exist') + cy.contains('Dataset Explore Tool').should('exist') + }) + + it('calls the getDatasetExternalToolResolved use case when clicking on an explore option', () => { + externalToolsRepository.getDatasetExternalToolResolved = cy + .stub() + .as('getDatasetExternalToolResolved') + .resolves(DatasetExternalToolResolvedMother.create()) + + cy.customMount( + + ) + + cy.findByText('Dataset Explore Tool').click() + + cy.get('@getDatasetExternalToolResolved').should('have.been.calledOnce') + }) +}) diff --git a/tests/component/sections/edit-dataset-metadata/EditDatasetMetadata.spec.tsx b/tests/component/sections/edit-dataset-metadata/EditDatasetMetadata.spec.tsx index 641ef6b3a..a903875c2 100644 --- a/tests/component/sections/edit-dataset-metadata/EditDatasetMetadata.spec.tsx +++ b/tests/component/sections/edit-dataset-metadata/EditDatasetMetadata.spec.tsx @@ -1,7 +1,7 @@ import { ReactNode } from 'react' import { DatasetRepository } from '../../../../src/dataset/domain/repositories/DatasetRepository' import { DatasetMother } from '../../dataset/domain/models/DatasetMother' -import { LoadingProvider } from '../../../../src/sections/loading/LoadingProvider' +import { LoadingProvider } from '../../../../src/shared/contexts/loading/LoadingProvider' import { Dataset as DatasetModel } from '../../../../src/dataset/domain/models/Dataset' import { DatasetProvider } from '../../../../src/sections/dataset/DatasetProvider' import { MetadataBlockInfoMother } from '../../metadata-block-info/domain/models/MetadataBlockInfoMother' diff --git a/tests/component/sections/edit-file-metadata/EditFileMetadata.spec.tsx b/tests/component/sections/edit-file-metadata/EditFileMetadata.spec.tsx index 32d4c8bbc..32ad3ff82 100644 --- a/tests/component/sections/edit-file-metadata/EditFileMetadata.spec.tsx +++ b/tests/component/sections/edit-file-metadata/EditFileMetadata.spec.tsx @@ -2,7 +2,7 @@ import { EditFileMetadata, EditFileMetadataReferrer } from '@/sections/edit-file-metadata/EditFileMetadata' -import { LoadingProvider } from '../../../../src/sections/loading/LoadingProvider' +import { LoadingProvider } from '../../../../src/shared/contexts/loading/LoadingProvider' import { FileMother } from '@tests/component/files/domain/models/FileMother' import { FilePermissionsMother } from '@tests/component/files/domain/models/FilePermissionsMother' import { FileMockRepository } from '@/stories/file/FileMockRepository' diff --git a/tests/component/sections/loading/LoadingProvider.spec.tsx b/tests/component/sections/loading/LoadingProvider.spec.tsx index 189ed9a89..9c1e0f9d0 100644 --- a/tests/component/sections/loading/LoadingProvider.spec.tsx +++ b/tests/component/sections/loading/LoadingProvider.spec.tsx @@ -1,5 +1,5 @@ -import { LoadingProvider } from '../../../../src/sections/loading/LoadingProvider' -import { useLoading } from '../../../../src/sections/loading/LoadingContext' +import { LoadingProvider } from '../../../../src/shared/contexts/loading/LoadingProvider' +import { useLoading } from '../../../../src/shared/contexts/loading/LoadingContext' describe('LoadingProvider', () => { it('should render children', () => { diff --git a/tests/component/sections/replace-file/ReplaceFile.spec.tsx b/tests/component/sections/replace-file/ReplaceFile.spec.tsx index f4de39d98..56e9bf69f 100644 --- a/tests/component/sections/replace-file/ReplaceFile.spec.tsx +++ b/tests/component/sections/replace-file/ReplaceFile.spec.tsx @@ -4,7 +4,7 @@ import { FileMetadataMother, FileTypeMother } from '@tests/component/files/domain/models/FileMetadataMother' -import { LoadingProvider } from '../../../../src/sections/loading/LoadingProvider' +import { LoadingProvider } from '../../../../src/shared/contexts/loading/LoadingProvider' import { FileMockRepository } from '../../../../src/stories/file/FileMockRepository' const fileMockRepository = new FileMockRepository() diff --git a/tests/component/sections/upload-dataset-files/UploadDatasetFiles.spec.tsx b/tests/component/sections/upload-dataset-files/UploadDatasetFiles.spec.tsx index afd9fd29b..7540ca5c3 100644 --- a/tests/component/sections/upload-dataset-files/UploadDatasetFiles.spec.tsx +++ b/tests/component/sections/upload-dataset-files/UploadDatasetFiles.spec.tsx @@ -5,7 +5,7 @@ import { Dataset as DatasetModel } from '../../../../src/dataset/domain/models/D import { ReactNode } from 'react' import { DatasetProvider } from '../../../../src/sections/dataset/DatasetProvider' import { UploadDatasetFiles } from '../../../../src/sections/upload-dataset-files/UploadDatasetFiles' -import { LoadingProvider } from '../../../../src/sections/loading/LoadingProvider' +import { LoadingProvider } from '../../../../src/shared/contexts/loading/LoadingProvider' import { FileMockRepository } from '../../../../src/stories/file/FileMockRepository' const fileRepository: FileRepository = {} as FileRepository diff --git a/tests/support/commands.tsx b/tests/support/commands.tsx index e98f45176..d302cc2b0 100644 --- a/tests/support/commands.tsx +++ b/tests/support/commands.tsx @@ -50,6 +50,8 @@ import { SessionContext } from '@/sections/session/SessionContext' import { User } from '@/users/domain/models/User' import { OIDC_AUTH_CONFIG } from '@/config' import { ToastContainer } from 'react-toastify' +import { ExternalToolsProvider } from '@/shared/contexts/external-tools/ExternalToolsProvider' +import { ExternalToolsMockRepository } from '@/stories/shared-mock-repositories/externalTools/ExternalToolsMockRepository' // Define your custom mount function @@ -71,7 +73,9 @@ Cypress.Commands.add( return cy.mount( - + + + From 54c2c11d02ea410102eb3fe0427247d9b6df0f27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Tue, 26 Aug 2025 10:37:54 -0300 Subject: [PATCH 06/51] test: window open fails on github actions --- .../DatasetExploreOptions.spec.tsx | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/tests/component/sections/dataset/dataset-action-buttons/access-dataset-menu/DatasetExploreOptions.spec.tsx b/tests/component/sections/dataset/dataset-action-buttons/access-dataset-menu/DatasetExploreOptions.spec.tsx index 5caf66643..5889f1ac9 100644 --- a/tests/component/sections/dataset/dataset-action-buttons/access-dataset-menu/DatasetExploreOptions.spec.tsx +++ b/tests/component/sections/dataset/dataset-action-buttons/access-dataset-menu/DatasetExploreOptions.spec.tsx @@ -1,7 +1,6 @@ import { ExternalToolsRepository } from '@/externalTools/domain/repositories/ExternalToolsRepository' import { DatasetExploreOptions } from '@/sections/dataset/dataset-action-buttons/access-dataset-menu/DatasetExploreOptions' import { ExternalToolsProvider } from '@/shared/contexts/external-tools/ExternalToolsProvider' -import { DatasetExternalToolResolvedMother } from '@tests/component/externalTools/domain/models/DatasetExternalToolResolvedMother' const externalToolsRepository: ExternalToolsRepository = {} as ExternalToolsRepository @@ -30,22 +29,4 @@ describe('DatasetExploreOptions', () => { cy.contains('Explore Options').should('exist') cy.contains('Dataset Explore Tool').should('exist') }) - - it('calls the getDatasetExternalToolResolved use case when clicking on an explore option', () => { - externalToolsRepository.getDatasetExternalToolResolved = cy - .stub() - .as('getDatasetExternalToolResolved') - .resolves(DatasetExternalToolResolvedMother.create()) - - cy.customMount( - - ) - - cy.findByText('Dataset Explore Tool').click() - - cy.get('@getDatasetExternalToolResolved').should('have.been.calledOnce') - }) }) From 0e18505d17a9133b82213e638c75b20f268436a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Tue, 26 Aug 2025 10:38:02 -0300 Subject: [PATCH 07/51] fix: import --- .../top-bar-progress-indicator/TopBarProgressIndicator.spec.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/component/sections/layout/top-bar-progress-indicator/TopBarProgressIndicator.spec.tsx b/tests/component/sections/layout/top-bar-progress-indicator/TopBarProgressIndicator.spec.tsx index 9a15b7051..6ac27df41 100644 --- a/tests/component/sections/layout/top-bar-progress-indicator/TopBarProgressIndicator.spec.tsx +++ b/tests/component/sections/layout/top-bar-progress-indicator/TopBarProgressIndicator.spec.tsx @@ -1,5 +1,5 @@ import TopBarProgressIndicator from '../../../../../src/sections/layout/topbar-progress-indicator/TopbarProgressIndicator' -import { LoadingContext } from '../../../../../src/sections/loading/LoadingContext' +import { LoadingContext } from '../../../../../src/shared/contexts/loading/LoadingContext' describe('TopBarProgressIndicator', () => { it('should render without errors', () => { From 014b61a49f27d6447de1e3e2b9231109dcea81de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Wed, 27 Aug 2025 00:21:21 -0300 Subject: [PATCH 08/51] fix(DesignSystem): buttons in groups with tooltips --- .../assets/styles/bootstrap-customized.scss | 63 +++++++++++++++++-- .../button-group/ButtonGroup.module.scss | 9 ++- .../button-group/ButtonGroup.stories.tsx | 22 +++++++ 3 files changed, 89 insertions(+), 5 deletions(-) diff --git a/packages/design-system/src/lib/assets/styles/bootstrap-customized.scss b/packages/design-system/src/lib/assets/styles/bootstrap-customized.scss index 987045ab6..67d5fc5eb 100644 --- a/packages/design-system/src/lib/assets/styles/bootstrap-customized.scss +++ b/packages/design-system/src/lib/assets/styles/bootstrap-customized.scss @@ -128,17 +128,72 @@ th { vertical-align: middle; } -.btn-group > div:not(:last-child) > .btn-group > .btn, -.btn-group-vertical > div:not(:last-child) > .btn-group > .btn { +/* Start - Overrides to Fix Group Butttons with Buttons with Tooltips */ + +.btn-group > .btn, +.btn-group > div > .btn { + margin-left: -1px; +} + +.btn-group-vertical > .btn-group > .btn { + margin-left: 0; +} + +/* stylelint-disable */ +.btn-group-vertical > .btn, +.btn-group-vertical > div > .btn { + margin-top: -1px; +} + +.btn-group-vertical > .btn:not(:last-child, :first-child), +.btn-group-vertical > div:not(:last-child, :first-child, [role='group']) { + width: 100%; + border-radius: 0; +} + +.btn-group > div:not(:first-child) > .btn { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.btn-group > div:not(:last-child) > .btn { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.btn-group-vertical > div:not(:last-child, [role='group']), +.btn-group-vertical > div:not(:first-child, [role='group']), +.btn-group-vertical > div:not(:last-child, [role='group']) > .btn, +.btn-group-vertical > div:not(:first-child, [role='group']) > .btn { + width: 100%; +} + +.btn-group-vertical + > div:not(:first-child, :last-child, [role='group']) + > .btn:not(.dropdown-toggle) { + border-radius: 0; +} + +.btn-group-vertical > div:first-child:not([role='group']) > .btn:not(.dropdown-toggle) { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} + +.btn-group-vertical > div:last-child:not([role='group']) > .btn:not(.dropdown-toggle) { + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.btn-group > div:not(:last-child) > .btn-group > .btn { border-top-right-radius: 0; border-bottom-right-radius: 0; } -.btn-group > div:not(:first-child) > .btn-group > .btn, -.btn-group-vertical > div:not(:first-child) > .btn-group > .btn { +.btn-group > div:not(:first-child) > .btn-group > .btn { border-top-left-radius: 0; border-bottom-left-radius: 0; } +/* End - Overrides to Fix Group Butttons with Buttons with Tooltips */ .btn-link { text-decoration: none; diff --git a/packages/design-system/src/lib/components/button-group/ButtonGroup.module.scss b/packages/design-system/src/lib/components/button-group/ButtonGroup.module.scss index e54fd9cad..0f805ce49 100644 --- a/packages/design-system/src/lib/components/button-group/ButtonGroup.module.scss +++ b/packages/design-system/src/lib/components/button-group/ButtonGroup.module.scss @@ -1,6 +1,13 @@ @import 'src/lib/assets/styles/design-tokens/colors.module'; .border > button, -.border > [role='group'] > button { +.border > [role='group'] > button, +.border > button:not(.dropdown-item) { border: 1px solid $dv-button-border-color; } + +.border > div { + &:global > .btn { + border: 1px solid $dv-button-border-color; + } +} diff --git a/packages/design-system/src/lib/stories/button-group/ButtonGroup.stories.tsx b/packages/design-system/src/lib/stories/button-group/ButtonGroup.stories.tsx index 2fbd7422a..bf3c4e17e 100644 --- a/packages/design-system/src/lib/stories/button-group/ButtonGroup.stories.tsx +++ b/packages/design-system/src/lib/stories/button-group/ButtonGroup.stories.tsx @@ -51,6 +51,25 @@ export const VerticalButtonGroup: Story = { ) } +export const VerticalButtonGroupWithTooltips: Story = { + render: () => ( + + + + + + + + + + + + + + + ) +} + export const NestedButtonGroups: Story = { render: () => ( @@ -99,6 +118,9 @@ export const ButtonGroupWithTooltips: Story = { Item 2 + + + ) } From b4ef18051a11f5c02406c3403c2baa4fde193a0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Wed, 27 Aug 2025 00:37:08 -0300 Subject: [PATCH 09/51] fix: lint issues and unneeded button group --- public/locales/en/dataset.json | 2 +- public/locales/en/shared.json | 2 +- .../link-dataset-button/LinkDatasetButton.tsx | 10 ++++------ 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/public/locales/en/dataset.json b/public/locales/en/dataset.json index cd629de8e..890700f20 100644 --- a/public/locales/en/dataset.json +++ b/public/locales/en/dataset.json @@ -319,4 +319,4 @@ "contact": { "tip": "Use email button above to contact." } -} \ No newline at end of file +} diff --git a/public/locales/en/shared.json b/public/locales/en/shared.json index 69f1e816a..8bcbc0321 100644 --- a/public/locales/en/shared.json +++ b/public/locales/en/shared.json @@ -291,4 +291,4 @@ "truncateMoreTip": "Click to read the full {{contentName}}", "truncateLessTip": "Click to collapse the {{contentName}}" } -} \ No newline at end of file +} diff --git a/src/sections/dataset/dataset-action-buttons/link-dataset-button/LinkDatasetButton.tsx b/src/sections/dataset/dataset-action-buttons/link-dataset-button/LinkDatasetButton.tsx index 3c3ee4c03..e50cd4692 100644 --- a/src/sections/dataset/dataset-action-buttons/link-dataset-button/LinkDatasetButton.tsx +++ b/src/sections/dataset/dataset-action-buttons/link-dataset-button/LinkDatasetButton.tsx @@ -1,4 +1,4 @@ -import { Button, ButtonGroup } from '@iqss/dataverse-design-system' +import { Button } from '@iqss/dataverse-design-system' import { Dataset, DatasetPublishingStatus } from '../../../../dataset/domain/models/Dataset' import { useTranslation } from 'react-i18next' import { useSession } from '../../../session/SessionContext' @@ -25,10 +25,8 @@ export function LinkDatasetButton({ dataset }: LinkDatasetButtonProps) { } return ( - - - + ) } From bd03dc5917acd1904112931c4dbb09b24e1ac417 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Wed, 27 Aug 2025 01:19:24 -0300 Subject: [PATCH 10/51] feat: add file preview icon on dataset files --- package-lock.json | 8 ++--- package.json | 2 +- .../domain/models/ExternalTool.ts | 1 + .../DatasetExploreOptions.tsx | 1 - .../file-action-buttons/FileActionButtons.tsx | 36 ++++++++++++++++--- .../shared/link-to-page/LinkToPage.tsx | 10 ++++-- 6 files changed, 45 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index a7cf7db51..3871eced2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@dnd-kit/sortable": "8.0.0", "@dnd-kit/utilities": "3.2.2", "@faker-js/faker": "7.6.0", - "@iqss/dataverse-client-javascript": "2.0.0-pr353.5ed2e8a", + "@iqss/dataverse-client-javascript": "2.0.0-pr353.2435a25", "@iqss/dataverse-design-system": "*", "@istanbuljs/nyc-config-typescript": "1.0.2", "@tanstack/react-table": "8.9.2", @@ -3561,9 +3561,9 @@ }, "node_modules/@iqss/dataverse-client-javascript": { "name": "@IQSS/dataverse-client-javascript", - "version": "2.0.0-pr353.5ed2e8a", - "resolved": "https://npm.pkg.github.com/download/@IQSS/dataverse-client-javascript/2.0.0-pr353.5ed2e8a/61b6036f0c33d8dedcadd9873a7b745a41bb9f31", - "integrity": "sha512-rm1YsyDnG1rZUD2c2XUUIPxN5RikvAbY3UkdsEvBtTWIUcb2UhzHgwZGpxTLQiA2OVS6Hqhy2+J6VVWt6PXOOQ==", + "version": "2.0.0-pr353.2435a25", + "resolved": "https://npm.pkg.github.com/download/@IQSS/dataverse-client-javascript/2.0.0-pr353.2435a25/82f991d8437106760e39c6311ed61ed0e83f143b", + "integrity": "sha512-urewQErxe2CuLl+PTKo8IJDWFb2Hkgorscse8dAx3ITcEnt12L/R2yK8gkfAqNOOuEnNsjlGAtnnyKjcN8y6eg==", "license": "MIT", "dependencies": { "@types/node": "^18.15.11", diff --git a/package.json b/package.json index 22f0ea598..30494a144 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "@dnd-kit/sortable": "8.0.0", "@dnd-kit/utilities": "3.2.2", "@faker-js/faker": "7.6.0", - "@iqss/dataverse-client-javascript": "2.0.0-pr353.5ed2e8a", + "@iqss/dataverse-client-javascript": "2.0.0-pr353.2435a25", "@iqss/dataverse-design-system": "*", "@istanbuljs/nyc-config-typescript": "1.0.2", "@tanstack/react-table": "8.9.2", diff --git a/src/externalTools/domain/models/ExternalTool.ts b/src/externalTools/domain/models/ExternalTool.ts index beb9f4b6e..a9168c34a 100644 --- a/src/externalTools/domain/models/ExternalTool.ts +++ b/src/externalTools/domain/models/ExternalTool.ts @@ -4,6 +4,7 @@ export interface ExternalTool { description: string types: ToolType[] scope: ToolScope + contentType?: string // Only present when scope is 'file' } export enum ToolType { diff --git a/src/sections/dataset/dataset-action-buttons/access-dataset-menu/DatasetExploreOptions.tsx b/src/sections/dataset/dataset-action-buttons/access-dataset-menu/DatasetExploreOptions.tsx index 699f7867a..204869933 100644 --- a/src/sections/dataset/dataset-action-buttons/access-dataset-menu/DatasetExploreOptions.tsx +++ b/src/sections/dataset/dataset-action-buttons/access-dataset-menu/DatasetExploreOptions.tsx @@ -7,7 +7,6 @@ import { useExternalTools } from '@/shared/contexts/external-tools/ExternalTools import { getDatasetExternalToolResolved } from '@/externalTools/domain/useCases/GetDatasetExternalToolResolved' import { ExternalToolsRepository } from '@/externalTools/domain/repositories/ExternalToolsRepository' -// TODO:ME - Add File Preview icon on dataset files. // TODO:ME - Add File Explore options. interface DatasetExploreOptionsProps { diff --git a/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/FileActionButtons.tsx b/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/FileActionButtons.tsx index 4f2e66e3e..c925fc153 100644 --- a/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/FileActionButtons.tsx +++ b/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/FileActionButtons.tsx @@ -1,11 +1,17 @@ -import { AccessFileMenu } from '../../../../../../file/file-action-buttons/access-file-menu/AccessFileMenu' -import { FilePreview } from '../../../../../../../files/domain/models/FilePreview' -import { FileOptionsMenu } from './file-options-menu/FileOptionsMenu' -import { ButtonGroup } from '@iqss/dataverse-design-system' import { useTranslation } from 'react-i18next' -import { DatasetPublishingStatus } from '../../../../../../../dataset/domain/models/Dataset' +import { ButtonGroup, Tooltip, useTheme } from '@iqss/dataverse-design-system' +import { EyeFill } from 'react-bootstrap-icons' import { FileRepository } from '@/files/domain/repositories/FileRepository' import { DatasetRepository } from '@/dataset/domain/repositories/DatasetRepository' +import { useExternalTools } from '@/shared/contexts/external-tools/ExternalToolsProvider' +import { FilePreview } from '@/files/domain/models/FilePreview' +import { DatasetPublishingStatus } from '@/dataset/domain/models/Dataset' +import { ExternalTool } from '@/externalTools/domain/models/ExternalTool' +import { AccessFileMenu } from '@/sections/file/file-action-buttons/access-file-menu/AccessFileMenu' +import { FileOptionsMenu } from './file-options-menu/FileOptionsMenu' +import { LinkToPage } from '@/sections/shared/link-to-page/LinkToPage' +import { Route } from '@/sections/Route.enum' +import { DvObjectType } from '@/shared/hierarchy/domain/models/UpwardHierarchyNode' interface FileActionButtonsProps { file: FilePreview @@ -18,9 +24,29 @@ export function FileActionButtons({ datasetRepository }: FileActionButtonsProps) { const { t } = useTranslation('files') + const theme = useTheme() + const { externalTools } = useExternalTools() + const filePreviewExternalTool: ExternalTool[] = externalTools.filter( + (tool) => tool.contentType === file.metadata.type.value + ) return ( + {filePreviewExternalTool.length > 0 && ( + + + + + + )} + type: DvObjectType + className?: string } export function LinkToPage({ children, page, searchParams, - type + type, + className }: PropsWithChildren) { const searchParamsString: string = searchParams ? '?' + encodeSearchParamsToURI(searchParams) : '' @@ -21,7 +23,11 @@ export function LinkToPage({ return {children} } - return {children} + return ( + + {children} + + ) } const encodeSearchParamsToURI = (searchParams: Record) => { From 70da76dbfbfda154770dd425dc1f6a1d9dc4acca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Wed, 27 Aug 2025 09:57:51 -0300 Subject: [PATCH 11/51] feat: link and initial tabs logic --- public/locales/en/file.json | 5 +- src/sections/Route.enum.ts | 3 +- .../file-action-buttons/FileActionButtons.tsx | 13 +++-- src/sections/file/File.tsx | 51 ++++++++++++++++- src/sections/file/FileFactory.tsx | 4 ++ src/sections/file/FilePageHelper.ts | 56 +++++++++++++++++++ .../FileEmbededExternalTool.tsx | 40 +++++++++++++ .../external-tools/ExternalToolsProvider.tsx | 9 +++ 8 files changed, 170 insertions(+), 11 deletions(-) create mode 100644 src/sections/file/FilePageHelper.ts create mode 100644 src/sections/file/file-embeded-external-tool/FileEmbededExternalTool.tsx diff --git a/public/locales/en/file.json b/public/locales/en/file.json index 5582256f1..51b366204 100644 --- a/public/locales/en/file.json +++ b/public/locales/en/file.json @@ -1,7 +1,10 @@ { "tabs": { "metadata": "Metadata", - "versions": "Versions" + "versions": "Versions", + "preview": "Preview", + "query": "Query", + "fileTools": "File Tools" }, "fileVersion": { "version": "Dataset Version", diff --git a/src/sections/Route.enum.ts b/src/sections/Route.enum.ts index 2adbe8334..d7d64fb7a 100644 --- a/src/sections/Route.enum.ts +++ b/src/sections/Route.enum.ts @@ -81,5 +81,6 @@ export enum QueryParamKey { DATASET_VERSION = 'datasetVersion', REFERRER = 'referrer', AUTH_STATE = 'state', - VALID_TOKEN_BUT_NOT_LINKED_ACCOUNT = 'validTokenButNotLinkedAccount' + VALID_TOKEN_BUT_NOT_LINKED_ACCOUNT = 'validTokenButNotLinkedAccount', + TOOL_TYPE = 'toolType' } diff --git a/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/FileActionButtons.tsx b/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/FileActionButtons.tsx index c925fc153..c9fec86ea 100644 --- a/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/FileActionButtons.tsx +++ b/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/FileActionButtons.tsx @@ -6,11 +6,11 @@ import { DatasetRepository } from '@/dataset/domain/repositories/DatasetReposito import { useExternalTools } from '@/shared/contexts/external-tools/ExternalToolsProvider' import { FilePreview } from '@/files/domain/models/FilePreview' import { DatasetPublishingStatus } from '@/dataset/domain/models/Dataset' -import { ExternalTool } from '@/externalTools/domain/models/ExternalTool' +import { ExternalTool, ToolType } from '@/externalTools/domain/models/ExternalTool' import { AccessFileMenu } from '@/sections/file/file-action-buttons/access-file-menu/AccessFileMenu' import { FileOptionsMenu } from './file-options-menu/FileOptionsMenu' import { LinkToPage } from '@/sections/shared/link-to-page/LinkToPage' -import { Route } from '@/sections/Route.enum' +import { QueryParamKey, Route } from '@/sections/Route.enum' import { DvObjectType } from '@/shared/hierarchy/domain/models/UpwardHierarchyNode' interface FileActionButtonsProps { @@ -26,20 +26,21 @@ export function FileActionButtons({ const { t } = useTranslation('files') const theme = useTheme() const { externalTools } = useExternalTools() - const filePreviewExternalTool: ExternalTool[] = externalTools.filter( + const filePreviewExternalTools: ExternalTool[] = externalTools.filter( (tool) => tool.contentType === file.metadata.type.value ) return ( - {filePreviewExternalTool.length > 0 && ( - + {filePreviewExternalTools.length > 0 && ( + diff --git a/src/sections/file/File.tsx b/src/sections/file/File.tsx index c6c046fcf..4b8d02e22 100644 --- a/src/sections/file/File.tsx +++ b/src/sections/file/File.tsx @@ -3,7 +3,7 @@ import styles from './File.module.scss' import { ButtonGroup, Col, Row, Tabs } from '@iqss/dataverse-design-system' import { FileRepository } from '../../files/domain/repositories/FileRepository' import { useFile } from './useFile' -import { useEffect, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import { useLoading } from '../../shared/contexts/loading/LoadingContext' import { FileSkeleton } from './FileSkeleton' import { DatasetCitation } from '../dataset/dataset-citation/DatasetCitation' @@ -19,24 +19,59 @@ import { NotFoundPage } from '../not-found-page/NotFoundPage' import { DraftAlert } from './draft-alert/DraftAlert' import { FileVersions } from './file-version/FileVersions' import { DatasetRepository } from '@/dataset/domain/repositories/DatasetRepository' +import { useExternalTools } from '@/shared/contexts/external-tools/ExternalToolsProvider' +import { FilePageHelper } from './FilePageHelper' +import { FileEmbededExternalTool } from './file-embeded-external-tool/FileEmbededExternalTool' interface FileProps { repository: FileRepository datasetRepository: DatasetRepository id: number datasetVersionNumber?: string + toolTypeSelectedQueryParam?: string } -export function File({ repository, id, datasetVersionNumber, datasetRepository }: FileProps) { +export function File({ + repository, + id, + datasetVersionNumber, + datasetRepository, + toolTypeSelectedQueryParam +}: FileProps) { const { setIsLoading } = useLoading() const { t } = useTranslation('file') const { file, isLoading } = useFile(repository, id, datasetVersionNumber) + const { externalTools } = useExternalTools() const [activeTab, setActiveTab] = useState('metadata') + const fileAssociatedPreviewOrQueryTools = useMemo( + () => + FilePageHelper.getFileAssociatedPreviewOrQueryTools(externalTools, file?.metadata.type.value), + [externalTools, file] + ) + + const externalToolTabTitle: string = FilePageHelper.getExternalToolTabTitle( + fileAssociatedPreviewOrQueryTools, + t, + file?.metadata.type.value + ) + useEffect(() => { setIsLoading(isLoading) }, [isLoading, setIsLoading]) + // To change active tab to external tool in case the file has associated tools + useEffect(() => { + if (file) { + const defaultActiveTab = FilePageHelper.defineDefaultActiveTab( + externalTools, + file?.metadata.type.value + ) + + setActiveTab(defaultActiveTab) + } + }, [file, externalTools]) + if (isLoading) { return } @@ -124,7 +159,17 @@ export function File({ repository, id, datasetVersionNumber, datasetRepository } - + + {fileAssociatedPreviewOrQueryTools.length > 0 && ( + +
+ +
+
+ )}
@@ -31,6 +34,7 @@ function FileWithSearchParams() { id={id} datasetVersionNumber={datasetVersionNumber} datasetRepository={datasetRepository} + toolTypeSelectedQueryParam={toolTypeSelectedQueryParam} /> ) } diff --git a/src/sections/file/FilePageHelper.ts b/src/sections/file/FilePageHelper.ts new file mode 100644 index 000000000..72414bcbb --- /dev/null +++ b/src/sections/file/FilePageHelper.ts @@ -0,0 +1,56 @@ +import { ExternalTool, ToolScope, ToolType } from '@/externalTools/domain/models/ExternalTool' + +export class FilePageHelper { + static readonly EXT_TOOL_TAB_KEY = 'extTool' + + static defineDefaultActiveTab(externalTools: ExternalTool[], fileType?: string): string { + if (this.getFileAssociatedPreviewOrQueryTools(externalTools, fileType).length > 0) { + return this.EXT_TOOL_TAB_KEY + } + + return 'metadata' + } + + static getFileAssociatedPreviewOrQueryTools( + externalTools: ExternalTool[], + fileType?: string + ): ExternalTool[] { + return externalTools + .filter((tool) => tool.scope === ToolScope.File) + .filter( + (tool) => tool.types.includes(ToolType.Preview) || tool.types.includes(ToolType.Query) + ) + .filter((tool) => (fileType ? tool.contentType === fileType : false)) + } + + static getExternalToolTabTitle( + fileAssociatedPreviewOrQueryTools: ExternalTool[], + t: (key: string) => string, + fileType?: string + ): string { + // Only one tool associated and is a preview tool + if ( + fileAssociatedPreviewOrQueryTools.length === 1 && + fileAssociatedPreviewOrQueryTools[0].types.includes(ToolType.Preview) && + fileType === fileAssociatedPreviewOrQueryTools[0].contentType + ) { + return t('tabs.preview') + } + + // Only one tool associated and is a query tool + if ( + fileAssociatedPreviewOrQueryTools.length === 1 && + fileAssociatedPreviewOrQueryTools[0].types.includes(ToolType.Query) && + fileType === fileAssociatedPreviewOrQueryTools[0].contentType + ) { + return t('tabs.query') + } + + // More than one tool associated + if (fileAssociatedPreviewOrQueryTools.length > 1) { + return t('tabs.fileTools') + } + + return t('tabs.preview') + } +} diff --git a/src/sections/file/file-embeded-external-tool/FileEmbededExternalTool.tsx b/src/sections/file/file-embeded-external-tool/FileEmbededExternalTool.tsx new file mode 100644 index 000000000..f93fe8cf4 --- /dev/null +++ b/src/sections/file/file-embeded-external-tool/FileEmbededExternalTool.tsx @@ -0,0 +1,40 @@ +/** + * This component will render an embedded external tool if the file has one associated. + * This could be a "preview" or "query" tool type. + * If more than one tool is associated with the file, we show a dropdown to select which one to use. + * The tool resolved URL is fetched when the component is mounted or the tool selection changes. + * The tool is rendered in an iframe. + */ + +/* TODO:ME - If is in view and only first time then fetch resolved url and show iframe */ + +/* TODO:ME - Open in new window button */ + +import { ExternalTool } from '@/externalTools/domain/models/ExternalTool' +import { File } from '@/files/domain/models/File' + +interface FileEmbededExternalToolProps { + file: File + associdatedTools: ExternalTool[] +} + +export const FileEmbededExternalTool = ({ + file, + associdatedTools +}: FileEmbededExternalToolProps) => { + const moreThanOneTool = associdatedTools.length > 1 + + return ( +
+
+ Aca un dropdown para seleccionar la herramienta si hay más de una, deberia ser condicional. +
+ + +
+ ) +} diff --git a/src/shared/contexts/external-tools/ExternalToolsProvider.tsx b/src/shared/contexts/external-tools/ExternalToolsProvider.tsx index 778d47369..5e08f32c2 100644 --- a/src/shared/contexts/external-tools/ExternalToolsProvider.tsx +++ b/src/shared/contexts/external-tools/ExternalToolsProvider.tsx @@ -13,6 +13,7 @@ type ExternalToolsContextValue = { datasetExploreTools: ExternalTool[] fileExploreTools: ExternalTool[] filePreviewTools: ExternalTool[] + fileQueryTools: ExternalTool[] } const ExternalToolsContext = createContext(undefined) @@ -74,6 +75,12 @@ export function ExternalToolsProvider({ ) }, [externalTools]) + const fileQueryTools = useMemo(() => { + return externalTools.filter( + (tool) => tool.scope === ToolScope.File && tool.types.includes(ToolType.Query) + ) + }, [externalTools]) + const value = useMemo( () => ({ externalTools, @@ -82,6 +89,7 @@ export function ExternalToolsProvider({ datasetExploreTools, fileExploreTools, filePreviewTools, + fileQueryTools, refreshExternalTools: fetchExternalTools }), [ @@ -91,6 +99,7 @@ export function ExternalToolsProvider({ datasetExploreTools, fileExploreTools, filePreviewTools, + fileQueryTools, fetchExternalTools ] ) From f72f9fb252edcce2530cf19c87e67df64c2a50bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Wed, 27 Aug 2025 13:03:35 -0300 Subject: [PATCH 12/51] fix(DesignSystem): update button group styles for vertical alignment --- .../assets/styles/bootstrap-customized.scss | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/packages/design-system/src/lib/assets/styles/bootstrap-customized.scss b/packages/design-system/src/lib/assets/styles/bootstrap-customized.scss index 67d5fc5eb..ea128189e 100644 --- a/packages/design-system/src/lib/assets/styles/bootstrap-customized.scss +++ b/packages/design-system/src/lib/assets/styles/bootstrap-customized.scss @@ -135,7 +135,8 @@ th { margin-left: -1px; } -.btn-group-vertical > .btn-group > .btn { +.btn-group-vertical > .btn-group > .btn, +.btn-group-vertical > div:not([role='group']) > .btn-group > .btn { margin-left: 0; } @@ -146,8 +147,8 @@ th { } .btn-group-vertical > .btn:not(:last-child, :first-child), -.btn-group-vertical > div:not(:last-child, :first-child, [role='group']) { - width: 100%; +.btn-group-vertical > div:not(:last-child, :first-child, [role='group']), +.btn-group-vertical > div:not(:last-child, :first-child, [role='group']) > .btn-group { border-radius: 0; } @@ -161,25 +162,27 @@ th { border-bottom-right-radius: 0; } -.btn-group-vertical > div:not(:last-child, [role='group']), -.btn-group-vertical > div:not(:first-child, [role='group']), -.btn-group-vertical > div:not(:last-child, [role='group']) > .btn, -.btn-group-vertical > div:not(:first-child, [role='group']) > .btn { +.btn-group-vertical > div:not([role='group']), +.btn-group-vertical > div:not([role='group']) > .btn, +.btn-group-vertical > div:not([role='group']) > .btn-group { width: 100%; } .btn-group-vertical > div:not(:first-child, :last-child, [role='group']) - > .btn:not(.dropdown-toggle) { + > .btn:not(.dropdown-toggle), +.btn-group-vertical > div:not(:last-child, :first-child, [role='group']) > .btn-group > .btn { border-radius: 0; } -.btn-group-vertical > div:first-child:not([role='group']) > .btn:not(.dropdown-toggle) { +.btn-group-vertical > div:first-child:not([role='group']) > .btn:not(.dropdown-toggle), +.btn-group-vertical > div:first-child:not([role='group']) > .btn-group > .btn { border-bottom-right-radius: 0; border-bottom-left-radius: 0; } -.btn-group-vertical > div:last-child:not([role='group']) > .btn:not(.dropdown-toggle) { +.btn-group-vertical > div:last-child:not([role='group']) > .btn:not(.dropdown-toggle), +.btn-group-vertical > div:last-child:not([role='group']) > .btn-group > .btn { border-top-left-radius: 0; border-top-right-radius: 0; } From 9cdabe397f599635329326ac2be519e59fd711d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Wed, 27 Aug 2025 13:23:51 -0300 Subject: [PATCH 13/51] feat: query and preview buttons on dataset file actions --- .../file-action-buttons/FileActionButtons.tsx | 57 ++++++++++++------- 1 file changed, 36 insertions(+), 21 deletions(-) diff --git a/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/FileActionButtons.tsx b/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/FileActionButtons.tsx index c9fec86ea..018d87aee 100644 --- a/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/FileActionButtons.tsx +++ b/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/FileActionButtons.tsx @@ -1,6 +1,6 @@ import { useTranslation } from 'react-i18next' import { ButtonGroup, Tooltip, useTheme } from '@iqss/dataverse-design-system' -import { EyeFill } from 'react-bootstrap-icons' +import { EyeFill, Robot } from 'react-bootstrap-icons' import { FileRepository } from '@/files/domain/repositories/FileRepository' import { DatasetRepository } from '@/dataset/domain/repositories/DatasetRepository' import { useExternalTools } from '@/shared/contexts/external-tools/ExternalToolsProvider' @@ -12,6 +12,8 @@ import { FileOptionsMenu } from './file-options-menu/FileOptionsMenu' import { LinkToPage } from '@/sections/shared/link-to-page/LinkToPage' import { QueryParamKey, Route } from '@/sections/Route.enum' import { DvObjectType } from '@/shared/hierarchy/domain/models/UpwardHierarchyNode' +import { FilePageHelper } from '@/sections/file/FilePageHelper' +import { useMediaQuery } from '@/shared/hooks/useMediaQuery' interface FileActionButtonsProps { file: FilePreview @@ -25,29 +27,42 @@ export function FileActionButtons({ }: FileActionButtonsProps) { const { t } = useTranslation('files') const theme = useTheme() + const isBelow768px = useMediaQuery('(max-width: 768px)') const { externalTools } = useExternalTools() - const filePreviewExternalTools: ExternalTool[] = externalTools.filter( - (tool) => tool.contentType === file.metadata.type.value - ) + const fileAssociatedPreviewOrQueryTools: ExternalTool[] = + FilePageHelper.getFileAssociatedPreviewOrQueryTools(externalTools, file.metadata.type.value) - return ( - - {filePreviewExternalTools.length > 0 && ( - - - - - - )} + console.log(file) + return ( + + {fileAssociatedPreviewOrQueryTools.length > 0 && + fileAssociatedPreviewOrQueryTools.map((tool) => ( + + + {tool.types.includes(ToolType.Preview) && ( + + )} + {tool.types.includes(ToolType.Query) && ( + + )} + + + ))} Date: Wed, 27 Aug 2025 13:24:39 -0300 Subject: [PATCH 14/51] feat: improve responsive on mobile --- .../file-info-cell/FileInfoCell.module.scss | 16 ++++++++++++++++ .../file-thumbnail/FileThumbnail.module.scss | 4 ++++ .../file/file-preview/FileIcon.module.scss | 4 ++++ 3 files changed, 24 insertions(+) diff --git a/src/sections/dataset/dataset-files/files-table/file-info/file-info-cell/FileInfoCell.module.scss b/src/sections/dataset/dataset-files/files-table/file-info/file-info-cell/FileInfoCell.module.scss index 6159d4d95..a608f7dc6 100644 --- a/src/sections/dataset/dataset-files/files-table/file-info/file-info-cell/FileInfoCell.module.scss +++ b/src/sections/dataset/dataset-files/files-table/file-info/file-info-cell/FileInfoCell.module.scss @@ -8,15 +8,31 @@ .body-container { padding-left: 10px; + // Link title + > a { + @media (max-width: 576px) { + font-size: $dv-font-size-sm; + word-break: break-word; + } + } + &__subtext { color: $dv-subtext-color; font-size: $dv-font-size-sm; + + @media (max-width: 576px) { + word-break: break-word; + } } } .thumbnail-container { min-width: 80px; padding-left: 8px; + + @media (max-width: 576px) { + min-width: 30px; + } } .checksum-container { diff --git a/src/sections/dataset/dataset-files/files-table/file-info/file-info-cell/file-info-data/file-thumbnail/FileThumbnail.module.scss b/src/sections/dataset/dataset-files/files-table/file-info/file-info-cell/file-info-data/file-thumbnail/FileThumbnail.module.scss index eacbc5aa9..f985f3f1a 100644 --- a/src/sections/dataset/dataset-files/files-table/file-info/file-info-cell/file-info-data/file-thumbnail/FileThumbnail.module.scss +++ b/src/sections/dataset/dataset-files/files-table/file-info/file-info-cell/file-info-data/file-thumbnail/FileThumbnail.module.scss @@ -12,6 +12,10 @@ width: 55px; height: 80px; + + @media (max-width: 576px) { + width: 30px; + } } .tooltip { diff --git a/src/sections/file/file-preview/FileIcon.module.scss b/src/sections/file/file-preview/FileIcon.module.scss index 0cb812702..2c439fa05 100644 --- a/src/sections/file/file-preview/FileIcon.module.scss +++ b/src/sections/file/file-preview/FileIcon.module.scss @@ -8,4 +8,8 @@ .icon { color: $dv-subtext-color; font-size: 64px; + + @media (max-width: 576px) { + font-size: 24px; + } } From f979b8faa97b4dbe0b5f7c2d8597ecda8ad39991 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Wed, 27 Aug 2025 14:47:34 -0300 Subject: [PATCH 15/51] refactor: rename associated tools to applicable tools for clarity --- .../DatasetExploreOptions.tsx | 2 ++ .../file-action-buttons/FileActionButtons.tsx | 13 +++++----- src/sections/file/File.tsx | 16 +++++++----- src/sections/file/FilePageHelper.ts | 26 +++++++++---------- .../FileEmbededExternalTool.tsx | 13 ++++++---- 5 files changed, 40 insertions(+), 30 deletions(-) diff --git a/src/sections/dataset/dataset-action-buttons/access-dataset-menu/DatasetExploreOptions.tsx b/src/sections/dataset/dataset-action-buttons/access-dataset-menu/DatasetExploreOptions.tsx index 204869933..9ef175c5d 100644 --- a/src/sections/dataset/dataset-action-buttons/access-dataset-menu/DatasetExploreOptions.tsx +++ b/src/sections/dataset/dataset-action-buttons/access-dataset-menu/DatasetExploreOptions.tsx @@ -7,7 +7,9 @@ import { useExternalTools } from '@/shared/contexts/external-tools/ExternalTools import { getDatasetExternalToolResolved } from '@/externalTools/domain/useCases/GetDatasetExternalToolResolved' import { ExternalToolsRepository } from '@/externalTools/domain/repositories/ExternalToolsRepository' +// TODO:ME - Add File Previewer UI. // TODO:ME - Add File Explore options. +// TODO:ME - Add translations files. interface DatasetExploreOptionsProps { externalToolsRepository: ExternalToolsRepository diff --git a/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/FileActionButtons.tsx b/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/FileActionButtons.tsx index 018d87aee..f58e0e8ca 100644 --- a/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/FileActionButtons.tsx +++ b/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/FileActionButtons.tsx @@ -29,15 +29,16 @@ export function FileActionButtons({ const theme = useTheme() const isBelow768px = useMediaQuery('(max-width: 768px)') const { externalTools } = useExternalTools() - const fileAssociatedPreviewOrQueryTools: ExternalTool[] = - FilePageHelper.getFileAssociatedPreviewOrQueryTools(externalTools, file.metadata.type.value) - - console.log(file) + const fileApplicablePreviewOrQueryTools: ExternalTool[] = + FilePageHelper.getApplicablePreviewOrQueryToolsForFileType( + externalTools, + file.metadata.type.value + ) return ( - {fileAssociatedPreviewOrQueryTools.length > 0 && - fileAssociatedPreviewOrQueryTools.map((tool) => ( + {fileApplicablePreviewOrQueryTools.length > 0 && + fileApplicablePreviewOrQueryTools.map((tool) => ( ('metadata') - const fileAssociatedPreviewOrQueryTools = useMemo( + const fileApplicablePreviewOrQueryTools = useMemo( () => - FilePageHelper.getFileAssociatedPreviewOrQueryTools(externalTools, file?.metadata.type.value), + FilePageHelper.getApplicablePreviewOrQueryToolsForFileType( + externalTools, + file?.metadata.type.value + ), [externalTools, file] ) const externalToolTabTitle: string = FilePageHelper.getExternalToolTabTitle( - fileAssociatedPreviewOrQueryTools, + fileApplicablePreviewOrQueryTools, t, file?.metadata.type.value ) @@ -60,7 +63,7 @@ export function File({ setIsLoading(isLoading) }, [isLoading, setIsLoading]) - // To change active tab to external tool in case the file has associated tools + // To change active tab to external tool in case the file has applicable tools useEffect(() => { if (file) { const defaultActiveTab = FilePageHelper.defineDefaultActiveTab( @@ -160,12 +163,13 @@ export function File({ - {fileAssociatedPreviewOrQueryTools.length > 0 && ( + {fileApplicablePreviewOrQueryTools.length > 0 && (
diff --git a/src/sections/file/FilePageHelper.ts b/src/sections/file/FilePageHelper.ts index 72414bcbb..10c44a2ec 100644 --- a/src/sections/file/FilePageHelper.ts +++ b/src/sections/file/FilePageHelper.ts @@ -4,14 +4,14 @@ export class FilePageHelper { static readonly EXT_TOOL_TAB_KEY = 'extTool' static defineDefaultActiveTab(externalTools: ExternalTool[], fileType?: string): string { - if (this.getFileAssociatedPreviewOrQueryTools(externalTools, fileType).length > 0) { + if (this.getApplicablePreviewOrQueryToolsForFileType(externalTools, fileType).length > 0) { return this.EXT_TOOL_TAB_KEY } return 'metadata' } - static getFileAssociatedPreviewOrQueryTools( + static getApplicablePreviewOrQueryToolsForFileType( externalTools: ExternalTool[], fileType?: string ): ExternalTool[] { @@ -24,30 +24,30 @@ export class FilePageHelper { } static getExternalToolTabTitle( - fileAssociatedPreviewOrQueryTools: ExternalTool[], + fileApplicablePreviewOrQueryTools: ExternalTool[], t: (key: string) => string, fileType?: string ): string { - // Only one tool associated and is a preview tool + // Only one tool applicable and is a preview tool if ( - fileAssociatedPreviewOrQueryTools.length === 1 && - fileAssociatedPreviewOrQueryTools[0].types.includes(ToolType.Preview) && - fileType === fileAssociatedPreviewOrQueryTools[0].contentType + fileApplicablePreviewOrQueryTools.length === 1 && + fileApplicablePreviewOrQueryTools[0].types.includes(ToolType.Preview) && + fileType === fileApplicablePreviewOrQueryTools[0].contentType ) { return t('tabs.preview') } - // Only one tool associated and is a query tool + // Only one tool applicable and is a query tool if ( - fileAssociatedPreviewOrQueryTools.length === 1 && - fileAssociatedPreviewOrQueryTools[0].types.includes(ToolType.Query) && - fileType === fileAssociatedPreviewOrQueryTools[0].contentType + fileApplicablePreviewOrQueryTools.length === 1 && + fileApplicablePreviewOrQueryTools[0].types.includes(ToolType.Query) && + fileType === fileApplicablePreviewOrQueryTools[0].contentType ) { return t('tabs.query') } - // More than one tool associated - if (fileAssociatedPreviewOrQueryTools.length > 1) { + // More than one applicable tool + if (fileApplicablePreviewOrQueryTools.length > 1) { return t('tabs.fileTools') } diff --git a/src/sections/file/file-embeded-external-tool/FileEmbededExternalTool.tsx b/src/sections/file/file-embeded-external-tool/FileEmbededExternalTool.tsx index f93fe8cf4..3828c244c 100644 --- a/src/sections/file/file-embeded-external-tool/FileEmbededExternalTool.tsx +++ b/src/sections/file/file-embeded-external-tool/FileEmbededExternalTool.tsx @@ -1,7 +1,7 @@ /** - * This component will render an embedded external tool if the file has one associated. + * This component will render an embedded external tool if the file has one applicable. * This could be a "preview" or "query" tool type. - * If more than one tool is associated with the file, we show a dropdown to select which one to use. + * If more than one tool is applicable with the file, we show a dropdown to select which one to use. * The tool resolved URL is fetched when the component is mounted or the tool selection changes. * The tool is rendered in an iframe. */ @@ -15,14 +15,17 @@ import { File } from '@/files/domain/models/File' interface FileEmbededExternalToolProps { file: File - associdatedTools: ExternalTool[] + applicableTools: ExternalTool[] + toolTypeSelectedQueryParam: string | undefined } export const FileEmbededExternalTool = ({ file, - associdatedTools + applicableTools, + toolTypeSelectedQueryParam }: FileEmbededExternalToolProps) => { - const moreThanOneTool = associdatedTools.length > 1 + console.log({ applicableTools }) + const moreThanOneTool = applicableTools.length > 1 return (
From 9fe9a27146e5a9aa5d1a1a78504232d6f927c3a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Wed, 27 Aug 2025 17:10:49 -0300 Subject: [PATCH 16/51] feat(DesignSystem): Add size prop --- packages/design-system/CHANGELOG.md | 1 + .../dropdown-button/DropdownButton.tsx | 3 ++ .../DropdownButton.stories.tsx | 40 +++++++++++++++++++ 3 files changed, 44 insertions(+) diff --git a/packages/design-system/CHANGELOG.md b/packages/design-system/CHANGELOG.md index 30e620d7a..d19f17ad6 100644 --- a/packages/design-system/CHANGELOG.md +++ b/packages/design-system/CHANGELOG.md @@ -9,6 +9,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline - Add `customToggle` prop to allow custom toggle components. - Add `customToggleClassname` and `customToggleMenuClassname` props to allow custom styling of the custom toggle dropdown wrapper and menu. - Add `align` prop to control the alignment of the dropdown menu. + - Add `size` prop to control the size of the button (e.g., 'sm', 'lg'). - **DropdownButtonItem:** - Add `type` prop to allow specifying the type of the element. - **SelectAdvanced:** Fix word wrapping in options list to prevent overflow and ensure long text is displayed correctly. diff --git a/packages/design-system/src/lib/components/dropdown-button/DropdownButton.tsx b/packages/design-system/src/lib/components/dropdown-button/DropdownButton.tsx index 577346f10..b4e2bce2f 100644 --- a/packages/design-system/src/lib/components/dropdown-button/DropdownButton.tsx +++ b/packages/design-system/src/lib/components/dropdown-button/DropdownButton.tsx @@ -27,6 +27,7 @@ interface DropdownButtonProps { customToggleClassname?: string customToggleMenuClassname?: string align?: 'end' | 'start' + size?: 'sm' | 'lg' } export function DropdownButton({ @@ -43,6 +44,7 @@ export function DropdownButton({ customToggleClassname, customToggleMenuClassname, align, + size, children }: DropdownButtonProps) { // If customToggle is provided, use Dropdown instead of DropdownButtonBS @@ -58,6 +60,7 @@ export function DropdownButton({ return ( diff --git a/packages/design-system/src/lib/stories/dropdown-button/DropdownButton.stories.tsx b/packages/design-system/src/lib/stories/dropdown-button/DropdownButton.stories.tsx index 6d07033da..43464897f 100644 --- a/packages/design-system/src/lib/stories/dropdown-button/DropdownButton.stories.tsx +++ b/packages/design-system/src/lib/stories/dropdown-button/DropdownButton.stories.tsx @@ -7,6 +7,7 @@ import { CanvasFixedHeight } from '../CanvasFixedHeight' import { DropdownSeparator } from '../../components/dropdown-button/dropdown-separator/DropdownSeparator' import { DropdownHeader } from '../../components/dropdown-button/dropdown-header/DropdownHeader' import { Icon } from '../../components/icon/Icon' +import { Stack } from '../../components/stack/Stack' /** * ## Description @@ -237,3 +238,42 @@ export const WithCustomToggle: Story = { ) } + +export const AllSizesAtAGlance: Story = { + name: 'All sizes at a glance', + render: () => ( + + + + Item 1 + Item 2 + Item 3 + + + Item 1 + Item 2 + Item 3 + + + Item 1 + Item 2 + Item 3 + + + + ) +} From d0bc659662f76da0a7f6aaf3e62230a77ca9a9ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Wed, 27 Aug 2025 17:18:42 -0300 Subject: [PATCH 17/51] feat: initial tool switching --- src/sections/file/File.tsx | 7 +- src/sections/file/FileFactory.tsx | 6 +- src/sections/file/FilePageHelper.ts | 16 ++ .../FileEmbededExternalTool.tsx | 143 ++++++++++++++++-- 4 files changed, 159 insertions(+), 13 deletions(-) diff --git a/src/sections/file/File.tsx b/src/sections/file/File.tsx index fa2e16aa5..30461a4c9 100644 --- a/src/sections/file/File.tsx +++ b/src/sections/file/File.tsx @@ -2,6 +2,7 @@ import { useTranslation } from 'react-i18next' import styles from './File.module.scss' import { ButtonGroup, Col, Row, Tabs } from '@iqss/dataverse-design-system' import { FileRepository } from '../../files/domain/repositories/FileRepository' +import { ExternalToolsRepository } from '@/externalTools/domain/repositories/ExternalToolsRepository' import { useFile } from './useFile' import { useEffect, useMemo, useState } from 'react' import { useLoading } from '../../shared/contexts/loading/LoadingContext' @@ -27,6 +28,7 @@ interface FileProps { repository: FileRepository datasetRepository: DatasetRepository id: number + externalToolsRepository: ExternalToolsRepository datasetVersionNumber?: string toolTypeSelectedQueryParam?: string } @@ -36,6 +38,7 @@ export function File({ id, datasetVersionNumber, datasetRepository, + externalToolsRepository, toolTypeSelectedQueryParam }: FileProps) { const { setIsLoading } = useLoading() @@ -165,11 +168,13 @@ export function File({ {fileApplicablePreviewOrQueryTools.length > 0 && ( -
+
diff --git a/src/sections/file/FileFactory.tsx b/src/sections/file/FileFactory.tsx index 2d8bcc55c..3ba487693 100644 --- a/src/sections/file/FileFactory.tsx +++ b/src/sections/file/FileFactory.tsx @@ -1,14 +1,17 @@ import { ReactElement } from 'react' +import { useSearchParams } from 'react-router-dom' import { FileJSDataverseRepository } from '../../files/infrastructure/FileJSDataverseRepository' import { DatasetJSDataverseRepository } from '@/dataset/infrastructure/repositories/DatasetJSDataverseRepository' +import { ExternalToolsJSDataverseRepository } from '@/externalTools/infrastructure/repositories/ExternalToolsJSDataverseRepository' import { File } from './File' -import { useSearchParams } from 'react-router-dom' import { NotFoundPage } from '../not-found-page/NotFoundPage' import { searchParamVersionToDomainVersion } from '../../router' import { QueryParamKey } from '../Route.enum' const repository = new FileJSDataverseRepository() const datasetRepository = new DatasetJSDataverseRepository() +const externalToolsRepository = new ExternalToolsJSDataverseRepository() + export class FileFactory { static create(): ReactElement { return @@ -34,6 +37,7 @@ function FileWithSearchParams() { id={id} datasetVersionNumber={datasetVersionNumber} datasetRepository={datasetRepository} + externalToolsRepository={externalToolsRepository} toolTypeSelectedQueryParam={toolTypeSelectedQueryParam} /> ) diff --git a/src/sections/file/FilePageHelper.ts b/src/sections/file/FilePageHelper.ts index 10c44a2ec..726d57de1 100644 --- a/src/sections/file/FilePageHelper.ts +++ b/src/sections/file/FilePageHelper.ts @@ -53,4 +53,20 @@ export class FilePageHelper { return t('tabs.preview') } + + static getDefaultSelectedToolId( + toolTypeSelectedQueryParam: string | undefined, + applicableTools: ExternalTool[] + ): number { + if (toolTypeSelectedQueryParam) { + const matchedTool = applicableTools.find((tool) => + tool.types.includes(toolTypeSelectedQueryParam as ToolType) + ) + if (matchedTool) { + return matchedTool.id + } + } + // Fallback to the first applicable tool's id + return applicableTools[0].id + } } diff --git a/src/sections/file/file-embeded-external-tool/FileEmbededExternalTool.tsx b/src/sections/file/file-embeded-external-tool/FileEmbededExternalTool.tsx index 3828c244c..a3005ffaa 100644 --- a/src/sections/file/file-embeded-external-tool/FileEmbededExternalTool.tsx +++ b/src/sections/file/file-embeded-external-tool/FileEmbededExternalTool.tsx @@ -10,34 +10,155 @@ /* TODO:ME - Open in new window button */ +import { useEffect, useState } from 'react' import { ExternalTool } from '@/externalTools/domain/models/ExternalTool' import { File } from '@/files/domain/models/File' +import { DropdownButton, DropdownButtonItem, Spinner } from '@iqss/dataverse-design-system' +import { FileExternalToolResolved } from '@/externalTools/domain/models/FileExternalToolResolved' +import { ExternalToolsRepository } from '@/externalTools/domain/repositories/ExternalToolsRepository' +import { FilePageHelper } from '../FilePageHelper' +import { getFileExternalToolResolved } from '@/externalTools/domain/useCases/GetFileExternalToolResolved' +import { useTranslation } from 'react-i18next' interface FileEmbededExternalToolProps { file: File + isInView: boolean applicableTools: ExternalTool[] toolTypeSelectedQueryParam: string | undefined + externalToolsRepository: ExternalToolsRepository } export const FileEmbededExternalTool = ({ file, + isInView, applicableTools, - toolTypeSelectedQueryParam + toolTypeSelectedQueryParam, + externalToolsRepository }: FileEmbededExternalToolProps) => { - console.log({ applicableTools }) + const { t, i18n } = useTranslation('file') + const [toolIdSelected, setToolIdSelected] = useState( + FilePageHelper.getDefaultSelectedToolId(toolTypeSelectedQueryParam, applicableTools) + ) + const [isLoadingToolIframe, setIsLoadingToolIframe] = useState(true) + const [fileExternalToolsResolved, setFileExternalToolsResolved] = + useState(null) + const moreThanOneTool = applicableTools.length > 1 + const handleToolSelect = (eventKey: string | null) => { + setToolIdSelected(Number(eventKey)) + } + + const openInNewWindow = async () => { + // // If already opening, do nothing + // if (isOpening) return + // setIsOpening(true) + // const newWindow = window.open('', '_blank') + // // If the window didn't open, likely due to a popup blocker + // if (!newWindow || newWindow.closed || typeof newWindow.closed === 'undefined') { + // toast.info(tShared('allowPopups')) + // setIsOpening(false) + // newWindow?.close() + // return + // } + // try { + // // Set a temporary title on the new window while fetching the tool URL + // newWindow.document.title = `Loading ${toolDisplayName}...` + // const datasetExternalTool = await getDatasetExternalToolResolved( + // externalToolsRepository, + // persistentId, + // toolId, + // { preview: false, locale: i18n.language } + // ) + // // Change the location of the new window to the tool URL + // newWindow.location.href = datasetExternalTool.toolUrlResolved + // } catch (error) { + // // If there's an error, notify the user and close the new window + // toast.error(tShared('externalToolOpeningFailed')) + // if (!newWindow?.closed) newWindow.close() + // } finally { + // setIsOpening(false) + // } + } + + useEffect(() => { + if (isInView) { + const fetchFileExternalToolResolved = async () => { + setIsLoadingToolIframe(true) + setFileExternalToolsResolved(null) + + try { + const fileExternalTool = await getFileExternalToolResolved( + externalToolsRepository, + file.id, + toolIdSelected, + { preview: true, locale: i18n.language } + ) + + setFileExternalToolsResolved(fileExternalTool) + } catch (error) { + console.error('Error fetching tool resolved URL:', error) + } finally { + setIsLoadingToolIframe(false) + } + } + + void fetchFileExternalToolResolved() + console.log('Fetch tool resolved url for tool id:', toolIdSelected) + } + }, [isInView, toolIdSelected, externalToolsRepository, file.id, i18n.language]) + return (
-
- Aca un dropdown para seleccionar la herramienta si hay más de una, deberia ser condicional. -
- - +
+ {moreThanOneTool && ( +
+ + {applicableTools.map((tool) => ( + + {tool.displayName} + + ))} + +
+ )} + {/* If tool is preview */} + {/* {fileExternalToolsResolved && applicableTools.( +
+ {t('previewToolDescription', { toolName: fileExternalToolsResolved.toolDisplayName })} +
+ )} */} +
+ +
+ {fileExternalToolsResolved && ( + + )} + {isLoadingToolIframe && ( +
+ +
+ )} +
) } From 247c399e9336e00784a2cdef188daa4b579d70cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Thu, 28 Aug 2025 11:30:47 -0300 Subject: [PATCH 18/51] feat: loading iframe, error handling, flickering, etc --- public/locales/en/file.json | 5 +- src/sections/file/File.tsx | 4 +- src/sections/file/FilePageHelper.ts | 4 + .../FileEmbededExternalTool.module.scss | 46 ++++++ .../FileEmbededExternalTool.tsx | 143 ++++++++---------- 5 files changed, 120 insertions(+), 82 deletions(-) create mode 100644 src/sections/file/file-embeded-external-tool/FileEmbededExternalTool.module.scss diff --git a/public/locales/en/file.json b/public/locales/en/file.json index 51b366204..4dcd3facf 100644 --- a/public/locales/en/file.json +++ b/public/locales/en/file.json @@ -141,5 +141,8 @@ "requestAccessTooltipText": "If checked, users can request access to the restricted files in this dataset.", "termsOfAccessTooltipTex": "information on how and if users can access restricted files in this dataset." }, - "getCategoriesError": "Something went wrong fetching available categories. Try again later." + "getCategoriesError": "Something went wrong fetching available categories. Try again later.", + "fileEmbededExternalTool": { + "defaultLoadingToolError": "Something went wrong loading the external tool. Try again later." + } } diff --git a/src/sections/file/File.tsx b/src/sections/file/File.tsx index 30461a4c9..feecf689b 100644 --- a/src/sections/file/File.tsx +++ b/src/sections/file/File.tsx @@ -23,6 +23,7 @@ import { DatasetRepository } from '@/dataset/domain/repositories/DatasetReposito import { useExternalTools } from '@/shared/contexts/external-tools/ExternalToolsProvider' import { FilePageHelper } from './FilePageHelper' import { FileEmbededExternalTool } from './file-embeded-external-tool/FileEmbededExternalTool' +import { useScrollTop } from '@/shared/hooks/useScrollTop' interface FileProps { repository: FileRepository @@ -41,11 +42,12 @@ export function File({ externalToolsRepository, toolTypeSelectedQueryParam }: FileProps) { + useScrollTop() const { setIsLoading } = useLoading() const { t } = useTranslation('file') const { file, isLoading } = useFile(repository, id, datasetVersionNumber) const { externalTools } = useExternalTools() - const [activeTab, setActiveTab] = useState('metadata') + const [activeTab, setActiveTab] = useState() const fileApplicablePreviewOrQueryTools = useMemo( () => diff --git a/src/sections/file/FilePageHelper.ts b/src/sections/file/FilePageHelper.ts index 726d57de1..f8df9eb33 100644 --- a/src/sections/file/FilePageHelper.ts +++ b/src/sections/file/FilePageHelper.ts @@ -4,6 +4,10 @@ export class FilePageHelper { static readonly EXT_TOOL_TAB_KEY = 'extTool' static defineDefaultActiveTab(externalTools: ExternalTool[], fileType?: string): string { + if (externalTools.length === 0) { + return 'metadata' + } + if (this.getApplicablePreviewOrQueryToolsForFileType(externalTools, fileType).length > 0) { return this.EXT_TOOL_TAB_KEY } diff --git a/src/sections/file/file-embeded-external-tool/FileEmbededExternalTool.module.scss b/src/sections/file/file-embeded-external-tool/FileEmbededExternalTool.module.scss new file mode 100644 index 000000000..5435ac588 --- /dev/null +++ b/src/sections/file/file-embeded-external-tool/FileEmbededExternalTool.module.scss @@ -0,0 +1,46 @@ +.iframe-container { + position: relative; + aspect-ratio: 16 / 9; + padding-block: 1rem; + + .iframe { + width: 100%; + height: 100%; + border: none; + } + + .overlay { + position: absolute; + inset: 0; + top: 1rem; + z-index: 2; + background-color: #fff; + opacity: 1; + pointer-events: none; // Prevents blocking interactions with the iframe + } + + &.loaded { + .overlay { + animation: ext-tool-overlay-fade-out 200ms ease forwards; + animation-delay: 1.25s; // To keep the overlay for a bit longer after the iframe is loaded to mask any flickering + + // A11ty: avoid animations for user that do not prefer it + @media (prefers-reduced-motion: reduce) { + opacity: 0; + animation: none; + } + } + } + + &.error { + .overlay { + opacity: 0; + } + } + + @keyframes ext-tool-overlay-fade-out { + to { + opacity: 0; + } + } +} diff --git a/src/sections/file/file-embeded-external-tool/FileEmbededExternalTool.tsx b/src/sections/file/file-embeded-external-tool/FileEmbededExternalTool.tsx index a3005ffaa..800542dcc 100644 --- a/src/sections/file/file-embeded-external-tool/FileEmbededExternalTool.tsx +++ b/src/sections/file/file-embeded-external-tool/FileEmbededExternalTool.tsx @@ -11,14 +11,19 @@ /* TODO:ME - Open in new window button */ import { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import Skeleton from 'react-loading-skeleton' +import cn from 'classnames' +import { WriteError } from '@iqss/dataverse-client-javascript' +import { Alert, DropdownButton, DropdownButtonItem } from '@iqss/dataverse-design-system' import { ExternalTool } from '@/externalTools/domain/models/ExternalTool' import { File } from '@/files/domain/models/File' -import { DropdownButton, DropdownButtonItem, Spinner } from '@iqss/dataverse-design-system' import { FileExternalToolResolved } from '@/externalTools/domain/models/FileExternalToolResolved' import { ExternalToolsRepository } from '@/externalTools/domain/repositories/ExternalToolsRepository' -import { FilePageHelper } from '../FilePageHelper' import { getFileExternalToolResolved } from '@/externalTools/domain/useCases/GetFileExternalToolResolved' -import { useTranslation } from 'react-i18next' +import { JSDataverseWriteErrorHandler } from '@/shared/helpers/JSDataverseWriteErrorHandler' +import { FilePageHelper } from '../FilePageHelper' +import styles from './FileEmbededExternalTool.module.scss' interface FileEmbededExternalToolProps { file: File @@ -35,78 +40,57 @@ export const FileEmbededExternalTool = ({ toolTypeSelectedQueryParam, externalToolsRepository }: FileEmbededExternalToolProps) => { - const { t, i18n } = useTranslation('file') + const { t, i18n } = useTranslation('file', { keyPrefix: 'fileEmbededExternalTool' }) const [toolIdSelected, setToolIdSelected] = useState( FilePageHelper.getDefaultSelectedToolId(toolTypeSelectedQueryParam, applicableTools) ) - const [isLoadingToolIframe, setIsLoadingToolIframe] = useState(true) - const [fileExternalToolsResolved, setFileExternalToolsResolved] = + const [iframeLoaded, setIframeLoaded] = useState(false) + const [errorLoadingTool, setErrorLoadingTool] = useState(null) + const [fileExternalToolResolved, setFileExternalToolResolved] = useState(null) const moreThanOneTool = applicableTools.length > 1 - const handleToolSelect = (eventKey: string | null) => { - setToolIdSelected(Number(eventKey)) - } + const handleToolSelect = (eventKey: string | null) => setToolIdSelected(Number(eventKey)) - const openInNewWindow = async () => { - // // If already opening, do nothing - // if (isOpening) return - // setIsOpening(true) - // const newWindow = window.open('', '_blank') - // // If the window didn't open, likely due to a popup blocker - // if (!newWindow || newWindow.closed || typeof newWindow.closed === 'undefined') { - // toast.info(tShared('allowPopups')) - // setIsOpening(false) - // newWindow?.close() - // return - // } - // try { - // // Set a temporary title on the new window while fetching the tool URL - // newWindow.document.title = `Loading ${toolDisplayName}...` - // const datasetExternalTool = await getDatasetExternalToolResolved( - // externalToolsRepository, - // persistentId, - // toolId, - // { preview: false, locale: i18n.language } - // ) - // // Change the location of the new window to the tool URL - // newWindow.location.href = datasetExternalTool.toolUrlResolved - // } catch (error) { - // // If there's an error, notify the user and close the new window - // toast.error(tShared('externalToolOpeningFailed')) - // if (!newWindow?.closed) newWindow.close() - // } finally { - // setIsOpening(false) - // } + const handleOnLoadIframe = () => setIframeLoaded(true) + const handleOnErrorIframe = () => { + setIframeLoaded(false) + setErrorLoadingTool(t('defaultLoadingToolError')) } + // Loads the tool every time the tab is in view or the tool selection changes. useEffect(() => { - if (isInView) { - const fetchFileExternalToolResolved = async () => { - setIsLoadingToolIframe(true) - setFileExternalToolsResolved(null) + if (!isInView) return - try { - const fileExternalTool = await getFileExternalToolResolved( - externalToolsRepository, - file.id, - toolIdSelected, - { preview: true, locale: i18n.language } - ) + const fetchFileExternalToolResolved = async () => { + setIframeLoaded(false) + setErrorLoadingTool(null) + setFileExternalToolResolved(null) - setFileExternalToolsResolved(fileExternalTool) - } catch (error) { - console.error('Error fetching tool resolved URL:', error) - } finally { - setIsLoadingToolIframe(false) + try { + const fileExternalTool = await getFileExternalToolResolved( + externalToolsRepository, + file.id, + toolIdSelected, + { preview: true, locale: i18n.language } + ) + + setFileExternalToolResolved(fileExternalTool) + } catch (err: WriteError | unknown) { + if (err instanceof WriteError) { + const error = new JSDataverseWriteErrorHandler(err) + const formattedError = + error.getReasonWithoutStatusCode() ?? /* istanbul ignore next */ error.getErrorMessage() + setErrorLoadingTool(formattedError) + } else { + setErrorLoadingTool(t('defaultLoadingToolError')) } } - - void fetchFileExternalToolResolved() - console.log('Fetch tool resolved url for tool id:', toolIdSelected) } - }, [isInView, toolIdSelected, externalToolsRepository, file.id, i18n.language]) + + void fetchFileExternalToolResolved() + }, [isInView, toolIdSelected, externalToolsRepository, file.id, t, i18n.language]) return (
@@ -131,32 +115,31 @@ export const FileEmbededExternalTool = ({
)} - {/* If tool is preview */} - {/* {fileExternalToolsResolved && applicableTools.( -
- {t('previewToolDescription', { toolName: fileExternalToolsResolved.toolDisplayName })} -
- )} */}
-
- {fileExternalToolsResolved && ( +
+ {fileExternalToolResolved && ( )} - {isLoadingToolIframe && ( -
- -
+ {/* Keep skeleton overlay on top of the iframe while it loads to mask flickering */} +
+ +
+ {/* Show error message if the fetching the tool URL fails or the iframe somehow fails. */} + {errorLoadingTool && ( + + {errorLoadingTool} + )}
From e197185713c7d22c76d32a5d6480a4ee24596cb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Thu, 28 Aug 2025 14:27:26 -0300 Subject: [PATCH 19/51] feat: finish with file embeded tools --- public/locales/en/file.json | 3 +- src/sections/file/File.tsx | 4 +- src/sections/file/FilePageHelper.ts | 6 ++ .../FileEmbededExternalTool.module.scss | 8 ++ .../FileEmbededExternalTool.tsx | 76 +++++++++---------- 5 files changed, 57 insertions(+), 40 deletions(-) diff --git a/public/locales/en/file.json b/public/locales/en/file.json index 4dcd3facf..05113ab83 100644 --- a/public/locales/en/file.json +++ b/public/locales/en/file.json @@ -142,7 +142,8 @@ "termsOfAccessTooltipTex": "information on how and if users can access restricted files in this dataset." }, "getCategoriesError": "Something went wrong fetching available categories. Try again later.", - "fileEmbededExternalTool": { + "previewTab": { + "openInNewWindow": "Open in New Window", "defaultLoadingToolError": "Something went wrong loading the external tool. Try again later." } } diff --git a/src/sections/file/File.tsx b/src/sections/file/File.tsx index feecf689b..26949b7fb 100644 --- a/src/sections/file/File.tsx +++ b/src/sections/file/File.tsx @@ -47,7 +47,9 @@ export function File({ const { t } = useTranslation('file') const { file, isLoading } = useFile(repository, id, datasetVersionNumber) const { externalTools } = useExternalTools() - const [activeTab, setActiveTab] = useState() + const [activeTab, setActiveTab] = useState( + toolTypeSelectedQueryParam ? FilePageHelper.EXT_TOOL_TAB_KEY : 'metadata' + ) const fileApplicablePreviewOrQueryTools = useMemo( () => diff --git a/src/sections/file/FilePageHelper.ts b/src/sections/file/FilePageHelper.ts index f8df9eb33..df4698b68 100644 --- a/src/sections/file/FilePageHelper.ts +++ b/src/sections/file/FilePageHelper.ts @@ -73,4 +73,10 @@ export class FilePageHelper { // Fallback to the first applicable tool's id return applicableTools[0].id } + + static replacePreviewParamInToolUrl(url: string, preview: boolean): string { + const u = new URL(url, window.location.href) + u.searchParams.set('preview', String(preview)) + return u.toString() + } } diff --git a/src/sections/file/file-embeded-external-tool/FileEmbededExternalTool.module.scss b/src/sections/file/file-embeded-external-tool/FileEmbededExternalTool.module.scss index 5435ac588..eab2d88c6 100644 --- a/src/sections/file/file-embeded-external-tool/FileEmbededExternalTool.module.scss +++ b/src/sections/file/file-embeded-external-tool/FileEmbededExternalTool.module.scss @@ -1,3 +1,8 @@ +.header { + display: flex; + gap: 0.5rem; +} + .iframe-container { position: relative; aspect-ratio: 16 / 9; @@ -14,6 +19,9 @@ inset: 0; top: 1rem; z-index: 2; + display: flex; + justify-content: center; + padding-top: 3rem; background-color: #fff; opacity: 1; pointer-events: none; // Prevents blocking interactions with the iframe diff --git a/src/sections/file/file-embeded-external-tool/FileEmbededExternalTool.tsx b/src/sections/file/file-embeded-external-tool/FileEmbededExternalTool.tsx index 800542dcc..77b907857 100644 --- a/src/sections/file/file-embeded-external-tool/FileEmbededExternalTool.tsx +++ b/src/sections/file/file-embeded-external-tool/FileEmbededExternalTool.tsx @@ -1,21 +1,9 @@ -/** - * This component will render an embedded external tool if the file has one applicable. - * This could be a "preview" or "query" tool type. - * If more than one tool is applicable with the file, we show a dropdown to select which one to use. - * The tool resolved URL is fetched when the component is mounted or the tool selection changes. - * The tool is rendered in an iframe. - */ - -/* TODO:ME - If is in view and only first time then fetch resolved url and show iframe */ - -/* TODO:ME - Open in new window button */ - import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' -import Skeleton from 'react-loading-skeleton' import cn from 'classnames' import { WriteError } from '@iqss/dataverse-client-javascript' -import { Alert, DropdownButton, DropdownButtonItem } from '@iqss/dataverse-design-system' +import { Alert, DropdownButton, DropdownButtonItem, Spinner } from '@iqss/dataverse-design-system' +import { BoxArrowUpRight } from 'react-bootstrap-icons' import { ExternalTool } from '@/externalTools/domain/models/ExternalTool' import { File } from '@/files/domain/models/File' import { FileExternalToolResolved } from '@/externalTools/domain/models/FileExternalToolResolved' @@ -40,7 +28,7 @@ export const FileEmbededExternalTool = ({ toolTypeSelectedQueryParam, externalToolsRepository }: FileEmbededExternalToolProps) => { - const { t, i18n } = useTranslation('file', { keyPrefix: 'fileEmbededExternalTool' }) + const { t, i18n } = useTranslation('file', { keyPrefix: 'previewTab' }) const [toolIdSelected, setToolIdSelected] = useState( FilePageHelper.getDefaultSelectedToolId(toolTypeSelectedQueryParam, applicableTools) ) @@ -93,29 +81,41 @@ export const FileEmbededExternalTool = ({ }, [isInView, toolIdSelected, externalToolsRepository, file.id, t, i18n.language]) return ( -
-
+
+
{moreThanOneTool && ( -
- - {applicableTools.map((tool) => ( - - {tool.displayName} - - ))} - -
+ + {applicableTools.map((tool) => ( + + {tool.displayName} + + ))} + )} -
+ + {fileExternalToolResolved && ( + // eslint-disable-next-line react/jsx-no-target-blank + + + {t('openInNewWindow')} + + )} +
- +
{/* Show error message if the fetching the tool URL fails or the iframe somehow fails. */} {errorLoadingTool && ( @@ -142,6 +142,6 @@ export const FileEmbededExternalTool = ({ )}
-
+ ) } From 5f4378b0d1f6d24a39a7fca41d899203fcf14c10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Thu, 28 Aug 2025 16:11:30 -0300 Subject: [PATCH 20/51] Added file tool options in access file menu and simplify things --- public/locales/en/dataset.json | 3 +- public/locales/en/shared.json | 4 +- src/sections/dataset/Dataset.tsx | 4 - src/sections/dataset/DatasetFactory.tsx | 4 - .../DatasetActionButtons.tsx | 6 +- .../access-dataset-menu/AccessDatasetMenu.tsx | 10 +- .../DatasetExploreOptions.tsx | 24 ++-- src/sections/file/File.tsx | 4 - src/sections/file/FileFactory.tsx | 3 - .../access-file-menu/AccessFileMenu.tsx | 5 +- .../access-file-menu/FileToolOptions.tsx | 114 ++++++++++++++++++ .../FileEmbededExternalTool.tsx | 9 +- .../external-tools/ExternalToolsProvider.tsx | 7 +- src/stories/dataset/Dataset.stories.tsx | 10 -- .../DatasetActionButtons.stories.tsx | 4 - .../AccessDatasetMenu.stories.tsx | 4 - .../sections/dataset/Dataset.spec.tsx | 18 --- .../DatasetActionButtons.spec.tsx | 4 - .../AccessDatasetMenu.spec.tsx | 10 -- .../DatasetExploreOptions.spec.tsx | 12 +- 20 files changed, 146 insertions(+), 113 deletions(-) create mode 100644 src/sections/file/file-action-buttons/access-file-menu/FileToolOptions.tsx diff --git a/public/locales/en/dataset.json b/public/locales/en/dataset.json index 890700f20..d2bf605c1 100644 --- a/public/locales/en/dataset.json +++ b/public/locales/en/dataset.json @@ -69,8 +69,7 @@ "zip": "Download ZIP", "originalZip": "Original Format ZIP", "archivalZip": "Archival Format (.tab) ZIP" - }, - "exploreOptions": "Explore Options" + } }, "uploadFiles": "Upload Files", "share": { diff --git a/public/locales/en/shared.json b/public/locales/en/shared.json index 8bcbc0321..f5be029b6 100644 --- a/public/locales/en/shared.json +++ b/public/locales/en/shared.json @@ -290,5 +290,7 @@ "truncateLessBtn": "Collapse {{contentName}}", "truncateMoreTip": "Click to read the full {{contentName}}", "truncateLessTip": "Click to collapse the {{contentName}}" - } + }, + "exploreOptions": "Explore Options", + "queryOptions": "Query Options" } diff --git a/src/sections/dataset/Dataset.tsx b/src/sections/dataset/Dataset.tsx index a882043d8..151c2da49 100644 --- a/src/sections/dataset/Dataset.tsx +++ b/src/sections/dataset/Dataset.tsx @@ -31,7 +31,6 @@ import { DatasetVersions } from './dataset-versions/DatasetVersions' import { ContactRepository } from '@/contact/domain/repositories/ContactRepository' import { DatasetMetrics } from './dataset-metrics/DatasetMetrics' import { DatasetPublishingStatus } from '@/dataset/domain/models/Dataset' -import { ExternalToolsRepository } from '@/externalTools/domain/repositories/ExternalToolsRepository' interface DatasetProps { datasetRepository: DatasetRepository @@ -39,7 +38,6 @@ interface DatasetProps { metadataBlockInfoRepository: MetadataBlockInfoRepository collectionRepository: CollectionRepository contactRepository: ContactRepository - externalToolsRepository: ExternalToolsRepository filesTabInfiniteScrollEnabled?: boolean publishInProgress?: boolean tab?: string @@ -51,7 +49,6 @@ export function Dataset({ metadataBlockInfoRepository, collectionRepository, contactRepository, - externalToolsRepository, filesTabInfiniteScrollEnabled, publishInProgress, tab = 'files' @@ -151,7 +148,6 @@ export function Dataset({ collectionRepository={collectionRepository} dataset={dataset} contactRepository={contactRepository} - externalToolsRepository={externalToolsRepository} /> @@ -100,7 +97,6 @@ function DatasetWithSearchParams() { fileRepository={fileRepository} metadataBlockInfoRepository={metadataBlockInfoRepository} contactRepository={contactRepository} - externalToolsRepository={externalToolsRepository} publishInProgress={publishInProgress} filesTabInfiniteScrollEnabled={FILES_TAB_INFINITE_SCROLL_ENABLED} tab={tab} diff --git a/src/sections/dataset/dataset-action-buttons/DatasetActionButtons.tsx b/src/sections/dataset/dataset-action-buttons/DatasetActionButtons.tsx index 2207d5261..0df1104a7 100644 --- a/src/sections/dataset/dataset-action-buttons/DatasetActionButtons.tsx +++ b/src/sections/dataset/dataset-action-buttons/DatasetActionButtons.tsx @@ -11,7 +11,6 @@ import { LinkDatasetButton } from './link-dataset-button/LinkDatasetButton' import { ShareDatasetButton } from './share-dataset-button/ShareDatasetButton' import { ContactButton } from '@/sections/shared/contact/ContactButton' import { ContactRepository } from '@/contact/domain/repositories/ContactRepository' -import { ExternalToolsRepository } from '@/externalTools/domain/repositories/ExternalToolsRepository' import styles from './DatasetActionButtons.module.scss' interface DatasetActionButtonsProps { @@ -19,15 +18,13 @@ interface DatasetActionButtonsProps { datasetRepository: DatasetRepository collectionRepository: CollectionRepository contactRepository: ContactRepository - externalToolsRepository: ExternalToolsRepository } export function DatasetActionButtons({ dataset, datasetRepository, collectionRepository, - contactRepository, - externalToolsRepository + contactRepository }: DatasetActionButtonsProps) { const { t } = useTranslation('dataset') @@ -41,7 +38,6 @@ export function DatasetActionButtons({ downloadUrls={dataset.downloadUrls} fileStore={dataset.fileStore} persistentId={dataset.persistentId} - externalToolsRepository={externalToolsRepository} /> - + ) } diff --git a/src/sections/dataset/dataset-action-buttons/access-dataset-menu/DatasetExploreOptions.tsx b/src/sections/dataset/dataset-action-buttons/access-dataset-menu/DatasetExploreOptions.tsx index 9ef175c5d..eeec17e1e 100644 --- a/src/sections/dataset/dataset-action-buttons/access-dataset-menu/DatasetExploreOptions.tsx +++ b/src/sections/dataset/dataset-action-buttons/access-dataset-menu/DatasetExploreOptions.tsx @@ -7,28 +7,22 @@ import { useExternalTools } from '@/shared/contexts/external-tools/ExternalTools import { getDatasetExternalToolResolved } from '@/externalTools/domain/useCases/GetDatasetExternalToolResolved' import { ExternalToolsRepository } from '@/externalTools/domain/repositories/ExternalToolsRepository' -// TODO:ME - Add File Previewer UI. -// TODO:ME - Add File Explore options. // TODO:ME - Add translations files. interface DatasetExploreOptionsProps { - externalToolsRepository: ExternalToolsRepository persistentId: string } -export const DatasetExploreOptions = ({ - externalToolsRepository, - persistentId -}: DatasetExploreOptionsProps) => { - const { t } = useTranslation('dataset') - const { datasetExploreTools } = useExternalTools() +export const DatasetExploreOptions = ({ persistentId }: DatasetExploreOptionsProps) => { + const { t } = useTranslation('shared') + const { datasetExploreTools, externalToolsRepository } = useExternalTools() - if (datasetExploreTools.length === 0) return null + if (!datasetExploreTools || datasetExploreTools.length === 0) return null return ( <> - {t('datasetActionButtons.accessDataset.exploreOptions')} + {t('exploreOptions')} {datasetExploreTools.map((tool) => ( @@ -58,18 +52,19 @@ const ExploreOption = ({ externalToolsRepository }: ExploreOptionProps) => { const [isOpening, setIsOpening] = useState(false) - const { t: tShared, i18n } = useTranslation('shared') + const { t, i18n } = useTranslation('shared') const handleClick = async () => { // If already opening, do nothing if (isOpening) return setIsOpening(true) + // Open a blank window immediately to avoid popup blockers const newWindow = window.open('', '_blank') // If the window didn't open, likely due to a popup blocker if (!newWindow || newWindow.closed || typeof newWindow.closed === 'undefined') { - toast.info(tShared('allowPopups')) + toast.info(t('allowPopups')) setIsOpening(false) newWindow?.close() return @@ -85,11 +80,12 @@ const ExploreOption = ({ toolId, { preview: false, locale: i18n.language } ) + // Change the location of the new window to the tool URL newWindow.location.href = datasetExternalTool.toolUrlResolved } catch (error) { // If there's an error, notify the user and close the new window - toast.error(tShared('externalToolOpeningFailed')) + toast.error(t('externalToolOpeningFailed')) if (!newWindow?.closed) newWindow.close() } finally { setIsOpening(false) diff --git a/src/sections/file/File.tsx b/src/sections/file/File.tsx index 26949b7fb..3f80a42b5 100644 --- a/src/sections/file/File.tsx +++ b/src/sections/file/File.tsx @@ -2,7 +2,6 @@ import { useTranslation } from 'react-i18next' import styles from './File.module.scss' import { ButtonGroup, Col, Row, Tabs } from '@iqss/dataverse-design-system' import { FileRepository } from '../../files/domain/repositories/FileRepository' -import { ExternalToolsRepository } from '@/externalTools/domain/repositories/ExternalToolsRepository' import { useFile } from './useFile' import { useEffect, useMemo, useState } from 'react' import { useLoading } from '../../shared/contexts/loading/LoadingContext' @@ -29,7 +28,6 @@ interface FileProps { repository: FileRepository datasetRepository: DatasetRepository id: number - externalToolsRepository: ExternalToolsRepository datasetVersionNumber?: string toolTypeSelectedQueryParam?: string } @@ -39,7 +37,6 @@ export function File({ id, datasetVersionNumber, datasetRepository, - externalToolsRepository, toolTypeSelectedQueryParam }: FileProps) { useScrollTop() @@ -178,7 +175,6 @@ export function File({ applicableTools={fileApplicablePreviewOrQueryTools} toolTypeSelectedQueryParam={toolTypeSelectedQueryParam} isInView={activeTab === FilePageHelper.EXT_TOOL_TAB_KEY} - externalToolsRepository={externalToolsRepository} />
diff --git a/src/sections/file/FileFactory.tsx b/src/sections/file/FileFactory.tsx index 3ba487693..fa3a86674 100644 --- a/src/sections/file/FileFactory.tsx +++ b/src/sections/file/FileFactory.tsx @@ -2,7 +2,6 @@ import { ReactElement } from 'react' import { useSearchParams } from 'react-router-dom' import { FileJSDataverseRepository } from '../../files/infrastructure/FileJSDataverseRepository' import { DatasetJSDataverseRepository } from '@/dataset/infrastructure/repositories/DatasetJSDataverseRepository' -import { ExternalToolsJSDataverseRepository } from '@/externalTools/infrastructure/repositories/ExternalToolsJSDataverseRepository' import { File } from './File' import { NotFoundPage } from '../not-found-page/NotFoundPage' import { searchParamVersionToDomainVersion } from '../../router' @@ -10,7 +9,6 @@ import { QueryParamKey } from '../Route.enum' const repository = new FileJSDataverseRepository() const datasetRepository = new DatasetJSDataverseRepository() -const externalToolsRepository = new ExternalToolsJSDataverseRepository() export class FileFactory { static create(): ReactElement { @@ -37,7 +35,6 @@ function FileWithSearchParams() { id={id} datasetVersionNumber={datasetVersionNumber} datasetRepository={datasetRepository} - externalToolsRepository={externalToolsRepository} toolTypeSelectedQueryParam={toolTypeSelectedQueryParam} /> ) diff --git a/src/sections/file/file-action-buttons/access-file-menu/AccessFileMenu.tsx b/src/sections/file/file-action-buttons/access-file-menu/AccessFileMenu.tsx index 65153b184..c4966a532 100644 --- a/src/sections/file/file-action-buttons/access-file-menu/AccessFileMenu.tsx +++ b/src/sections/file/file-action-buttons/access-file-menu/AccessFileMenu.tsx @@ -1,3 +1,4 @@ +import { ReactElement } from 'react' import { Download, FileEarmark } from 'react-bootstrap-icons' import { AccessStatus } from './AccessStatus' import { RequestAccessOption } from './RequestAccessOption' @@ -6,7 +7,7 @@ import { useTranslation } from 'react-i18next' import { FileDownloadOptions } from './FileDownloadOptions' import { FileAccess } from '../../../../files/domain/models/FileAccess' import { FileMetadata } from '../../../../files/domain/models/FileMetadata' -import { ReactElement } from 'react' +import { FileExploreOptions, FileQueryOptions } from './FileToolOptions' interface FileActionButtonAccessFileProps { id: number @@ -82,6 +83,8 @@ export function AccessFileMenu({ isTabular={metadata.isTabular} userHasDownloadPermission={userHasDownloadPermission} /> + + ) diff --git a/src/sections/file/file-action-buttons/access-file-menu/FileToolOptions.tsx b/src/sections/file/file-action-buttons/access-file-menu/FileToolOptions.tsx new file mode 100644 index 000000000..a30b7de40 --- /dev/null +++ b/src/sections/file/file-action-buttons/access-file-menu/FileToolOptions.tsx @@ -0,0 +1,114 @@ +// FileToolOptions.tsx +import { useState } from 'react' +import { toast } from 'react-toastify' +import { useTranslation } from 'react-i18next' +import { BarChartFill as BarChartFillIcon } from 'react-bootstrap-icons' +import { DropdownButtonItem, DropdownHeader } from '@iqss/dataverse-design-system' +import { useExternalTools } from '@/shared/contexts/external-tools/ExternalToolsProvider' +import { getFileExternalToolResolved } from '@/externalTools/domain/useCases/GetFileExternalToolResolved' +import { ExternalToolsRepository } from '@/externalTools/domain/repositories/ExternalToolsRepository' + +type ToolKind = 'explore' | 'query' + +interface FileToolOptionsProps { + fileId: number + kind: ToolKind +} + +const FileToolOptions = ({ fileId, kind }: FileToolOptionsProps) => { + const { t } = useTranslation('shared') + const { fileExploreTools, fileQueryTools, externalToolsRepository } = useExternalTools() + + const tools = kind === 'explore' ? fileExploreTools : fileQueryTools + if (!tools || tools.length === 0) return null + + const headerLabel = kind === 'explore' ? t('exploreOptions') : t('queryOptions') + + return ( + <> + + {headerLabel} + + + + {tools.map((tool) => ( + + ))} + + ) +} + +interface ToolOptionProps { + toolId: number + toolDisplayName: string + fileId: number + externalToolsRepository: ExternalToolsRepository +} + +const ToolOption = ({ + toolId, + toolDisplayName, + fileId, + externalToolsRepository +}: ToolOptionProps) => { + const [isOpening, setIsOpening] = useState(false) + const { t, i18n } = useTranslation('shared') + + const handleClick = async () => { + // If already opening, do nothing + if (isOpening) return + setIsOpening(true) + + // Open a blank window immediately to avoid popup blockers + const newWindow = window.open('', '_blank') + + // If the window didn't open, likely due to a popup blocker + if (!newWindow || newWindow.closed || typeof newWindow.closed === 'undefined') { + toast.info(t('allowPopups')) + setIsOpening(false) + newWindow?.close() + return + } + + try { + // Set a temporary title on the new window while fetching the tool URL + newWindow.document.title = `Loading ${toolDisplayName}...` + + const fileExternalTool = await getFileExternalToolResolved( + externalToolsRepository, + fileId, + toolId, + { preview: false, locale: i18n.language } + ) + // Change the location of the new window to the tool URL + newWindow.location.href = fileExternalTool.toolUrlResolved + } catch (error) { + // If there's an error, notify the user and close the new window + toast.error(t('externalToolOpeningFailed')) + if (!newWindow?.closed) newWindow.close() + } finally { + setIsOpening(false) + } + } + + return ( + + {toolDisplayName} + + ) +} + +/** Wrappers for readability */ +export const FileExploreOptions = (props: Omit) => ( + +) + +export const FileQueryOptions = (props: Omit) => ( + +) diff --git a/src/sections/file/file-embeded-external-tool/FileEmbededExternalTool.tsx b/src/sections/file/file-embeded-external-tool/FileEmbededExternalTool.tsx index 77b907857..628ae6691 100644 --- a/src/sections/file/file-embeded-external-tool/FileEmbededExternalTool.tsx +++ b/src/sections/file/file-embeded-external-tool/FileEmbededExternalTool.tsx @@ -4,12 +4,12 @@ import cn from 'classnames' import { WriteError } from '@iqss/dataverse-client-javascript' import { Alert, DropdownButton, DropdownButtonItem, Spinner } from '@iqss/dataverse-design-system' import { BoxArrowUpRight } from 'react-bootstrap-icons' +import { useExternalTools } from '@/shared/contexts/external-tools/ExternalToolsProvider' import { ExternalTool } from '@/externalTools/domain/models/ExternalTool' -import { File } from '@/files/domain/models/File' import { FileExternalToolResolved } from '@/externalTools/domain/models/FileExternalToolResolved' -import { ExternalToolsRepository } from '@/externalTools/domain/repositories/ExternalToolsRepository' import { getFileExternalToolResolved } from '@/externalTools/domain/useCases/GetFileExternalToolResolved' import { JSDataverseWriteErrorHandler } from '@/shared/helpers/JSDataverseWriteErrorHandler' +import { File } from '@/files/domain/models/File' import { FilePageHelper } from '../FilePageHelper' import styles from './FileEmbededExternalTool.module.scss' @@ -18,17 +18,16 @@ interface FileEmbededExternalToolProps { isInView: boolean applicableTools: ExternalTool[] toolTypeSelectedQueryParam: string | undefined - externalToolsRepository: ExternalToolsRepository } export const FileEmbededExternalTool = ({ file, isInView, applicableTools, - toolTypeSelectedQueryParam, - externalToolsRepository + toolTypeSelectedQueryParam }: FileEmbededExternalToolProps) => { const { t, i18n } = useTranslation('file', { keyPrefix: 'previewTab' }) + const { externalToolsRepository } = useExternalTools() const [toolIdSelected, setToolIdSelected] = useState( FilePageHelper.getDefaultSelectedToolId(toolTypeSelectedQueryParam, applicableTools) ) diff --git a/src/shared/contexts/external-tools/ExternalToolsProvider.tsx b/src/shared/contexts/external-tools/ExternalToolsProvider.tsx index 5e08f32c2..6e05b8c16 100644 --- a/src/shared/contexts/external-tools/ExternalToolsProvider.tsx +++ b/src/shared/contexts/external-tools/ExternalToolsProvider.tsx @@ -14,6 +14,7 @@ type ExternalToolsContextValue = { fileExploreTools: ExternalTool[] filePreviewTools: ExternalTool[] fileQueryTools: ExternalTool[] + externalToolsRepository: ExternalToolsRepository } const ExternalToolsContext = createContext(undefined) @@ -90,7 +91,8 @@ export function ExternalToolsProvider({ fileExploreTools, filePreviewTools, fileQueryTools, - refreshExternalTools: fetchExternalTools + refreshExternalTools: fetchExternalTools, + externalToolsRepository }), [ externalTools, @@ -100,7 +102,8 @@ export function ExternalToolsProvider({ fileExploreTools, filePreviewTools, fileQueryTools, - fetchExternalTools + fetchExternalTools, + externalToolsRepository ] ) diff --git a/src/stories/dataset/Dataset.stories.tsx b/src/stories/dataset/Dataset.stories.tsx index 8b369e738..7c28f802a 100644 --- a/src/stories/dataset/Dataset.stories.tsx +++ b/src/stories/dataset/Dataset.stories.tsx @@ -19,7 +19,6 @@ import { MetadataBlockInfoMockRepository } from '../shared-mock-repositories/met import { DatasetMockRepository } from './DatasetMockRepository' import { CollectionMockRepository } from '@/stories/collection/CollectionMockRepository' import { ContactMockRepository } from '../shared-mock-repositories/contact/ContactMockRepository' -import { ExternalToolsMockRepository } from '../shared-mock-repositories/externalTools/ExternalToolsMockRepository' const meta: Meta = { title: 'Pages/Dataset', @@ -43,7 +42,6 @@ export const Default: Story = { fileRepository={new FileMockRepository()} metadataBlockInfoRepository={new MetadataBlockInfoMockRepository()} contactRepository={new ContactMockRepository()} - externalToolsRepository={new ExternalToolsMockRepository()} filesTabInfiniteScrollEnabled /> ) @@ -57,7 +55,6 @@ export const WithNormalPagination: Story = { fileRepository={new FileMockRepository()} metadataBlockInfoRepository={new MetadataBlockInfoMockRepository()} contactRepository={new ContactMockRepository()} - externalToolsRepository={new ExternalToolsMockRepository()} /> ) } @@ -71,7 +68,6 @@ export const DraftWithAllDatasetPermissions: Story = { fileRepository={new FileMockRepository()} metadataBlockInfoRepository={new MetadataBlockInfoMockRepository()} contactRepository={new ContactMockRepository()} - externalToolsRepository={new ExternalToolsMockRepository()} filesTabInfiniteScrollEnabled /> ) @@ -85,7 +81,6 @@ export const Deaccessioned: Story = { fileRepository={new FileMockRepository()} metadataBlockInfoRepository={new MetadataBlockInfoMockRepository()} contactRepository={new ContactMockRepository()} - externalToolsRepository={new ExternalToolsMockRepository()} filesTabInfiniteScrollEnabled /> ) @@ -99,7 +94,6 @@ export const LoggedInAsOwner: Story = { fileRepository={new FileMockRepository()} metadataBlockInfoRepository={new MetadataBlockInfoMockRepository()} contactRepository={new ContactMockRepository()} - externalToolsRepository={new ExternalToolsMockRepository()} filesTabInfiniteScrollEnabled /> ) @@ -114,7 +108,6 @@ export const Loading: Story = { fileRepository={new FileMockRepository()} metadataBlockInfoRepository={new MetadataBlockInfoMockRepository()} contactRepository={new ContactMockRepository()} - externalToolsRepository={new ExternalToolsMockRepository()} filesTabInfiniteScrollEnabled /> ) @@ -129,7 +122,6 @@ export const DatasetNotFound: Story = { fileRepository={new FileMockRepository()} metadataBlockInfoRepository={new MetadataBlockInfoMockRepository()} contactRepository={new ContactMockRepository()} - externalToolsRepository={new ExternalToolsMockRepository()} filesTabInfiniteScrollEnabled /> ) @@ -144,7 +136,6 @@ export const DatasetAnonymizedView: Story = { fileRepository={new FileMockRepository()} metadataBlockInfoRepository={new MetadataBlockInfoMockRepository()} contactRepository={new ContactMockRepository()} - externalToolsRepository={new ExternalToolsMockRepository()} filesTabInfiniteScrollEnabled /> ) @@ -159,7 +150,6 @@ export const DatasetWithNoFiles: Story = { fileRepository={new FileMockNoDataRepository()} metadataBlockInfoRepository={new MetadataBlockInfoMockRepository()} contactRepository={new ContactMockRepository()} - externalToolsRepository={new ExternalToolsMockRepository()} filesTabInfiniteScrollEnabled /> ) diff --git a/src/stories/dataset/dataset-action-buttons/DatasetActionButtons.stories.tsx b/src/stories/dataset/dataset-action-buttons/DatasetActionButtons.stories.tsx index 9bf11d545..cd68165a1 100644 --- a/src/stories/dataset/dataset-action-buttons/DatasetActionButtons.stories.tsx +++ b/src/stories/dataset/dataset-action-buttons/DatasetActionButtons.stories.tsx @@ -12,7 +12,6 @@ import { WithLoggedInUser } from '../../WithLoggedInUser' import { DatasetMockRepository } from '../DatasetMockRepository' import { CollectionMockRepository } from '@/stories/collection/CollectionMockRepository' import { ContactMockRepository } from '@/stories/shared-mock-repositories/contact/ContactMockRepository' -import { ExternalToolsMockRepository } from '@/stories/shared-mock-repositories/externalTools/ExternalToolsMockRepository' const meta: Meta = { title: 'Sections/Dataset Page/DatasetActionButtons', @@ -44,7 +43,6 @@ export const WithPublishPermissions: Story = { datasetRepository={new DatasetMockRepository()} collectionRepository={new CollectionMockRepository()} contactRepository={new ContactMockRepository()} - externalToolsRepository={new ExternalToolsMockRepository()} /> ) } @@ -65,7 +63,6 @@ export const WithNoDatasetPermissions: Story = { datasetRepository={new DatasetMockRepository()} collectionRepository={new CollectionMockRepository()} contactRepository={new ContactMockRepository()} - externalToolsRepository={new ExternalToolsMockRepository()} /> ) } @@ -90,7 +87,6 @@ export const WithUpdateAndNoPublishDatasetPermissions: Story = { datasetRepository={new DatasetMockRepository()} collectionRepository={new CollectionMockRepository()} contactRepository={new ContactMockRepository()} - externalToolsRepository={new ExternalToolsMockRepository()} /> ) } diff --git a/src/stories/dataset/dataset-action-buttons/access-dataset-menu/AccessDatasetMenu.stories.tsx b/src/stories/dataset/dataset-action-buttons/access-dataset-menu/AccessDatasetMenu.stories.tsx index 7b2b13769..42ee18524 100644 --- a/src/stories/dataset/dataset-action-buttons/access-dataset-menu/AccessDatasetMenu.stories.tsx +++ b/src/stories/dataset/dataset-action-buttons/access-dataset-menu/AccessDatasetMenu.stories.tsx @@ -8,7 +8,6 @@ import { DatasetVersionMother } from '../../../../../tests/component/dataset/domain/models/DatasetMother' import { AccessDatasetMenu } from '../../../../sections/dataset/dataset-action-buttons/access-dataset-menu/AccessDatasetMenu' -import { ExternalToolsMockRepository } from '@/stories/shared-mock-repositories/externalTools/ExternalToolsMockRepository' const meta: Meta = { title: 'Sections/Dataset Page/DatasetActionButtons/AccessDatasetMenu', @@ -32,7 +31,6 @@ export const WithDownloadNotAllowed: Story = { fileDownloadSizes={[DatasetFileDownloadSizeMother.createOriginal()]} downloadUrls={DatasetDownloadUrlsMother.create()} fileStore="s3" - externalToolsRepository={new ExternalToolsMockRepository()} persistentId="doi:10.5072/FK2/ABCDEFGH" /> ) @@ -46,7 +44,6 @@ export const WithoutTabularFiles: Story = { fileDownloadSizes={[DatasetFileDownloadSizeMother.createOriginal()]} downloadUrls={DatasetDownloadUrlsMother.create()} fileStore="s3" - externalToolsRepository={new ExternalToolsMockRepository()} persistentId="doi:10.5072/FK2/ABCDEFGH" /> ) @@ -63,7 +60,6 @@ export const WithTabularFiles: Story = { ]} downloadUrls={DatasetDownloadUrlsMother.create()} fileStore="s3" - externalToolsRepository={new ExternalToolsMockRepository()} persistentId="doi:10.5072/FK2/ABCDEFGH" /> ) diff --git a/tests/component/sections/dataset/Dataset.spec.tsx b/tests/component/sections/dataset/Dataset.spec.tsx index f9a8795c1..c50e8ab3c 100644 --- a/tests/component/sections/dataset/Dataset.spec.tsx +++ b/tests/component/sections/dataset/Dataset.spec.tsx @@ -25,7 +25,6 @@ import { DatasetVersionSummaryStringValues } from '@/dataset/domain/models/DatasetVersionSummaryInfo' import { ContactRepository } from '@/contact/domain/repositories/ContactRepository' -import { ExternalToolsRepository } from '@/externalTools/domain/repositories/ExternalToolsRepository' const setAnonymizedView = () => {} const fileRepository: FileRepository = {} as FileRepository @@ -33,7 +32,6 @@ const datasetRepository: DatasetRepository = {} as DatasetRepository const metadataBlockInfoRepository: MetadataBlockInfoRepository = {} as MetadataBlockInfoRepository const collectionRepository: CollectionRepository = {} as CollectionRepository const contactRepository = {} as ContactRepository -const externalToolsRepository = {} as ExternalToolsRepository const TOTAL_FILES_COUNT = 200 const allFiles = FilePreviewMother.createMany(TOTAL_FILES_COUNT) @@ -317,7 +315,6 @@ describe('Dataset', () => { metadataBlockInfoRepository={metadataBlockInfoRepository} collectionRepository={collectionRepository} contactRepository={contactRepository} - externalToolsRepository={externalToolsRepository} />, testDataset ) @@ -336,7 +333,6 @@ describe('Dataset', () => { metadataBlockInfoRepository={metadataBlockInfoRepository} collectionRepository={collectionRepository} contactRepository={contactRepository} - externalToolsRepository={externalToolsRepository} />, emptyDataset ) @@ -355,7 +351,6 @@ describe('Dataset', () => { metadataBlockInfoRepository={metadataBlockInfoRepository} collectionRepository={collectionRepository} contactRepository={contactRepository} - externalToolsRepository={externalToolsRepository} />, dataset ) @@ -371,7 +366,6 @@ describe('Dataset', () => { metadataBlockInfoRepository={metadataBlockInfoRepository} collectionRepository={collectionRepository} contactRepository={contactRepository} - externalToolsRepository={externalToolsRepository} />, testDataset ) @@ -388,7 +382,6 @@ describe('Dataset', () => { metadataBlockInfoRepository={metadataBlockInfoRepository} collectionRepository={collectionRepository} contactRepository={contactRepository} - externalToolsRepository={externalToolsRepository} />, testDataset ) @@ -408,7 +401,6 @@ describe('Dataset', () => { metadataBlockInfoRepository={metadataBlockInfoRepository} collectionRepository={collectionRepository} contactRepository={contactRepository} - externalToolsRepository={externalToolsRepository} />, testDataset ) @@ -431,7 +423,6 @@ describe('Dataset', () => { metadataBlockInfoRepository={metadataBlockInfoRepository} collectionRepository={collectionRepository} contactRepository={contactRepository} - externalToolsRepository={externalToolsRepository} />, testDataset ) @@ -454,7 +445,6 @@ describe('Dataset', () => { metadataBlockInfoRepository={metadataBlockInfoRepository} collectionRepository={collectionRepository} contactRepository={contactRepository} - externalToolsRepository={externalToolsRepository} />, testDataset ) @@ -478,7 +468,6 @@ describe('Dataset', () => { metadataBlockInfoRepository={metadataBlockInfoRepository} collectionRepository={collectionRepository} contactRepository={contactRepository} - externalToolsRepository={externalToolsRepository} />, testDataset ) @@ -500,7 +489,6 @@ describe('Dataset', () => { metadataBlockInfoRepository={metadataBlockInfoRepository} collectionRepository={collectionRepository} contactRepository={contactRepository} - externalToolsRepository={externalToolsRepository} />, testDataset ) @@ -522,7 +510,6 @@ describe('Dataset', () => { metadataBlockInfoRepository={metadataBlockInfoRepository} collectionRepository={collectionRepository} contactRepository={contactRepository} - externalToolsRepository={externalToolsRepository} />, testDatasetAnonymized ) @@ -540,7 +527,6 @@ describe('Dataset', () => { metadataBlockInfoRepository={metadataBlockInfoRepository} collectionRepository={collectionRepository} contactRepository={contactRepository} - externalToolsRepository={externalToolsRepository} />, testDataset ) @@ -557,7 +543,6 @@ describe('Dataset', () => { filesTabInfiniteScrollEnabled={true} collectionRepository={collectionRepository} contactRepository={contactRepository} - externalToolsRepository={externalToolsRepository} />, testDataset ) @@ -575,7 +560,6 @@ describe('Dataset', () => { metadataBlockInfoRepository={metadataBlockInfoRepository} collectionRepository={collectionRepository} contactRepository={contactRepository} - externalToolsRepository={externalToolsRepository} />, testDataset ) @@ -610,7 +594,6 @@ describe('Dataset', () => { metadataBlockInfoRepository={metadataBlockInfoRepository} collectionRepository={collectionRepository} contactRepository={contactRepository} - externalToolsRepository={externalToolsRepository} />, testDataset ) @@ -632,7 +615,6 @@ describe('Dataset', () => { metadataBlockInfoRepository={metadataBlockInfoRepository} collectionRepository={collectionRepository} contactRepository={contactRepository} - externalToolsRepository={externalToolsRepository} />, testDataset ) diff --git a/tests/component/sections/dataset/dataset-action-buttons/DatasetActionButtons.spec.tsx b/tests/component/sections/dataset/dataset-action-buttons/DatasetActionButtons.spec.tsx index c5689f8e5..daac142f0 100644 --- a/tests/component/sections/dataset/dataset-action-buttons/DatasetActionButtons.spec.tsx +++ b/tests/component/sections/dataset/dataset-action-buttons/DatasetActionButtons.spec.tsx @@ -9,12 +9,10 @@ import { DatasetVersionMother } from '../../../dataset/domain/models/DatasetMother' import { ContactRepository } from '@/contact/domain/repositories/ContactRepository' -import { ExternalToolsRepository } from '@/externalTools/domain/repositories/ExternalToolsRepository' const datasetRepository: DatasetRepository = {} as DatasetRepository const collectionRepository: CollectionRepository = {} as CollectionRepository const contactRepository: ContactRepository = {} as ContactRepository -const externalToolsRepository: ExternalToolsRepository = {} as ExternalToolsRepository describe('DatasetActionButtons', () => { it('renders the DatasetActionButtons with the Publish button', () => { @@ -32,7 +30,6 @@ describe('DatasetActionButtons', () => { datasetRepository={datasetRepository} collectionRepository={collectionRepository} contactRepository={contactRepository} - externalToolsRepository={externalToolsRepository} /> ) @@ -64,7 +61,6 @@ describe('DatasetActionButtons', () => { datasetRepository={datasetRepository} collectionRepository={collectionRepository} contactRepository={contactRepository} - externalToolsRepository={externalToolsRepository} /> ) diff --git a/tests/component/sections/dataset/dataset-action-buttons/access-dataset-menu/AccessDatasetMenu.spec.tsx b/tests/component/sections/dataset/dataset-action-buttons/access-dataset-menu/AccessDatasetMenu.spec.tsx index 6fd214117..e088fc0d8 100644 --- a/tests/component/sections/dataset/dataset-action-buttons/access-dataset-menu/AccessDatasetMenu.spec.tsx +++ b/tests/component/sections/dataset/dataset-action-buttons/access-dataset-menu/AccessDatasetMenu.spec.tsx @@ -6,10 +6,8 @@ import { DatasetVersionMother } from '../../../../dataset/domain/models/DatasetMother' import { FileSizeUnit } from '../../../../../../src/files/domain/models/FileMetadata' -import { ExternalToolsRepository } from '@/externalTools/domain/repositories/ExternalToolsRepository' const downloadUrls = DatasetDownloadUrlsMother.create() -const externalToolsRepository: ExternalToolsRepository = {} as ExternalToolsRepository describe('AccessDatasetMenu', () => { it('renders the AccessDatasetMenu if the user has download files permissions and the dataset is not deaccessioned', () => { @@ -27,7 +25,6 @@ describe('AccessDatasetMenu', () => { permissions={permissions} downloadUrls={downloadUrls} fileStore="s3" - externalToolsRepository={externalToolsRepository} persistentId="doi:10.5072/FK2/ABCDEFGH" /> ) @@ -53,7 +50,6 @@ describe('AccessDatasetMenu', () => { permissions={permissions} downloadUrls={downloadUrls} fileStore="s3" - externalToolsRepository={externalToolsRepository} persistentId="doi:10.5072/FK2/ABCDEFGH" /> ) @@ -75,7 +71,6 @@ describe('AccessDatasetMenu', () => { permissions={permissions} downloadUrls={downloadUrls} fileStore="s3" - externalToolsRepository={externalToolsRepository} persistentId="doi:10.5072/FK2/ABCDEFGH" /> ) @@ -97,7 +92,6 @@ describe('AccessDatasetMenu', () => { permissions={permissions} downloadUrls={downloadUrls} fileStore="s3" - externalToolsRepository={externalToolsRepository} persistentId="doi:10.5072/FK2/ABCDEFGH" /> ) @@ -118,7 +112,6 @@ describe('AccessDatasetMenu', () => { permissions={permissions} downloadUrls={downloadUrls} fileStore="s3" - externalToolsRepository={externalToolsRepository} persistentId="doi:10.5072/FK2/ABCDEFGH" /> ) @@ -147,7 +140,6 @@ describe('AccessDatasetMenu', () => { permissions={permissions} downloadUrls={downloadUrls} fileStore="s3" - externalToolsRepository={externalToolsRepository} persistentId="doi:10.5072/FK2/ABCDEFGH" /> ) @@ -179,7 +171,6 @@ describe('AccessDatasetMenu', () => { permissions={permissions} downloadUrls={downloadUrls} fileStore="s3" - externalToolsRepository={externalToolsRepository} persistentId="doi:10.5072/FK2/ABCDEFGH" /> ) @@ -201,7 +192,6 @@ describe('AccessDatasetMenu', () => { permissions={permissions} downloadUrls={downloadUrls} fileStore="not-s3" - externalToolsRepository={externalToolsRepository} persistentId="doi:10.5072/FK2/ABCDEFGH" /> ) diff --git a/tests/component/sections/dataset/dataset-action-buttons/access-dataset-menu/DatasetExploreOptions.spec.tsx b/tests/component/sections/dataset/dataset-action-buttons/access-dataset-menu/DatasetExploreOptions.spec.tsx index 5889f1ac9..374f68641 100644 --- a/tests/component/sections/dataset/dataset-action-buttons/access-dataset-menu/DatasetExploreOptions.spec.tsx +++ b/tests/component/sections/dataset/dataset-action-buttons/access-dataset-menu/DatasetExploreOptions.spec.tsx @@ -9,22 +9,14 @@ describe('DatasetExploreOptions', () => { externalToolsRepository.getExternalTools = cy.stub().resolves([]) cy.customMount( - + ) cy.findByText('Explore Options').should('not.exist') }) it('renders the explore options if there are dataset explore tools', () => { - cy.customMount( - - ) + cy.customMount() cy.contains('Explore Options').should('exist') cy.contains('Dataset Explore Tool').should('exist') From 3684d2cf46acf79281540affcc5e3c040541887a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Thu, 28 Aug 2025 17:16:37 -0300 Subject: [PATCH 21/51] feat: enhance file action buttons and access menu with permission checks --- .../file-action-buttons/FileActionButtons.tsx | 5 ++++- src/sections/file/File.tsx | 6 ++++-- .../file-action-buttons/access-file-menu/AccessFileMenu.tsx | 4 ++-- .../access-file-menu/FileToolOptions.tsx | 5 ++++- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/FileActionButtons.tsx b/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/FileActionButtons.tsx index f58e0e8ca..69e62b7fc 100644 --- a/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/FileActionButtons.tsx +++ b/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/FileActionButtons.tsx @@ -35,9 +35,12 @@ export function FileActionButtons({ file.metadata.type.value ) + const showExternalToolsButtons = + fileApplicablePreviewOrQueryTools.length > 0 && file.permissions.canDownloadFile + return ( - {fileApplicablePreviewOrQueryTools.length > 0 && + {showExternalToolsButtons && fileApplicablePreviewOrQueryTools.map((tool) => ( ( - toolTypeSelectedQueryParam ? FilePageHelper.EXT_TOOL_TAB_KEY : 'metadata' + toolTypeSelectedQueryParam && file?.permissions.canDownloadFile + ? FilePageHelper.EXT_TOOL_TAB_KEY + : 'metadata' ) const fileApplicablePreviewOrQueryTools = useMemo( @@ -167,7 +169,7 @@ export function File({ - {fileApplicablePreviewOrQueryTools.length > 0 && ( + {fileApplicablePreviewOrQueryTools.length > 0 && file.permissions.canDownloadFile && (
- - + + ) diff --git a/src/sections/file/file-action-buttons/access-file-menu/FileToolOptions.tsx b/src/sections/file/file-action-buttons/access-file-menu/FileToolOptions.tsx index a30b7de40..4d0f81119 100644 --- a/src/sections/file/file-action-buttons/access-file-menu/FileToolOptions.tsx +++ b/src/sections/file/file-action-buttons/access-file-menu/FileToolOptions.tsx @@ -13,15 +13,18 @@ type ToolKind = 'explore' | 'query' interface FileToolOptionsProps { fileId: number kind: ToolKind + userHasDownloadPermission: boolean } -const FileToolOptions = ({ fileId, kind }: FileToolOptionsProps) => { +const FileToolOptions = ({ fileId, kind, userHasDownloadPermission }: FileToolOptionsProps) => { const { t } = useTranslation('shared') const { fileExploreTools, fileQueryTools, externalToolsRepository } = useExternalTools() const tools = kind === 'explore' ? fileExploreTools : fileQueryTools if (!tools || tools.length === 0) return null + if (!userHasDownloadPermission) return null + const headerLabel = kind === 'explore' ? t('exploreOptions') : t('queryOptions') return ( From f5b5815baf3ef976195890638e717b7fd27901e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Thu, 28 Aug 2025 17:22:39 -0300 Subject: [PATCH 22/51] chore: tweaks --- .../domain/models/DatasetExternalToolResolved.ts | 2 +- src/externalTools/domain/models/FileExternalToolResolved.ts | 2 +- .../access-dataset-menu/DatasetExploreOptions.tsx | 2 -- .../file-embeded-external-tool/FileEmbededExternalTool.tsx | 4 ++-- .../contexts/external-tools/ExternalToolsProvider.tsx | 6 +++--- 5 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/externalTools/domain/models/DatasetExternalToolResolved.ts b/src/externalTools/domain/models/DatasetExternalToolResolved.ts index fabf2f698..b96007a05 100644 --- a/src/externalTools/domain/models/DatasetExternalToolResolved.ts +++ b/src/externalTools/domain/models/DatasetExternalToolResolved.ts @@ -1,5 +1,5 @@ export interface DatasetExternalToolResolved { - toolUrlResolved: string + toolUrlResolved: string // The URL to access the external tool. The URL includes necessary authentication tokens and parameters based on the user's permissions and the tool's configuration. displayName: string datasetId: number preview: boolean diff --git a/src/externalTools/domain/models/FileExternalToolResolved.ts b/src/externalTools/domain/models/FileExternalToolResolved.ts index 2fe2303d7..a205f64b3 100644 --- a/src/externalTools/domain/models/FileExternalToolResolved.ts +++ b/src/externalTools/domain/models/FileExternalToolResolved.ts @@ -1,5 +1,5 @@ export interface FileExternalToolResolved { - toolUrlResolved: string + toolUrlResolved: string // The URL to access the external tool. The URL includes necessary authentication tokens and parameters based on the user's permissions and the tool's configuration. displayName: string fileId: number preview: boolean diff --git a/src/sections/dataset/dataset-action-buttons/access-dataset-menu/DatasetExploreOptions.tsx b/src/sections/dataset/dataset-action-buttons/access-dataset-menu/DatasetExploreOptions.tsx index eeec17e1e..68adc0bac 100644 --- a/src/sections/dataset/dataset-action-buttons/access-dataset-menu/DatasetExploreOptions.tsx +++ b/src/sections/dataset/dataset-action-buttons/access-dataset-menu/DatasetExploreOptions.tsx @@ -7,8 +7,6 @@ import { useExternalTools } from '@/shared/contexts/external-tools/ExternalTools import { getDatasetExternalToolResolved } from '@/externalTools/domain/useCases/GetDatasetExternalToolResolved' import { ExternalToolsRepository } from '@/externalTools/domain/repositories/ExternalToolsRepository' -// TODO:ME - Add translations files. - interface DatasetExploreOptionsProps { persistentId: string } diff --git a/src/sections/file/file-embeded-external-tool/FileEmbededExternalTool.tsx b/src/sections/file/file-embeded-external-tool/FileEmbededExternalTool.tsx index 628ae6691..51f091a8f 100644 --- a/src/sections/file/file-embeded-external-tool/FileEmbededExternalTool.tsx +++ b/src/sections/file/file-embeded-external-tool/FileEmbededExternalTool.tsx @@ -130,11 +130,11 @@ export const FileEmbededExternalTool = ({ onError={handleOnErrorIframe} role="presentation"> )} - {/* Keep skeleton overlay on top of the iframe while it loads to mask flickering */} + {/* Keep overlay on top of the iframe while it loads to mask flickering */}
- {/* Show error message if the fetching the tool URL fails or the iframe somehow fails. */} + {/* Show error message if fetching the tool URL fails or the iframe somehow fails. */} {errorLoadingTool && ( {errorLoadingTool} diff --git a/src/shared/contexts/external-tools/ExternalToolsProvider.tsx b/src/shared/contexts/external-tools/ExternalToolsProvider.tsx index 6e05b8c16..760b91cca 100644 --- a/src/shared/contexts/external-tools/ExternalToolsProvider.tsx +++ b/src/shared/contexts/external-tools/ExternalToolsProvider.tsx @@ -8,7 +8,7 @@ import { JSDataverseReadErrorHandler } from '@/shared/helpers/JSDataverseReadErr type ExternalToolsContextValue = { externalTools: ExternalTool[] loading: boolean - error: unknown + error: string | null refreshExternalTools: () => Promise datasetExploreTools: ExternalTool[] fileExploreTools: ExternalTool[] @@ -30,11 +30,11 @@ export function ExternalToolsProvider({ }: ExternalToolsProviderProps) { const [externalTools, setExternalTools] = useState([]) const [loading, setLoading] = useState(true) - const [error, setError] = useState(undefined) + const [error, setError] = useState(null) const fetchExternalTools = useCallback(async () => { setLoading(true) - setError(undefined) + setError(null) try { const data = await getExternalTools(externalToolsRepository) From 1ad25a5a48e89a9102c659c295ce7b1a78a20065 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Fri, 29 Aug 2025 08:45:09 -0300 Subject: [PATCH 23/51] test: cases for file tools in dataset table files rows --- .../access-dataset-menu/AccessDatasetMenu.tsx | 4 +- .../file-action-buttons/FileActionButtons.tsx | 48 +------------ .../file-action-buttons/FileTools.tsx | 61 ++++++++++++++++ .../domain/models/ExternalToolsMother.ts | 17 ++++- .../FileActionButtons.spec.tsx | 15 +++- .../file-action-buttons/FileTools.spec.tsx | 71 +++++++++++++++++++ 6 files changed, 165 insertions(+), 51 deletions(-) create mode 100644 src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/FileTools.tsx create mode 100644 tests/component/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/FileTools.spec.tsx diff --git a/src/sections/dataset/dataset-action-buttons/access-dataset-menu/AccessDatasetMenu.tsx b/src/sections/dataset/dataset-action-buttons/access-dataset-menu/AccessDatasetMenu.tsx index bae8f76b1..fb8a4ee3f 100644 --- a/src/sections/dataset/dataset-action-buttons/access-dataset-menu/AccessDatasetMenu.tsx +++ b/src/sections/dataset/dataset-action-buttons/access-dataset-menu/AccessDatasetMenu.tsx @@ -10,6 +10,8 @@ import { FileDownloadSize, FileDownloadMode } from '../../../../files/domain/mod import { Download as DownloadIcon } from 'react-bootstrap-icons' import { DatasetExploreOptions } from './DatasetExploreOptions' +// TODO: add compute feature + interface AccessDatasetMenuProps { version: DatasetVersion permissions: DatasetPermissions @@ -107,5 +109,3 @@ const DatasetDownloadOptions = ({ ) } - -// TODO: add compute feature diff --git a/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/FileActionButtons.tsx b/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/FileActionButtons.tsx index 69e62b7fc..1533f19de 100644 --- a/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/FileActionButtons.tsx +++ b/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/FileActionButtons.tsx @@ -1,19 +1,13 @@ import { useTranslation } from 'react-i18next' -import { ButtonGroup, Tooltip, useTheme } from '@iqss/dataverse-design-system' -import { EyeFill, Robot } from 'react-bootstrap-icons' +import { ButtonGroup } from '@iqss/dataverse-design-system' import { FileRepository } from '@/files/domain/repositories/FileRepository' import { DatasetRepository } from '@/dataset/domain/repositories/DatasetRepository' -import { useExternalTools } from '@/shared/contexts/external-tools/ExternalToolsProvider' import { FilePreview } from '@/files/domain/models/FilePreview' import { DatasetPublishingStatus } from '@/dataset/domain/models/Dataset' -import { ExternalTool, ToolType } from '@/externalTools/domain/models/ExternalTool' import { AccessFileMenu } from '@/sections/file/file-action-buttons/access-file-menu/AccessFileMenu' import { FileOptionsMenu } from './file-options-menu/FileOptionsMenu' -import { LinkToPage } from '@/sections/shared/link-to-page/LinkToPage' -import { QueryParamKey, Route } from '@/sections/Route.enum' -import { DvObjectType } from '@/shared/hierarchy/domain/models/UpwardHierarchyNode' -import { FilePageHelper } from '@/sections/file/FilePageHelper' import { useMediaQuery } from '@/shared/hooks/useMediaQuery' +import { FileTools } from './FileTools' interface FileActionButtonsProps { file: FilePreview @@ -26,47 +20,11 @@ export function FileActionButtons({ datasetRepository }: FileActionButtonsProps) { const { t } = useTranslation('files') - const theme = useTheme() const isBelow768px = useMediaQuery('(max-width: 768px)') - const { externalTools } = useExternalTools() - const fileApplicablePreviewOrQueryTools: ExternalTool[] = - FilePageHelper.getApplicablePreviewOrQueryToolsForFileType( - externalTools, - file.metadata.type.value - ) - - const showExternalToolsButtons = - fileApplicablePreviewOrQueryTools.length > 0 && file.permissions.canDownloadFile return ( - {showExternalToolsButtons && - fileApplicablePreviewOrQueryTools.map((tool) => ( - - - {tool.types.includes(ToolType.Preview) && ( - - )} - {tool.types.includes(ToolType.Query) && ( - - )} - - - ))} + { + const theme = useTheme() + const { externalTools } = useExternalTools() + + const fileApplicablePreviewOrQueryTools: ExternalTool[] = + FilePageHelper.getApplicablePreviewOrQueryToolsForFileType( + externalTools, + file.metadata.type.value + ) + + const showExternalToolsButtons = fileApplicablePreviewOrQueryTools.length > 0 && canDownloadFile + + if (!showExternalToolsButtons) return null + + return ( + <> + {fileApplicablePreviewOrQueryTools.map((tool) => ( + + + {tool.types.includes(ToolType.Preview) && ( + + )} + {tool.types.includes(ToolType.Query) && ( + + )} + + + ))} + + ) +} diff --git a/tests/component/externalTools/domain/models/ExternalToolsMother.ts b/tests/component/externalTools/domain/models/ExternalToolsMother.ts index 669071bf8..6afb79b46 100644 --- a/tests/component/externalTools/domain/models/ExternalToolsMother.ts +++ b/tests/component/externalTools/domain/models/ExternalToolsMother.ts @@ -26,7 +26,8 @@ export class ExternalToolsMother { displayName: 'File Preview Tool', description: 'Description for File Preview Tool', scope: ToolScope.File, - types: [ToolType.Preview] + types: [ToolType.Preview], + contentType: 'text/plain' } } @@ -36,7 +37,19 @@ export class ExternalToolsMother { displayName: 'File Explore Tool', description: 'Description for File Explore Tool', scope: ToolScope.File, - types: [ToolType.Explore] + types: [ToolType.Explore], + contentType: 'text/plain' + } + } + + static createFileQueryTool(): ExternalTool { + return { + id: 4, + displayName: 'File Query Tool', + description: 'Description for File Query Tool', + scope: ToolScope.File, + types: [ToolType.Query], + contentType: 'text/plain' } } } diff --git a/tests/component/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/FileActionButtons.spec.tsx b/tests/component/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/FileActionButtons.spec.tsx index 6880e1f90..3e62ab49d 100644 --- a/tests/component/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/FileActionButtons.spec.tsx +++ b/tests/component/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/FileActionButtons.spec.tsx @@ -10,10 +10,17 @@ import { FileRepository } from '@/files/domain/repositories/FileRepository' const file = FilePreviewMother.createDefault() const fileRepository: FileRepository = {} as FileRepository +const datasetRepository: DatasetRepository = {} as DatasetRepository describe('FileActionButtons', () => { it('renders the file action buttons', () => { - cy.customMount() + cy.customMount( + + ) cy.findByRole('group', { name: 'File Action Buttons' }).should('exist') cy.findByRole('button', { name: 'Access File' }).should('exist') @@ -32,7 +39,11 @@ describe('FileActionButtons', () => { - + ) diff --git a/tests/component/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/FileTools.spec.tsx b/tests/component/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/FileTools.spec.tsx new file mode 100644 index 000000000..1bca6df69 --- /dev/null +++ b/tests/component/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/FileTools.spec.tsx @@ -0,0 +1,71 @@ +import { ExternalToolsRepository } from '@/externalTools/domain/repositories/ExternalToolsRepository' +import { FileTools } from '@/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/FileTools' +import { QueryParamKey } from '@/sections/Route.enum' +import { ExternalToolsProvider } from '@/shared/contexts/external-tools/ExternalToolsProvider' +import { ExternalToolsMother } from '@tests/component/externalTools/domain/models/ExternalToolsMother' +import { FileMetadataMother } from '@tests/component/files/domain/models/FileMetadataMother' +import { FilePreviewMother } from '@tests/component/files/domain/models/FilePreviewMother' + +const testFilePreview = FilePreviewMother.createDefault() +const testExternalToolsRepository: ExternalToolsRepository = {} as ExternalToolsRepository + +describe('FileTools', () => { + beforeEach(() => { + testExternalToolsRepository.getExternalTools = cy + .stub() + .resolves([ + ExternalToolsMother.createFilePreviewTool(), + ExternalToolsMother.createFileQueryTool() + ]) + }) + + it('renders external tool buttons when user can download the file and there are applicable tools', () => { + cy.customMount( + + + + ) + + cy.findByRole('link', { name: `Preview ${testFilePreview.name}` }) + .should('exist') + .as('filePreviewButton') + + cy.findByRole('link', { name: `Query ${testFilePreview.name}` }) + .should('exist') + .as('fileQueryButton') + + cy.get('@filePreviewButton') + .should('have.attr', 'href') + .and('include', `id=${testFilePreview.id}`) + .and('include', `datasetVersion=${testFilePreview.datasetVersionNumber.toString()}`) + .and('include', `${QueryParamKey.TOOL_TYPE}=preview`) + + cy.get('@fileQueryButton') + .should('have.attr', 'href') + .and('include', `id=${testFilePreview.id}`) + .and('include', `datasetVersion=${testFilePreview.datasetVersionNumber.toString()}`) + .and('include', `${QueryParamKey.TOOL_TYPE}=query`) + }) + + it('does not render external tool buttons when user cannot download the file', () => { + cy.customMount( + + + + ) + }) + + it('does not render external tool buttons when there are no applicable tools for the file type', () => { + // File type "tabular" has no applicable preview or query tools in the test repository + const fileWithoutApplicableTools = FilePreviewMother.create({ + id: 2, + metadata: FileMetadataMother.createTabular() + }) + + cy.customMount( + + + + ) + }) +}) From d79c4a3773a77e28bd014db01b1c9e59e14f1e6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Fri, 29 Aug 2025 10:19:48 -0300 Subject: [PATCH 24/51] feat: add stories --- .storybook/preview.tsx | 4 +- src/sections/file/File.tsx | 7 ++- .../FileEmbeddedExternalTool.module.scss} | 0 .../FileEmbeddedExternalTool.tsx} | 15 ++--- .../AccessDatasetMenu.stories.tsx | 21 +++++++ src/stories/file/File.stories.tsx | 58 +++++++++++++++++++ .../AccessFileMenu.stories.tsx | 18 ++++++ .../FileEmbeddedExternalTool.stories.tsx | 45 ++++++++++++++ .../ExternalToolsMockRepository.ts | 10 ++++ .../domain/models/ExternalToolsMother.ts | 3 +- .../models/FileExternalToolResolvedMother.ts | 2 +- 11 files changed, 169 insertions(+), 14 deletions(-) rename src/sections/file/{file-embeded-external-tool/FileEmbededExternalTool.module.scss => file-embedded-external-tool/FileEmbeddedExternalTool.module.scss} (100%) rename src/sections/file/{file-embeded-external-tool/FileEmbededExternalTool.tsx => file-embedded-external-tool/FileEmbeddedExternalTool.tsx} (92%) create mode 100644 src/stories/file/file-embedded-external-tool/FileEmbeddedExternalTool.stories.tsx diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index faee4990c..d8567fdfb 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -4,7 +4,7 @@ import { ThemeProvider } from '@iqss/dataverse-design-system' import { createBrowserRouter, RouteObject, RouterProvider } from 'react-router-dom' import { FakerHelper } from '../tests/component/shared/FakerHelper' import { ExternalToolsProvider } from '../src/shared/contexts/external-tools/ExternalToolsProvider' -import { ExternalToolsMockRepository } from '../src/stories/shared-mock-repositories/externalTools/ExternalToolsMockRepository' +import { ExternalToolsEmptyMockRepository } from '../src/stories/shared-mock-repositories/externalTools/ExternalToolsMockRepository' import 'react-loading-skeleton/dist/skeleton.css' import '../src/assets/global.scss' import '../src/assets/swal-custom.scss' @@ -34,7 +34,7 @@ const preview: Preview = { return ( - + diff --git a/src/sections/file/File.tsx b/src/sections/file/File.tsx index d5f04be42..4175666f6 100644 --- a/src/sections/file/File.tsx +++ b/src/sections/file/File.tsx @@ -21,7 +21,7 @@ import { FileVersions } from './file-version/FileVersions' import { DatasetRepository } from '@/dataset/domain/repositories/DatasetRepository' import { useExternalTools } from '@/shared/contexts/external-tools/ExternalToolsProvider' import { FilePageHelper } from './FilePageHelper' -import { FileEmbededExternalTool } from './file-embeded-external-tool/FileEmbededExternalTool' +import { FileEmbeddedExternalTool } from './file-embedded-external-tool/FileEmbeddedExternalTool' import { useScrollTop } from '@/shared/hooks/useScrollTop' interface FileProps { @@ -43,7 +43,7 @@ export function File({ const { setIsLoading } = useLoading() const { t } = useTranslation('file') const { file, isLoading } = useFile(repository, id, datasetVersionNumber) - const { externalTools } = useExternalTools() + const { externalTools, externalToolsRepository } = useExternalTools() const [activeTab, setActiveTab] = useState( toolTypeSelectedQueryParam && file?.permissions.canDownloadFile ? FilePageHelper.EXT_TOOL_TAB_KEY @@ -172,11 +172,12 @@ export function File({ {fileApplicablePreviewOrQueryTools.length > 0 && file.permissions.canDownloadFile && (
-
diff --git a/src/sections/file/file-embeded-external-tool/FileEmbededExternalTool.module.scss b/src/sections/file/file-embedded-external-tool/FileEmbeddedExternalTool.module.scss similarity index 100% rename from src/sections/file/file-embeded-external-tool/FileEmbededExternalTool.module.scss rename to src/sections/file/file-embedded-external-tool/FileEmbeddedExternalTool.module.scss diff --git a/src/sections/file/file-embeded-external-tool/FileEmbededExternalTool.tsx b/src/sections/file/file-embedded-external-tool/FileEmbeddedExternalTool.tsx similarity index 92% rename from src/sections/file/file-embeded-external-tool/FileEmbededExternalTool.tsx rename to src/sections/file/file-embedded-external-tool/FileEmbeddedExternalTool.tsx index 51f091a8f..958279b95 100644 --- a/src/sections/file/file-embeded-external-tool/FileEmbededExternalTool.tsx +++ b/src/sections/file/file-embedded-external-tool/FileEmbeddedExternalTool.tsx @@ -4,30 +4,31 @@ import cn from 'classnames' import { WriteError } from '@iqss/dataverse-client-javascript' import { Alert, DropdownButton, DropdownButtonItem, Spinner } from '@iqss/dataverse-design-system' import { BoxArrowUpRight } from 'react-bootstrap-icons' -import { useExternalTools } from '@/shared/contexts/external-tools/ExternalToolsProvider' import { ExternalTool } from '@/externalTools/domain/models/ExternalTool' +import { ExternalToolsRepository } from '@/externalTools/domain/repositories/ExternalToolsRepository' import { FileExternalToolResolved } from '@/externalTools/domain/models/FileExternalToolResolved' import { getFileExternalToolResolved } from '@/externalTools/domain/useCases/GetFileExternalToolResolved' import { JSDataverseWriteErrorHandler } from '@/shared/helpers/JSDataverseWriteErrorHandler' import { File } from '@/files/domain/models/File' import { FilePageHelper } from '../FilePageHelper' -import styles from './FileEmbededExternalTool.module.scss' +import styles from './FileEmbeddedExternalTool.module.scss' -interface FileEmbededExternalToolProps { +interface FileEmbeddedExternalToolProps { file: File isInView: boolean applicableTools: ExternalTool[] toolTypeSelectedQueryParam: string | undefined + externalToolsRepository: ExternalToolsRepository } -export const FileEmbededExternalTool = ({ +export const FileEmbeddedExternalTool = ({ file, isInView, applicableTools, - toolTypeSelectedQueryParam -}: FileEmbededExternalToolProps) => { + toolTypeSelectedQueryParam, + externalToolsRepository +}: FileEmbeddedExternalToolProps) => { const { t, i18n } = useTranslation('file', { keyPrefix: 'previewTab' }) - const { externalToolsRepository } = useExternalTools() const [toolIdSelected, setToolIdSelected] = useState( FilePageHelper.getDefaultSelectedToolId(toolTypeSelectedQueryParam, applicableTools) ) diff --git a/src/stories/dataset/dataset-action-buttons/access-dataset-menu/AccessDatasetMenu.stories.tsx b/src/stories/dataset/dataset-action-buttons/access-dataset-menu/AccessDatasetMenu.stories.tsx index 42ee18524..852af5d33 100644 --- a/src/stories/dataset/dataset-action-buttons/access-dataset-menu/AccessDatasetMenu.stories.tsx +++ b/src/stories/dataset/dataset-action-buttons/access-dataset-menu/AccessDatasetMenu.stories.tsx @@ -8,6 +8,8 @@ import { DatasetVersionMother } from '../../../../../tests/component/dataset/domain/models/DatasetMother' import { AccessDatasetMenu } from '../../../../sections/dataset/dataset-action-buttons/access-dataset-menu/AccessDatasetMenu' +import { ExternalToolsProvider } from '@/shared/contexts/external-tools/ExternalToolsProvider' +import { ExternalToolsMockRepository } from '@/stories/shared-mock-repositories/externalTools/ExternalToolsMockRepository' const meta: Meta = { title: 'Sections/Dataset Page/DatasetActionButtons/AccessDatasetMenu', @@ -64,3 +66,22 @@ export const WithTabularFiles: Story = { /> ) } + +export const WithExploreOptionsTools: Story = { + render: () => ( + + + + ) +} diff --git a/src/stories/file/File.stories.tsx b/src/stories/file/File.stories.tsx index ce6185fb0..2344b068e 100644 --- a/src/stories/file/File.stories.tsx +++ b/src/stories/file/File.stories.tsx @@ -7,6 +7,10 @@ import { FileMockLoadingRepository } from './FileMockLoadingRepository' import { FileMockNoDataRepository } from './FileMockNoDataRepository' import { FileMother } from '../../../tests/component/files/domain/models/FileMother' import { DatasetMockRepository } from '../dataset/DatasetMockRepository' +import { ExternalToolsProvider } from '@/shared/contexts/external-tools/ExternalToolsProvider' +import { ExternalToolsMockRepository } from '../shared-mock-repositories/externalTools/ExternalToolsMockRepository' +import { FakerHelper } from '@tests/component/shared/FakerHelper' +import { ExternalToolsMother } from '@tests/component/externalTools/domain/models/ExternalToolsMother' const meta: Meta = { title: 'Pages/File', @@ -70,3 +74,57 @@ export const FileNotFound: Story = { /> ) } + +export const WithMultipleExternalTools: Story = { + render: () => ( + + + + ) +} + +const externalToolsRepositoryOnlyPreviewTool = new ExternalToolsMockRepository() +externalToolsRepositoryOnlyPreviewTool.getExternalTools = () => { + return new Promise((resolve) => { + setTimeout(() => { + resolve([ExternalToolsMother.createFilePreviewTool()]) + }, FakerHelper.loadingTimout()) + }) +} + +export const WithOnlyOnePreviewExternalTool: Story = { + render: () => ( + + + + ) +} + +const externalToolsRepositoryOnlyQueryTool = new ExternalToolsMockRepository() +externalToolsRepositoryOnlyQueryTool.getExternalTools = () => { + return new Promise((resolve) => { + setTimeout(() => { + resolve([ExternalToolsMother.createFileQueryTool()]) + }, FakerHelper.loadingTimout()) + }) +} + +export const WithOnlyOneQueryExternalTool: Story = { + render: () => ( + + + + ) +} diff --git a/src/stories/file/file-action-buttons/access-file-menu/AccessFileMenu.stories.tsx b/src/stories/file/file-action-buttons/access-file-menu/AccessFileMenu.stories.tsx index dd4e3c26d..8ccf6638f 100644 --- a/src/stories/file/file-action-buttons/access-file-menu/AccessFileMenu.stories.tsx +++ b/src/stories/file/file-action-buttons/access-file-menu/AccessFileMenu.stories.tsx @@ -4,6 +4,8 @@ import { WithI18next } from '../../../WithI18next' import { WithSettings } from '../../../WithSettings' import { FileAccessMother } from '../../../../../tests/component/files/domain/models/FileAccessMother' import { FileMetadataMother } from '../../../../../tests/component/files/domain/models/FileMetadataMother' +import { ExternalToolsProvider } from '@/shared/contexts/external-tools/ExternalToolsProvider' +import { ExternalToolsMockRepository } from '@/stories/shared-mock-repositories/externalTools/ExternalToolsMockRepository' const meta: Meta = { title: 'Sections/File Page/Action Buttons/AccessFileMenu', @@ -168,3 +170,19 @@ export const WithEmbargoAndRestrictedWithAccessGranted: Story = { /> ) } + +export const WithExploreOptionsTools: Story = { + render: () => ( + + + + ) +} diff --git a/src/stories/file/file-embedded-external-tool/FileEmbeddedExternalTool.stories.tsx b/src/stories/file/file-embedded-external-tool/FileEmbeddedExternalTool.stories.tsx new file mode 100644 index 000000000..dcc339520 --- /dev/null +++ b/src/stories/file/file-embedded-external-tool/FileEmbeddedExternalTool.stories.tsx @@ -0,0 +1,45 @@ +import { Meta, StoryObj } from '@storybook/react' +import { WithI18next } from '../../WithI18next' +import { FileEmbeddedExternalTool } from '@/sections/file/file-embedded-external-tool/FileEmbeddedExternalTool' +import { FileMother } from '@tests/component/files/domain/models/FileMother' +import { ExternalToolsMockRepository } from '@/stories/shared-mock-repositories/externalTools/ExternalToolsMockRepository' +import { ExternalToolsMother } from '@tests/component/externalTools/domain/models/ExternalToolsMother' + +const meta: Meta = { + title: 'Sections/File Page/File External Tools Tab', + component: FileEmbeddedExternalTool, + decorators: [WithI18next] +} + +export default meta +type Story = StoryObj + +const file = FileMother.createRealistic() // text/plain file +const externalToolsRepository = new ExternalToolsMockRepository() + +export const WithOneToolOnly: Story = { + render: () => ( + + ) +} + +export const WithMoreThanOneTool: Story = { + render: () => ( + + ) +} diff --git a/src/stories/shared-mock-repositories/externalTools/ExternalToolsMockRepository.ts b/src/stories/shared-mock-repositories/externalTools/ExternalToolsMockRepository.ts index 8b7ac3308..2a91a7aea 100644 --- a/src/stories/shared-mock-repositories/externalTools/ExternalToolsMockRepository.ts +++ b/src/stories/shared-mock-repositories/externalTools/ExternalToolsMockRepository.ts @@ -41,3 +41,13 @@ export class ExternalToolsMockRepository implements ExternalToolsRepository { }) } } + +export class ExternalToolsEmptyMockRepository implements Partial { + getExternalTools(): Promise { + return new Promise((resolve) => { + setTimeout(() => { + resolve([]) + }, FakerHelper.loadingTimout()) + }) + } +} diff --git a/tests/component/externalTools/domain/models/ExternalToolsMother.ts b/tests/component/externalTools/domain/models/ExternalToolsMother.ts index 6afb79b46..e45df201a 100644 --- a/tests/component/externalTools/domain/models/ExternalToolsMother.ts +++ b/tests/component/externalTools/domain/models/ExternalToolsMother.ts @@ -6,7 +6,8 @@ export class ExternalToolsMother { return [ this.createDatasetExploreTool(), this.createFilePreviewTool(), - this.createFileExploreTool() + this.createFileExploreTool(), + this.createFileQueryTool() ].map((tool) => ({ ...tool, ...props })) } diff --git a/tests/component/externalTools/domain/models/FileExternalToolResolvedMother.ts b/tests/component/externalTools/domain/models/FileExternalToolResolvedMother.ts index 1ef610a9e..6253188ff 100644 --- a/tests/component/externalTools/domain/models/FileExternalToolResolvedMother.ts +++ b/tests/component/externalTools/domain/models/FileExternalToolResolvedMother.ts @@ -6,7 +6,7 @@ export class FileExternalToolResolvedMother { displayName: 'File Explore Tool', fileId: 1, preview: false, - toolUrlResolved: 'http://localhost:3000/external-tool', + toolUrlResolved: 'https://www.youtube.com/embed/MPQ0Tpgaxt0?si=376kd6g0CVmCtIOi', ...props } } From cb623b8fdb92711556729f5de838183d6f004ddb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Fri, 29 Aug 2025 12:01:33 -0300 Subject: [PATCH 25/51] test: adding more tests --- .../FileEmbeddedExternalTool.tsx | 5 +- src/stories/file/File.stories.tsx | 3 + .../models/FileExternalToolResolvedMother.ts | 2 +- .../file-action-buttons/FileTools.spec.tsx | 2 +- tests/component/sections/file/File.spec.tsx | 109 +++++++++++ .../access-file-menu/FileToolOptions.spec.tsx | 55 ++++++ .../FileEmbeddedExternalTool.spec.tsx | 171 ++++++++++++++++++ 7 files changed, 343 insertions(+), 4 deletions(-) create mode 100644 tests/component/sections/file/file-action-buttons/access-file-menu/FileToolOptions.spec.tsx create mode 100644 tests/component/sections/file/file-embedded-external-tool/FileEmbeddedExternalTool.spec.tsx diff --git a/src/sections/file/file-embedded-external-tool/FileEmbeddedExternalTool.tsx b/src/sections/file/file-embedded-external-tool/FileEmbeddedExternalTool.tsx index 958279b95..512e7a6d1 100644 --- a/src/sections/file/file-embedded-external-tool/FileEmbeddedExternalTool.tsx +++ b/src/sections/file/file-embedded-external-tool/FileEmbeddedExternalTool.tsx @@ -129,10 +129,11 @@ export const FileEmbeddedExternalTool = ({ className={styles.iframe} onLoad={handleOnLoadIframe} onError={handleOnErrorIframe} - role="presentation"> + role="presentation" + data-testid="external-tool-iframe"> )} {/* Keep overlay on top of the iframe while it loads to mask flickering */} -
+
{/* Show error message if fetching the tool URL fails or the iframe somehow fails. */} diff --git a/src/stories/file/File.stories.tsx b/src/stories/file/File.stories.tsx index 2344b068e..7e578e208 100644 --- a/src/stories/file/File.stories.tsx +++ b/src/stories/file/File.stories.tsx @@ -82,6 +82,7 @@ export const WithMultipleExternalTools: Story = { repository={new FileMockRepository(FileMother.createRealistic())} datasetRepository={new DatasetMockRepository()} id={56} + toolTypeSelectedQueryParam="preview" /> ) @@ -103,6 +104,7 @@ export const WithOnlyOnePreviewExternalTool: Story = { repository={new FileMockRepository(FileMother.createRealistic())} datasetRepository={new DatasetMockRepository()} id={56} + toolTypeSelectedQueryParam="preview" /> ) @@ -124,6 +126,7 @@ export const WithOnlyOneQueryExternalTool: Story = { repository={new FileMockRepository(FileMother.createRealistic())} datasetRepository={new DatasetMockRepository()} id={56} + toolTypeSelectedQueryParam="query" /> ) diff --git a/tests/component/externalTools/domain/models/FileExternalToolResolvedMother.ts b/tests/component/externalTools/domain/models/FileExternalToolResolvedMother.ts index 6253188ff..1c13d76d7 100644 --- a/tests/component/externalTools/domain/models/FileExternalToolResolvedMother.ts +++ b/tests/component/externalTools/domain/models/FileExternalToolResolvedMother.ts @@ -6,7 +6,7 @@ export class FileExternalToolResolvedMother { displayName: 'File Explore Tool', fileId: 1, preview: false, - toolUrlResolved: 'https://www.youtube.com/embed/MPQ0Tpgaxt0?si=376kd6g0CVmCtIOi', + toolUrlResolved: 'https://example.com/explore-tool?fileId=1', ...props } } diff --git a/tests/component/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/FileTools.spec.tsx b/tests/component/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/FileTools.spec.tsx index 1bca6df69..9ff86f86a 100644 --- a/tests/component/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/FileTools.spec.tsx +++ b/tests/component/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/FileTools.spec.tsx @@ -6,7 +6,7 @@ import { ExternalToolsMother } from '@tests/component/externalTools/domain/model import { FileMetadataMother } from '@tests/component/files/domain/models/FileMetadataMother' import { FilePreviewMother } from '@tests/component/files/domain/models/FilePreviewMother' -const testFilePreview = FilePreviewMother.createDefault() +const testFilePreview = FilePreviewMother.createDefault() // text/plain file const testExternalToolsRepository: ExternalToolsRepository = {} as ExternalToolsRepository describe('FileTools', () => { diff --git a/tests/component/sections/file/File.spec.tsx b/tests/component/sections/file/File.spec.tsx index 3d10a6f63..e10f577f8 100644 --- a/tests/component/sections/file/File.spec.tsx +++ b/tests/component/sections/file/File.spec.tsx @@ -2,6 +2,10 @@ import { FileRepository } from '../../../../src/files/domain/repositories/FileRe import { FileMother } from '../../files/domain/models/FileMother' import { File } from '../../../../src/sections/file/File' import { DatasetMockRepository } from '@/stories/dataset/DatasetMockRepository' +import { ExternalToolsRepository } from '@/externalTools/domain/repositories/ExternalToolsRepository' +import { ExternalToolsProvider } from '@/shared/contexts/external-tools/ExternalToolsProvider' +import { ExternalToolsMother } from '@tests/component/externalTools/domain/models/ExternalToolsMother' +import { FileExternalToolResolvedMother } from '@tests/component/externalTools/domain/models/FileExternalToolResolvedMother' const fileRepository: FileRepository = {} as FileRepository @@ -90,4 +94,109 @@ describe('File', () => { cy.contains('Contributors').should('exist') cy.contains('Published On').should('exist') }) + + describe('external tools tab', () => { + const externalToolsRepository: ExternalToolsRepository = {} as ExternalToolsRepository + + beforeEach(() => { + const testFile = FileMother.createRealistic() + fileRepository.getById = cy.stub().resolves(testFile) + externalToolsRepository.getExternalTools = cy + .stub() + .resolves([ExternalToolsMother.createFilePreviewTool()]) + externalToolsRepository.getFileExternalToolResolved = cy + .stub() + .resolves(FileExternalToolResolvedMother.create()) + }) + + it('renders the External Tools tab with "Preview" title if only one tool applicable and is a preview tool', () => { + cy.customMount( + + + + ) + + cy.findByRole('tab', { name: 'Preview' }).should('exist') + }) + + it('renders the External Tools tab with "Query" title if only one tool applicable and is an query tool', () => { + externalToolsRepository.getExternalTools = cy + .stub() + .resolves([ExternalToolsMother.createFileQueryTool()]) + + cy.customMount( + + + + ) + + cy.findByRole('tab', { name: 'Query' }).should('exist') + }) + + it('renders the External Tools tab with "File Tools" title if more than one applicable tool', () => { + externalToolsRepository.getExternalTools = cy + .stub() + .resolves([ + ExternalToolsMother.createFilePreviewTool(), + ExternalToolsMother.createFileQueryTool() + ]) + + cy.customMount( + + + + ) + + cy.findByRole('tab', { name: 'File Tools' }).should('exist') + }) + + it('does not render the External Tools tab if no applicable tools', () => { + externalToolsRepository.getExternalTools = cy.stub().resolves([]) + + cy.customMount( + + + + ) + + cy.findByRole('tab', { name: 'File Tools' }).should('not.exist') + cy.findByRole('tab', { name: 'Preview' }).should('not.exist') + cy.findByRole('tab', { name: 'Query' }).should('not.exist') + }) + + it('does not render the External Tools tab if applicable tool but user lacks download permission', () => { + const testFile = FileMother.createWithDownloadPermissionDenied() + fileRepository.getById = cy.stub().resolves(testFile) + + cy.customMount( + + + + ) + + cy.findByRole('tab', { name: 'File Tools' }).should('not.exist') + cy.findByRole('tab', { name: 'Preview' }).should('not.exist') + cy.findByRole('tab', { name: 'Query' }).should('not.exist') + }) + }) }) diff --git a/tests/component/sections/file/file-action-buttons/access-file-menu/FileToolOptions.spec.tsx b/tests/component/sections/file/file-action-buttons/access-file-menu/FileToolOptions.spec.tsx new file mode 100644 index 000000000..0c0b3405c --- /dev/null +++ b/tests/component/sections/file/file-action-buttons/access-file-menu/FileToolOptions.spec.tsx @@ -0,0 +1,55 @@ +import { ExternalToolsRepository } from '@/externalTools/domain/repositories/ExternalToolsRepository' +import { + FileExploreOptions, + FileQueryOptions +} from '@/sections/file/file-action-buttons/access-file-menu/FileToolOptions' +import { ExternalToolsProvider } from '@/shared/contexts/external-tools/ExternalToolsProvider' +import { ExternalToolsMother } from '@tests/component/externalTools/domain/models/ExternalToolsMother' + +const testExternalToolsRepository: ExternalToolsRepository = {} as ExternalToolsRepository + +describe('FileToolOptions', () => { + beforeEach(() => { + testExternalToolsRepository.getExternalTools = cy + .stub() + .resolves([ + ExternalToolsMother.createFileExploreTool(), + ExternalToolsMother.createFileQueryTool() + ]) + }) + + it('renders the tool options if user has download permission and file explore tools are available', () => { + cy.customMount( + + + + ) + + cy.findByText('Query Options').should('not.exist') + cy.findByText('Explore Options').should('exist') + cy.findByText('File Explore Tool').should('exist') + }) + + it('renders the tool options if user has download permission and file query tools are available', () => { + cy.customMount( + + + + ) + + cy.findByText('Explore Options').should('not.exist') + cy.findByText('Query Options').should('exist') + cy.findByText('File Query Tool').should('exist') + }) + + it('does not render the tool options if user lacks download permission', () => { + cy.customMount( + + + + ) + + cy.findByText('Explore Options').should('not.exist') + cy.findByText('Query Options').should('not.exist') + }) +}) diff --git a/tests/component/sections/file/file-embedded-external-tool/FileEmbeddedExternalTool.spec.tsx b/tests/component/sections/file/file-embedded-external-tool/FileEmbeddedExternalTool.spec.tsx new file mode 100644 index 000000000..469c57039 --- /dev/null +++ b/tests/component/sections/file/file-embedded-external-tool/FileEmbeddedExternalTool.spec.tsx @@ -0,0 +1,171 @@ +import { ExternalToolsRepository } from '@/externalTools/domain/repositories/ExternalToolsRepository' +import { FileEmbeddedExternalTool } from '@/sections/file/file-embedded-external-tool/FileEmbeddedExternalTool' +import { FilePageHelper } from '@/sections/file/FilePageHelper' +import { WriteError } from '@iqss/dataverse-client-javascript' +import { ExternalToolsMother } from '@tests/component/externalTools/domain/models/ExternalToolsMother' +import { FileExternalToolResolvedMother } from '@tests/component/externalTools/domain/models/FileExternalToolResolvedMother' +import { FileMother } from '@tests/component/files/domain/models/FileMother' + +const externalToolsRepository: ExternalToolsRepository = {} as ExternalToolsRepository // Used for fetching the tool resolved URL + +const testFile = FileMother.createRealistic() // text/plain file +const filePreviewTool = ExternalToolsMother.createFilePreviewTool() // id: 2 +const filePreviewToolResolved = FileExternalToolResolvedMother.create({ + displayName: 'File Preview Tool', + toolUrlResolved: 'https://example.com/preview-tool?fileId=1' +}) +const fileQueryTool = ExternalToolsMother.createFileQueryTool() // id: 4 +const fileQueryToolResolved = FileExternalToolResolvedMother.create({ + displayName: 'File Query Tool', + toolUrlResolved: 'https://example.com/query-tool?fileId=1' +}) + +describe('FileEmbeddedExternalTool', () => { + it('renders a single preview tool', () => { + externalToolsRepository.getFileExternalToolResolved = cy + .stub() + .resolves(filePreviewToolResolved) + + cy.customMount( + + ) + + cy.findByTestId('external-tool-iframe') + .should('exist') + .should('have.attr', 'src', filePreviewToolResolved.toolUrlResolved) + + // Just a small wait to cover the iframe onLoad event, there is a cypress package for this, but to avoid installing it, just a wait is fine. + cy.wait(1_000) + + cy.findByRole('link', { name: 'Open in New Window' }) + .should('exist') + .should( + 'have.attr', + 'href', + FilePageHelper.replacePreviewParamInToolUrl(filePreviewToolResolved.toolUrlResolved, false) + ) + }) + + it('renders multiple tools and allows switching between them', () => { + const getFileExternalToolResolvedStub = cy.stub() + + // Stub the calls to getFileExternalToolResolved for each tool + getFileExternalToolResolvedStub + .withArgs(testFile.id, filePreviewTool.id) + .resolves(filePreviewToolResolved) + + getFileExternalToolResolvedStub + .withArgs(testFile.id, fileQueryTool.id) + .resolves(fileQueryToolResolved) + + externalToolsRepository.getFileExternalToolResolved = getFileExternalToolResolvedStub + + cy.customMount( + + ) + // The "Change Tool" button is present to select between the two tools + cy.findByRole('button', { name: 'Change Tool' }).should('exist').as('changeToolButton') + cy.get('@changeToolButton').click() + cy.findByRole('button', { name: filePreviewTool.displayName }).should('exist') + cy.findByRole('button', { name: fileQueryTool.displayName }).should('exist') + cy.get('@changeToolButton').click() // Close the dropdown + + // Initially the preview tool is selected + + cy.findByTestId('external-tool-iframe') + .should('exist') + .should('have.attr', 'src', filePreviewToolResolved.toolUrlResolved) + + cy.findByRole('link', { name: 'Open in New Window' }) + .should('exist') + .should( + 'have.attr', + 'href', + FilePageHelper.replacePreviewParamInToolUrl(filePreviewToolResolved.toolUrlResolved, false) + ) + + // Now we select the query tool + cy.get('@changeToolButton').click() + cy.findByRole('button', { name: fileQueryTool.displayName }).click() + + cy.findByTestId('external-tool-iframe') + .should('exist') + .should('have.attr', 'src', fileQueryToolResolved.toolUrlResolved) + + cy.findByRole('link', { name: 'Open in New Window' }) + .should('exist') + .should( + 'have.attr', + 'href', + FilePageHelper.replacePreviewParamInToolUrl(fileQueryToolResolved.toolUrlResolved, false) + ) + }) + + it('does not load the iframe if tab wrapping the component is not in view', () => { + externalToolsRepository.getFileExternalToolResolved = cy + .stub() + .resolves(filePreviewToolResolved) + + cy.customMount( + + ) + cy.findByTestId('external-tool-iframe').should('not.exist') + }) + + describe('error handling', () => { + it('shows js dataverse error message if fetching the tool URL fails with a JSDataverseError', () => { + externalToolsRepository.getFileExternalToolResolved = cy + .stub() + .rejects(new WriteError('Some js dataverse processed error message.')) + cy.customMount( + + ) + + cy.findByText(/Some js dataverse processed error message./) + cy.findByTestId('external-tool-iframe').should('not.exist') + }) + + it('shows fallback error message if fetching the tool URL fails', () => { + externalToolsRepository.getFileExternalToolResolved = cy + .stub() + .rejects(new Error('Failed to fetch tool URL')) + + cy.customMount( + + ) + + cy.findByText(/Something went wrong loading the external tool. Try again later./) + cy.findByTestId('external-tool-iframe').should('not.exist') + }) + }) +}) From 5b76d1bfcf1aaed7915955e4b3c7bd63b41bf6f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Fri, 29 Aug 2025 15:19:32 -0300 Subject: [PATCH 26/51] refactor: reuse same component for dataset explore and configure options --- public/locales/en/shared.json | 3 +- ...oreOptions.tsx => DatasetToolsOptions.tsx} | 41 ++++++++---- .../access-dataset-menu/AccessDatasetMenu.tsx | 8 +-- .../edit-dataset-menu/EditDatasetMenu.tsx | 4 ++ .../DatasetToolOptions.spec.tsx | 64 +++++++++++++++++++ .../DatasetExploreOptions.spec.tsx | 24 ------- .../DatasetConfigureOptions.spec.tsx | 0 7 files changed, 103 insertions(+), 41 deletions(-) rename src/sections/dataset/dataset-action-buttons/{access-dataset-menu/DatasetExploreOptions.tsx => DatasetToolsOptions.tsx} (69%) create mode 100644 tests/component/sections/dataset/dataset-action-buttons/DatasetToolOptions.spec.tsx delete mode 100644 tests/component/sections/dataset/dataset-action-buttons/access-dataset-menu/DatasetExploreOptions.spec.tsx create mode 100644 tests/component/sections/dataset/dataset-action-buttons/edit-dataset-menu/DatasetConfigureOptions.spec.tsx diff --git a/public/locales/en/shared.json b/public/locales/en/shared.json index f5be029b6..1220adf46 100644 --- a/public/locales/en/shared.json +++ b/public/locales/en/shared.json @@ -292,5 +292,6 @@ "truncateLessTip": "Click to collapse the {{contentName}}" }, "exploreOptions": "Explore Options", - "queryOptions": "Query Options" + "queryOptions": "Query Options", + "configureOptions": "Configure Options" } diff --git a/src/sections/dataset/dataset-action-buttons/access-dataset-menu/DatasetExploreOptions.tsx b/src/sections/dataset/dataset-action-buttons/DatasetToolsOptions.tsx similarity index 69% rename from src/sections/dataset/dataset-action-buttons/access-dataset-menu/DatasetExploreOptions.tsx rename to src/sections/dataset/dataset-action-buttons/DatasetToolsOptions.tsx index 68adc0bac..ce988a203 100644 --- a/src/sections/dataset/dataset-action-buttons/access-dataset-menu/DatasetExploreOptions.tsx +++ b/src/sections/dataset/dataset-action-buttons/DatasetToolsOptions.tsx @@ -1,30 +1,38 @@ import { useState } from 'react' import { toast } from 'react-toastify' import { useTranslation } from 'react-i18next' -import { BarChartFill as BarChartFillIcon } from 'react-bootstrap-icons' +import { BarChartFill as BarChartFillIcon, GearFill } from 'react-bootstrap-icons' import { DropdownButtonItem, DropdownHeader } from '@iqss/dataverse-design-system' import { useExternalTools } from '@/shared/contexts/external-tools/ExternalToolsProvider' import { getDatasetExternalToolResolved } from '@/externalTools/domain/useCases/GetDatasetExternalToolResolved' import { ExternalToolsRepository } from '@/externalTools/domain/repositories/ExternalToolsRepository' -interface DatasetExploreOptionsProps { +type ToolKind = 'explore' | 'configure' + +interface DatasetToolOptionsProps { persistentId: string + kind: ToolKind } -export const DatasetExploreOptions = ({ persistentId }: DatasetExploreOptionsProps) => { +const DatasetToolOptions = ({ persistentId, kind }: DatasetToolOptionsProps) => { const { t } = useTranslation('shared') - const { datasetExploreTools, externalToolsRepository } = useExternalTools() + const { datasetExploreTools, datasetConfigureTools, externalToolsRepository } = useExternalTools() + + const tools = kind === 'explore' ? datasetExploreTools : datasetConfigureTools + if (!tools || tools.length === 0) return null - if (!datasetExploreTools || datasetExploreTools.length === 0) return null + const headerLabel = kind === 'explore' ? t('exploreOptions') : t('configureOptions') + const icon = kind === 'explore' ? : return ( <> - {t('exploreOptions')} - + {headerLabel} + {icon} - {datasetExploreTools.map((tool) => ( - ( + { +}: ToolOptionProps) => { const [isOpening, setIsOpening] = useState(false) const { t, i18n } = useTranslation('shared') @@ -96,3 +104,12 @@ const ExploreOption = ({ ) } + +/** Wrappers for readability */ +export const DatasetExploreOptions = (props: Omit) => ( + +) + +export const DatasetConfigureOptions = (props: Omit) => ( + +) diff --git a/src/sections/dataset/dataset-action-buttons/access-dataset-menu/AccessDatasetMenu.tsx b/src/sections/dataset/dataset-action-buttons/access-dataset-menu/AccessDatasetMenu.tsx index fb8a4ee3f..be2ac9942 100644 --- a/src/sections/dataset/dataset-action-buttons/access-dataset-menu/AccessDatasetMenu.tsx +++ b/src/sections/dataset/dataset-action-buttons/access-dataset-menu/AccessDatasetMenu.tsx @@ -1,14 +1,14 @@ +import { DropdownButton, DropdownButtonItem, DropdownHeader } from '@iqss/dataverse-design-system' +import { Download as DownloadIcon } from 'react-bootstrap-icons' +import { useTranslation } from 'react-i18next' import { DatasetDownloadUrls, DatasetPermissions, DatasetPublishingStatus, DatasetVersion } from '../../../../dataset/domain/models/Dataset' -import { DropdownButton, DropdownButtonItem, DropdownHeader } from '@iqss/dataverse-design-system' -import { useTranslation } from 'react-i18next' import { FileDownloadSize, FileDownloadMode } from '../../../../files/domain/models/FileMetadata' -import { Download as DownloadIcon } from 'react-bootstrap-icons' -import { DatasetExploreOptions } from './DatasetExploreOptions' +import { DatasetExploreOptions } from '../DatasetToolsOptions' // TODO: add compute feature diff --git a/src/sections/dataset/dataset-action-buttons/edit-dataset-menu/EditDatasetMenu.tsx b/src/sections/dataset/dataset-action-buttons/edit-dataset-menu/EditDatasetMenu.tsx index 043b5cd05..18a304b02 100644 --- a/src/sections/dataset/dataset-action-buttons/edit-dataset-menu/EditDatasetMenu.tsx +++ b/src/sections/dataset/dataset-action-buttons/edit-dataset-menu/EditDatasetMenu.tsx @@ -13,6 +13,7 @@ import { useSession } from '../../../session/SessionContext' import { QueryParamKey, Route } from '../../../Route.enum' import { DatasetRepository } from '@/dataset/domain/repositories/DatasetRepository' import { useNotImplementedModal } from '../../.././not-implemented/NotImplementedModalContext' +import { DatasetConfigureOptions } from '../DatasetToolsOptions' interface EditDatasetMenuProps { dataset: Dataset @@ -114,6 +115,9 @@ export function EditDatasetMenu({ dataset, datasetRepository }: EditDatasetMenuP {t('datasetActionButtons.editDataset.thumbnailsPlusWidgets')} + + + {!isDeaccessioned && ( diff --git a/tests/component/sections/dataset/dataset-action-buttons/DatasetToolOptions.spec.tsx b/tests/component/sections/dataset/dataset-action-buttons/DatasetToolOptions.spec.tsx new file mode 100644 index 000000000..7e37533ed --- /dev/null +++ b/tests/component/sections/dataset/dataset-action-buttons/DatasetToolOptions.spec.tsx @@ -0,0 +1,64 @@ +import { ExternalToolsRepository } from '@/externalTools/domain/repositories/ExternalToolsRepository' +import { + DatasetConfigureOptions, + DatasetExploreOptions +} from '@/sections/dataset/dataset-action-buttons/DatasetToolsOptions' +import { ExternalToolsProvider } from '@/shared/contexts/external-tools/ExternalToolsProvider' +import { ExternalToolsMother } from '@tests/component/externalTools/domain/models/ExternalToolsMother' + +const testExternalToolsRepository: ExternalToolsRepository = {} as ExternalToolsRepository + +describe('DatasetToolOptions', () => { + beforeEach(() => { + testExternalToolsRepository.getExternalTools = cy + .stub() + .resolves([ + ExternalToolsMother.createDatasetExploreTool(), + ExternalToolsMother.createDatasetConfigureTool() + ]) + }) + + it('renders the dataset configure tools options if they are available', () => { + cy.customMount( + + + + ) + + cy.findByText('Explore Options').should('not.exist') + cy.findByText('Configure Options').should('exist') + cy.findByText('Dataset Configure Tool').should('exist') + }) + + it('renders nothing if there are no dataset configure tools', () => { + testExternalToolsRepository.getExternalTools = cy.stub().resolves([]) + cy.customMount( + + + + ) + cy.findByText('Configure Options').should('not.exist') + }) + + it('renders the dataset explore tools options if they are available', () => { + cy.customMount( + + + + ) + + cy.findByText('Configure Options').should('not.exist') + cy.findByText('Explore Options').should('exist') + cy.findByText('Dataset Explore Tool').should('exist') + }) + + it('renders nothing if there are no dataset explore tools', () => { + testExternalToolsRepository.getExternalTools = cy.stub().resolves([]) + cy.customMount( + + + + ) + cy.findByText('Explore Options').should('not.exist') + }) +}) diff --git a/tests/component/sections/dataset/dataset-action-buttons/access-dataset-menu/DatasetExploreOptions.spec.tsx b/tests/component/sections/dataset/dataset-action-buttons/access-dataset-menu/DatasetExploreOptions.spec.tsx deleted file mode 100644 index 374f68641..000000000 --- a/tests/component/sections/dataset/dataset-action-buttons/access-dataset-menu/DatasetExploreOptions.spec.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { ExternalToolsRepository } from '@/externalTools/domain/repositories/ExternalToolsRepository' -import { DatasetExploreOptions } from '@/sections/dataset/dataset-action-buttons/access-dataset-menu/DatasetExploreOptions' -import { ExternalToolsProvider } from '@/shared/contexts/external-tools/ExternalToolsProvider' - -const externalToolsRepository: ExternalToolsRepository = {} as ExternalToolsRepository - -describe('DatasetExploreOptions', () => { - it('renders nothing if there are no dataset explore tools', () => { - externalToolsRepository.getExternalTools = cy.stub().resolves([]) - cy.customMount( - - - - ) - cy.findByText('Explore Options').should('not.exist') - }) - - it('renders the explore options if there are dataset explore tools', () => { - cy.customMount() - - cy.contains('Explore Options').should('exist') - cy.contains('Dataset Explore Tool').should('exist') - }) -}) diff --git a/tests/component/sections/dataset/dataset-action-buttons/edit-dataset-menu/DatasetConfigureOptions.spec.tsx b/tests/component/sections/dataset/dataset-action-buttons/edit-dataset-menu/DatasetConfigureOptions.spec.tsx new file mode 100644 index 000000000..e69de29bb From 843fbd559d16d74f05818f575fbf02448eab31c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Fri, 29 Aug 2025 15:20:57 -0300 Subject: [PATCH 27/51] feat: file tool options --- src/sections/file/FilePageHelper.ts | 12 ++++++++++ .../access-file-menu/AccessFileMenu.tsx | 12 ++++++++-- .../access-file-menu/FileToolOptions.tsx | 22 +++++++++++++++---- .../external-tools/ExternalToolsProvider.tsx | 9 ++++++++ .../domain/models/ExternalToolsMother.ts | 10 +++++++++ .../access-file-menu/FileToolOptions.spec.tsx | 21 +++++++++++++----- 6 files changed, 75 insertions(+), 11 deletions(-) diff --git a/src/sections/file/FilePageHelper.ts b/src/sections/file/FilePageHelper.ts index df4698b68..49ef99bb2 100644 --- a/src/sections/file/FilePageHelper.ts +++ b/src/sections/file/FilePageHelper.ts @@ -27,6 +27,18 @@ export class FilePageHelper { .filter((tool) => (fileType ? tool.contentType === fileType : false)) } + static getApplicableExploreOrQueryToolsForFileType( + externalTools: ExternalTool[], + fileType?: string + ): ExternalTool[] { + return externalTools + .filter((tool) => tool.scope === ToolScope.File) + .filter( + (tool) => tool.types.includes(ToolType.Explore) || tool.types.includes(ToolType.Query) + ) + .filter((tool) => (fileType ? tool.contentType === fileType : false)) + } + static getExternalToolTabTitle( fileApplicablePreviewOrQueryTools: ExternalTool[], t: (key: string) => string, diff --git a/src/sections/file/file-action-buttons/access-file-menu/AccessFileMenu.tsx b/src/sections/file/file-action-buttons/access-file-menu/AccessFileMenu.tsx index b273df94b..a5e35d588 100644 --- a/src/sections/file/file-action-buttons/access-file-menu/AccessFileMenu.tsx +++ b/src/sections/file/file-action-buttons/access-file-menu/AccessFileMenu.tsx @@ -83,8 +83,16 @@ export function AccessFileMenu({ isTabular={metadata.isTabular} userHasDownloadPermission={userHasDownloadPermission} /> - - + + ) diff --git a/src/sections/file/file-action-buttons/access-file-menu/FileToolOptions.tsx b/src/sections/file/file-action-buttons/access-file-menu/FileToolOptions.tsx index 4d0f81119..93353fcb5 100644 --- a/src/sections/file/file-action-buttons/access-file-menu/FileToolOptions.tsx +++ b/src/sections/file/file-action-buttons/access-file-menu/FileToolOptions.tsx @@ -1,4 +1,3 @@ -// FileToolOptions.tsx import { useState } from 'react' import { toast } from 'react-toastify' import { useTranslation } from 'react-i18next' @@ -7,23 +6,38 @@ import { DropdownButtonItem, DropdownHeader } from '@iqss/dataverse-design-syste import { useExternalTools } from '@/shared/contexts/external-tools/ExternalToolsProvider' import { getFileExternalToolResolved } from '@/externalTools/domain/useCases/GetFileExternalToolResolved' import { ExternalToolsRepository } from '@/externalTools/domain/repositories/ExternalToolsRepository' +import { FilePageHelper } from '../../FilePageHelper' +import { ExternalTool } from '@/externalTools/domain/models/ExternalTool' type ToolKind = 'explore' | 'query' interface FileToolOptionsProps { fileId: number + fileType: string kind: ToolKind userHasDownloadPermission: boolean } -const FileToolOptions = ({ fileId, kind, userHasDownloadPermission }: FileToolOptionsProps) => { +const FileToolOptions = ({ + fileId, + fileType, + kind, + userHasDownloadPermission +}: FileToolOptionsProps) => { const { t } = useTranslation('shared') - const { fileExploreTools, fileQueryTools, externalToolsRepository } = useExternalTools() + const { fileExploreTools, fileQueryTools, externalTools, externalToolsRepository } = + useExternalTools() + + if (!userHasDownloadPermission) return null const tools = kind === 'explore' ? fileExploreTools : fileQueryTools + if (!tools || tools.length === 0) return null - if (!userHasDownloadPermission) return null + const fileApplicablePreviewOrQueryTools: ExternalTool[] = + FilePageHelper.getApplicableExploreOrQueryToolsForFileType(externalTools, fileType) + + if (fileApplicablePreviewOrQueryTools.length === 0) return null const headerLabel = kind === 'explore' ? t('exploreOptions') : t('queryOptions') diff --git a/src/shared/contexts/external-tools/ExternalToolsProvider.tsx b/src/shared/contexts/external-tools/ExternalToolsProvider.tsx index 760b91cca..360e4c53d 100644 --- a/src/shared/contexts/external-tools/ExternalToolsProvider.tsx +++ b/src/shared/contexts/external-tools/ExternalToolsProvider.tsx @@ -11,6 +11,7 @@ type ExternalToolsContextValue = { error: string | null refreshExternalTools: () => Promise datasetExploreTools: ExternalTool[] + datasetConfigureTools: ExternalTool[] fileExploreTools: ExternalTool[] filePreviewTools: ExternalTool[] fileQueryTools: ExternalTool[] @@ -64,6 +65,12 @@ export function ExternalToolsProvider({ ) }, [externalTools]) + const datasetConfigureTools = useMemo(() => { + return externalTools.filter( + (tool) => tool.scope === ToolScope.Dataset && tool.types.includes(ToolType.Configure) + ) + }, [externalTools]) + const fileExploreTools = useMemo(() => { return externalTools.filter( (tool) => tool.scope === ToolScope.File && tool.types.includes(ToolType.Explore) @@ -88,6 +95,7 @@ export function ExternalToolsProvider({ loading, error, datasetExploreTools, + datasetConfigureTools, fileExploreTools, filePreviewTools, fileQueryTools, @@ -99,6 +107,7 @@ export function ExternalToolsProvider({ loading, error, datasetExploreTools, + datasetConfigureTools, fileExploreTools, filePreviewTools, fileQueryTools, diff --git a/tests/component/externalTools/domain/models/ExternalToolsMother.ts b/tests/component/externalTools/domain/models/ExternalToolsMother.ts index e45df201a..840ff907e 100644 --- a/tests/component/externalTools/domain/models/ExternalToolsMother.ts +++ b/tests/component/externalTools/domain/models/ExternalToolsMother.ts @@ -21,6 +21,16 @@ export class ExternalToolsMother { } } + static createDatasetConfigureTool(): ExternalTool { + return { + id: 5, + displayName: 'Dataset Configure Tool', + description: 'Description for Dataset Configure Tool', + scope: ToolScope.Dataset, + types: [ToolType.Configure] + } + } + static createFilePreviewTool(): ExternalTool { return { id: 2, diff --git a/tests/component/sections/file/file-action-buttons/access-file-menu/FileToolOptions.spec.tsx b/tests/component/sections/file/file-action-buttons/access-file-menu/FileToolOptions.spec.tsx index 0c0b3405c..8621c02ab 100644 --- a/tests/component/sections/file/file-action-buttons/access-file-menu/FileToolOptions.spec.tsx +++ b/tests/component/sections/file/file-action-buttons/access-file-menu/FileToolOptions.spec.tsx @@ -18,10 +18,10 @@ describe('FileToolOptions', () => { ]) }) - it('renders the tool options if user has download permission and file explore tools are available', () => { + it('renders the tool options if user has download permission and file explore tools are available and compatible with the type', () => { cy.customMount( - + ) @@ -30,10 +30,10 @@ describe('FileToolOptions', () => { cy.findByText('File Explore Tool').should('exist') }) - it('renders the tool options if user has download permission and file query tools are available', () => { + it('renders the tool options if user has download permission and file query tools are available and compatible with the type', () => { cy.customMount( - + ) @@ -45,7 +45,18 @@ describe('FileToolOptions', () => { it('does not render the tool options if user lacks download permission', () => { cy.customMount( - + + + ) + + cy.findByText('Explore Options').should('not.exist') + cy.findByText('Query Options').should('not.exist') + }) + + it('does not render the tool options if there are not applicable tools for the file type', () => { + cy.customMount( + + ) From 8e7c685521a84b135309d941c1c1aa932ec8a805 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Fri, 29 Aug 2025 16:28:07 -0300 Subject: [PATCH 28/51] feat: add configure tools in edit file menu --- src/sections/file/File.tsx | 1 + src/sections/file/FilePageHelper.ts | 5 +- .../access-file-menu/FileToolOptions.tsx | 53 +++++++++++-------- .../edit-file-menu/EditFileMenu.tsx | 6 ++- 4 files changed, 39 insertions(+), 26 deletions(-) diff --git a/src/sections/file/File.tsx b/src/sections/file/File.tsx index 4175666f6..1b719dd6d 100644 --- a/src/sections/file/File.tsx +++ b/src/sections/file/File.tsx @@ -162,6 +162,7 @@ export function File({ storageIdentifier={file.metadata.storageIdentifier} existingLabels={file.metadata.labels} isTabularFile={file.metadata.isTabular} + fileType={file.metadata.type.value} datasetRepository={datasetRepository} /> )} diff --git a/src/sections/file/FilePageHelper.ts b/src/sections/file/FilePageHelper.ts index 49ef99bb2..330102b68 100644 --- a/src/sections/file/FilePageHelper.ts +++ b/src/sections/file/FilePageHelper.ts @@ -27,15 +27,12 @@ export class FilePageHelper { .filter((tool) => (fileType ? tool.contentType === fileType : false)) } - static getApplicableExploreOrQueryToolsForFileType( + static getApplicableToolsForFileType( externalTools: ExternalTool[], fileType?: string ): ExternalTool[] { return externalTools .filter((tool) => tool.scope === ToolScope.File) - .filter( - (tool) => tool.types.includes(ToolType.Explore) || tool.types.includes(ToolType.Query) - ) .filter((tool) => (fileType ? tool.contentType === fileType : false)) } diff --git a/src/sections/file/file-action-buttons/access-file-menu/FileToolOptions.tsx b/src/sections/file/file-action-buttons/access-file-menu/FileToolOptions.tsx index 93353fcb5..840916b1b 100644 --- a/src/sections/file/file-action-buttons/access-file-menu/FileToolOptions.tsx +++ b/src/sections/file/file-action-buttons/access-file-menu/FileToolOptions.tsx @@ -1,7 +1,11 @@ import { useState } from 'react' import { toast } from 'react-toastify' import { useTranslation } from 'react-i18next' -import { BarChartFill as BarChartFillIcon } from 'react-bootstrap-icons' +import { + BarChartFill as BarChartFillIcon, + GearFill as GearFillIcon, + type Icon as IconType +} from 'react-bootstrap-icons' import { DropdownButtonItem, DropdownHeader } from '@iqss/dataverse-design-system' import { useExternalTools } from '@/shared/contexts/external-tools/ExternalToolsProvider' import { getFileExternalToolResolved } from '@/externalTools/domain/useCases/GetFileExternalToolResolved' @@ -9,43 +13,46 @@ import { ExternalToolsRepository } from '@/externalTools/domain/repositories/Ext import { FilePageHelper } from '../../FilePageHelper' import { ExternalTool } from '@/externalTools/domain/models/ExternalTool' -type ToolKind = 'explore' | 'query' +type ToolKind = 'explore' | 'query' | 'configure' interface FileToolOptionsProps { fileId: number fileType: string kind: ToolKind - userHasDownloadPermission: boolean } -const FileToolOptions = ({ - fileId, - fileType, - kind, - userHasDownloadPermission -}: FileToolOptionsProps) => { +const FileToolOptions = ({ fileId, fileType, kind }: FileToolOptionsProps) => { const { t } = useTranslation('shared') - const { fileExploreTools, fileQueryTools, externalTools, externalToolsRepository } = + const { fileExploreTools, fileQueryTools, fileConfigureTools, externalToolsRepository } = useExternalTools() - if (!userHasDownloadPermission) return null + /** Per-kind config (single source of truth) */ + const configByKind: Record< + ToolKind, + { + tools: ExternalTool[] + headerI18nKey: string + Icon: IconType + } + > = { + explore: { tools: fileExploreTools, headerI18nKey: 'exploreOptions', Icon: BarChartFillIcon }, + query: { tools: fileQueryTools, headerI18nKey: 'queryOptions', Icon: BarChartFillIcon }, + configure: { tools: fileConfigureTools, headerI18nKey: 'configureOptions', Icon: GearFillIcon } + } - const tools = kind === 'explore' ? fileExploreTools : fileQueryTools + const { tools, headerI18nKey, Icon } = configByKind[kind] if (!tools || tools.length === 0) return null - const fileApplicablePreviewOrQueryTools: ExternalTool[] = - FilePageHelper.getApplicableExploreOrQueryToolsForFileType(externalTools, fileType) + const applicableTools = FilePageHelper.getApplicableToolsForFileType(tools, fileType) - if (fileApplicablePreviewOrQueryTools.length === 0) return null - - const headerLabel = kind === 'explore' ? t('exploreOptions') : t('queryOptions') + if (applicableTools.length === 0) return null return ( <> - {headerLabel} - + {t(headerI18nKey)} + {tools.map((tool) => ( @@ -122,10 +129,14 @@ const ToolOption = ({ } /** Wrappers for readability */ -export const FileExploreOptions = (props: Omit) => ( +export const FileExploreToolsOptions = (props: Omit) => ( ) -export const FileQueryOptions = (props: Omit) => ( +export const FileQueryToolsOptions = (props: Omit) => ( ) + +export const FileConfigureToolsOptions = (props: Omit) => ( + +) diff --git a/src/sections/file/file-action-buttons/edit-file-menu/EditFileMenu.tsx b/src/sections/file/file-action-buttons/edit-file-menu/EditFileMenu.tsx index d0b824745..4dc6deb7b 100644 --- a/src/sections/file/file-action-buttons/edit-file-menu/EditFileMenu.tsx +++ b/src/sections/file/file-action-buttons/edit-file-menu/EditFileMenu.tsx @@ -10,6 +10,7 @@ import { EditFileMetadataReferrer } from '@/sections/edit-file-metadata/EditFile import { EditFileTagsButton } from './edit-file-tags/EditFileTagsButton' import { FileLabel } from '@/files/domain/models/FileMetadata' import { DatasetRepository } from '@/dataset/domain/repositories/DatasetRepository' +import { FileConfigureToolsOptions } from '../access-file-menu/FileToolOptions' interface EditFileMenuProps { fileId: number @@ -20,6 +21,7 @@ interface EditFileMenuProps { existingLabels?: FileLabel[] datasetRepository: DatasetRepository isTabularFile: boolean + fileType: string } export interface EditFileMenuDatasetInfo { @@ -38,7 +40,8 @@ export const EditFileMenu = ({ storageIdentifier, existingLabels, isTabularFile, - datasetRepository + datasetRepository, + fileType }: EditFileMenuProps) => { const { t } = useTranslation('file') @@ -86,6 +89,7 @@ export const EditFileMenu = ({ datasetRepository={datasetRepository} /> + ) } From f3028e2bb164833a346b477a69487c61a37fb8e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Fri, 29 Aug 2025 16:29:07 -0300 Subject: [PATCH 29/51] feat: add configure tools in edit file menu in dtaset files --- .../file-options-menu/FileOptionsMenu.tsx | 2 ++ .../contexts/external-tools/ExternalToolsProvider.tsx | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/file-options-menu/FileOptionsMenu.tsx b/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/file-options-menu/FileOptionsMenu.tsx index 00a420619..4311aeef6 100644 --- a/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/file-options-menu/FileOptionsMenu.tsx +++ b/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/file-options-menu/FileOptionsMenu.tsx @@ -10,6 +10,7 @@ import { useDataset } from '../../../../../../DatasetContext' import { FileRepository } from '@/files/domain/repositories/FileRepository' import { EditFilesMenuDatasetInfo } from '../../../edit-files-menu/EditFilesOptions' import { DatasetRepository } from '@/dataset/domain/repositories/DatasetRepository' +import { FileConfigureToolsOptions } from '@/sections/file/file-action-buttons/access-file-menu/FileToolOptions' interface FileOptionsMenuProps { file: FilePreview @@ -79,6 +80,7 @@ export function FileOptionsMenu({ file, fileRepository, datasetRepository }: Fil isHeader={false} datasetRepository={datasetRepository} /> + ) diff --git a/src/shared/contexts/external-tools/ExternalToolsProvider.tsx b/src/shared/contexts/external-tools/ExternalToolsProvider.tsx index 360e4c53d..605cec429 100644 --- a/src/shared/contexts/external-tools/ExternalToolsProvider.tsx +++ b/src/shared/contexts/external-tools/ExternalToolsProvider.tsx @@ -15,6 +15,7 @@ type ExternalToolsContextValue = { fileExploreTools: ExternalTool[] filePreviewTools: ExternalTool[] fileQueryTools: ExternalTool[] + fileConfigureTools: ExternalTool[] externalToolsRepository: ExternalToolsRepository } @@ -89,6 +90,12 @@ export function ExternalToolsProvider({ ) }, [externalTools]) + const fileConfigureTools = useMemo(() => { + return externalTools.filter( + (tool) => tool.scope === ToolScope.File && tool.types.includes(ToolType.Configure) + ) + }, [externalTools]) + const value = useMemo( () => ({ externalTools, @@ -99,6 +106,7 @@ export function ExternalToolsProvider({ fileExploreTools, filePreviewTools, fileQueryTools, + fileConfigureTools, refreshExternalTools: fetchExternalTools, externalToolsRepository }), @@ -111,6 +119,7 @@ export function ExternalToolsProvider({ fileExploreTools, filePreviewTools, fileQueryTools, + fileConfigureTools, fetchExternalTools, externalToolsRepository ] From 7ad8f8e416925ee541c70958fd5fbe7705e2fa72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Fri, 29 Aug 2025 16:29:49 -0300 Subject: [PATCH 30/51] feat: handle rendering according to permissions outside comp --- .../access-file-menu/AccessFileMenu.tsx | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/sections/file/file-action-buttons/access-file-menu/AccessFileMenu.tsx b/src/sections/file/file-action-buttons/access-file-menu/AccessFileMenu.tsx index a5e35d588..4fde63019 100644 --- a/src/sections/file/file-action-buttons/access-file-menu/AccessFileMenu.tsx +++ b/src/sections/file/file-action-buttons/access-file-menu/AccessFileMenu.tsx @@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next' import { FileDownloadOptions } from './FileDownloadOptions' import { FileAccess } from '../../../../files/domain/models/FileAccess' import { FileMetadata } from '../../../../files/domain/models/FileMetadata' -import { FileExploreOptions, FileQueryOptions } from './FileToolOptions' +import { FileExploreToolsOptions, FileQueryToolsOptions } from './FileToolOptions' interface FileActionButtonAccessFileProps { id: number @@ -83,16 +83,12 @@ export function AccessFileMenu({ isTabular={metadata.isTabular} userHasDownloadPermission={userHasDownloadPermission} /> - - + {userHasDownloadPermission && ( + <> + + + + )} ) From cf98a1978a41e3cbf5d9817ab0f45bcc5f164c11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Fri, 29 Aug 2025 16:30:20 -0300 Subject: [PATCH 31/51] feat: add stories and tests --- .../FileOptionsMenu.stories.tsx | 26 +++++ .../EditFileDropdown.stories.tsx | 36 ++++++ .../domain/models/ExternalToolsMother.ts | 14 ++- .../access-file-menu/FileToolOptions.spec.tsx | 110 ++++++++++++------ .../edit-file-menu/EditFileMenu.spec.tsx | 21 ++++ 5 files changed, 168 insertions(+), 39 deletions(-) diff --git a/src/stories/dataset/dataset-files/files-table/file-actions/file-action-buttons/file-options-menu/FileOptionsMenu.stories.tsx b/src/stories/dataset/dataset-files/files-table/file-actions/file-action-buttons/file-options-menu/FileOptionsMenu.stories.tsx index 76fdc9f8c..f3afe49b2 100644 --- a/src/stories/dataset/dataset-files/files-table/file-actions/file-action-buttons/file-options-menu/FileOptionsMenu.stories.tsx +++ b/src/stories/dataset/dataset-files/files-table/file-actions/file-action-buttons/file-options-menu/FileOptionsMenu.stories.tsx @@ -8,6 +8,10 @@ import { WithDatasetLockedFromEdits } from '../../../../../WithDatasetLockedFrom import { FilePreviewMother } from '../../../../../../../../tests/component/files/domain/models/FilePreviewMother' import { FileMockRepository } from '@/stories/file/FileMockRepository' import { DatasetMockRepository } from '@/stories/dataset/DatasetMockRepository' +import { ExternalToolsProvider } from '@/shared/contexts/external-tools/ExternalToolsProvider' +import { ExternalToolsMockRepository } from '@/stories/shared-mock-repositories/externalTools/ExternalToolsMockRepository' +import { FakerHelper } from '@tests/component/shared/FakerHelper' +import { ExternalToolsMother } from '@tests/component/externalTools/domain/models/ExternalToolsMother' const meta: Meta = { title: @@ -63,6 +67,28 @@ export const WithFileAlreadyDeleted: Story = { ) } +const externalToolsRepositoryWithFileConfigureTool = new ExternalToolsMockRepository() +externalToolsRepositoryWithFileConfigureTool.getExternalTools = () => { + return new Promise((resolve) => { + setTimeout(() => { + resolve([ExternalToolsMother.createFileConfigureTool()]) + }, FakerHelper.loadingTimout()) + }) +} + +export const WithConfigureTool: Story = { + decorators: [WithDatasetAllPermissionsGranted], + render: () => ( + + + + ) +} + // // export const WithEmbargoAllowed: Story = { // render: () => diff --git a/src/stories/file/file-action-buttons/edit-file-dropdown/EditFileDropdown.stories.tsx b/src/stories/file/file-action-buttons/edit-file-dropdown/EditFileDropdown.stories.tsx index c8205eb0a..31b83af68 100644 --- a/src/stories/file/file-action-buttons/edit-file-dropdown/EditFileDropdown.stories.tsx +++ b/src/stories/file/file-action-buttons/edit-file-dropdown/EditFileDropdown.stories.tsx @@ -5,6 +5,10 @@ import { EditFileMenu } from '@/sections/file/file-action-buttons/edit-file-menu import { FileMother } from '@tests/component/files/domain/models/FileMother' import { FileMockRepository } from '../../FileMockRepository' import { DatasetMockRepository } from '../../../dataset/DatasetMockRepository' +import { ExternalToolsProvider } from '@/shared/contexts/external-tools/ExternalToolsProvider' +import { ExternalToolsMockRepository } from '@/stories/shared-mock-repositories/externalTools/ExternalToolsMockRepository' +import { ExternalToolsMother } from '@tests/component/externalTools/domain/models/ExternalToolsMother' +import { FakerHelper } from '@tests/component/shared/FakerHelper' const storyFile = FileMother.createRealistic() @@ -32,6 +36,38 @@ export const Default: Story = { }} storageIdentifier="s3://10.5072/FK2/FNJFOR" isTabularFile={true} + fileType={storyFile.metadata.type.value} /> ) } + +const externalToolsRepositoryWithFileConfigureTool = new ExternalToolsMockRepository() +externalToolsRepositoryWithFileConfigureTool.getExternalTools = () => { + return new Promise((resolve) => { + setTimeout(() => { + resolve([ExternalToolsMother.createFileConfigureTool()]) + }, FakerHelper.loadingTimout()) + }) +} + +export const WithConfigureToolOption: Story = { + render: () => ( + + + + ) +} diff --git a/tests/component/externalTools/domain/models/ExternalToolsMother.ts b/tests/component/externalTools/domain/models/ExternalToolsMother.ts index 840ff907e..a43b50641 100644 --- a/tests/component/externalTools/domain/models/ExternalToolsMother.ts +++ b/tests/component/externalTools/domain/models/ExternalToolsMother.ts @@ -7,7 +7,8 @@ export class ExternalToolsMother { this.createDatasetExploreTool(), this.createFilePreviewTool(), this.createFileExploreTool(), - this.createFileQueryTool() + this.createFileQueryTool(), + this.createFileConfigureTool() ].map((tool) => ({ ...tool, ...props })) } @@ -63,4 +64,15 @@ export class ExternalToolsMother { contentType: 'text/plain' } } + + static createFileConfigureTool(): ExternalTool { + return { + id: 6, + displayName: 'File Configure Tool', + description: 'Description for File Configure Tool', + scope: ToolScope.File, + types: [ToolType.Configure], + contentType: 'text/plain' + } + } } diff --git a/tests/component/sections/file/file-action-buttons/access-file-menu/FileToolOptions.spec.tsx b/tests/component/sections/file/file-action-buttons/access-file-menu/FileToolOptions.spec.tsx index 8621c02ab..0514bb029 100644 --- a/tests/component/sections/file/file-action-buttons/access-file-menu/FileToolOptions.spec.tsx +++ b/tests/component/sections/file/file-action-buttons/access-file-menu/FileToolOptions.spec.tsx @@ -1,7 +1,8 @@ import { ExternalToolsRepository } from '@/externalTools/domain/repositories/ExternalToolsRepository' import { - FileExploreOptions, - FileQueryOptions + FileExploreToolsOptions, + FileQueryToolsOptions, + FileConfigureToolsOptions } from '@/sections/file/file-action-buttons/access-file-menu/FileToolOptions' import { ExternalToolsProvider } from '@/shared/contexts/external-tools/ExternalToolsProvider' import { ExternalToolsMother } from '@tests/component/externalTools/domain/models/ExternalToolsMother' @@ -14,53 +15,86 @@ describe('FileToolOptions', () => { .stub() .resolves([ ExternalToolsMother.createFileExploreTool(), - ExternalToolsMother.createFileQueryTool() + ExternalToolsMother.createFileQueryTool(), + ExternalToolsMother.createFileConfigureTool() ]) }) - it('renders the tool options if user has download permission and file explore tools are available and compatible with the type', () => { - cy.customMount( - - - - ) + describe('FileExploreToolsOptions', () => { + it('renders the tool options if file explore tools are available and compatible with the type', () => { + cy.customMount( + + + + ) - cy.findByText('Query Options').should('not.exist') - cy.findByText('Explore Options').should('exist') - cy.findByText('File Explore Tool').should('exist') - }) + cy.findByText('Query Options').should('not.exist') + cy.findByText('Configure Options').should('not.exist') + cy.findByText('Explore Options').should('exist') + cy.findByText('File Explore Tool').should('exist') + }) - it('renders the tool options if user has download permission and file query tools are available and compatible with the type', () => { - cy.customMount( - - - - ) + it('does not render the tool options if there are not applicable tools for the file type', () => { + cy.customMount( + + + + ) - cy.findByText('Explore Options').should('not.exist') - cy.findByText('Query Options').should('exist') - cy.findByText('File Query Tool').should('exist') + cy.findByText('Explore Options').should('not.exist') + cy.findByText('Query Options').should('not.exist') + }) }) - it('does not render the tool options if user lacks download permission', () => { - cy.customMount( - - - - ) + describe('FileQueryToolsOptions', () => { + it('renders the tool options if file query tools are available and compatible with the type', () => { + cy.customMount( + + + + ) + + cy.findByText('Explore Options').should('not.exist') + cy.findByText('Configure Options').should('not.exist') + cy.findByText('Query Options').should('exist') + cy.findByText('File Query Tool').should('exist') + }) + + it('does not render the tool options if there are not applicable tools for the file type', () => { + cy.customMount( + + + + ) - cy.findByText('Explore Options').should('not.exist') - cy.findByText('Query Options').should('not.exist') + cy.findByText('Explore Options').should('not.exist') + cy.findByText('Query Options').should('not.exist') + }) }) - it('does not render the tool options if there are not applicable tools for the file type', () => { - cy.customMount( - - - - ) + describe('FileConfigureToolsOptions', () => { + it('renders the tool options if file configure tools are available and compatible with the type', () => { + cy.customMount( + + + + ) + + cy.findByText('Explore Options').should('not.exist') + cy.findByText('Query Options').should('not.exist') + cy.findByText('Configure Options').should('exist') + cy.findByText('File Configure Tool').should('exist') + }) + + it('does not render the tool options if there are not applicable tools for the file type', () => { + cy.customMount( + + + + ) - cy.findByText('Explore Options').should('not.exist') - cy.findByText('Query Options').should('not.exist') + cy.findByText('Explore Options').should('not.exist') + cy.findByText('Query Options').should('not.exist') + }) }) }) diff --git a/tests/component/sections/file/file-action-buttons/edit-file-menu/EditFileMenu.spec.tsx b/tests/component/sections/file/file-action-buttons/edit-file-menu/EditFileMenu.spec.tsx index b77a01a9e..f3d4332d5 100644 --- a/tests/component/sections/file/file-action-buttons/edit-file-menu/EditFileMenu.spec.tsx +++ b/tests/component/sections/file/file-action-buttons/edit-file-menu/EditFileMenu.spec.tsx @@ -24,6 +24,7 @@ describe('EditFileMenu', () => { storageIdentifier="s3://10.5072/FK2/FNJFOR" isTabularFile={true} datasetRepository={new DatasetMockRepository()} + fileType={testFile.metadata.type.value} /> ) @@ -62,6 +63,7 @@ describe('EditFileMenu', () => { isTabularFile={true} storageIdentifier="non-s3://10.5072/FK2/FNJFOR" datasetRepository={new DatasetMockRepository()} + fileType={testFile.metadata.type.value} /> ) @@ -84,6 +86,7 @@ describe('EditFileMenu', () => { storageIdentifier="s3://10.5072/FK2/FNJFOR" isTabularFile={true} datasetRepository={new DatasetMockRepository()} + fileType={testFile.metadata.type.value} /> ) @@ -111,6 +114,7 @@ describe('EditFileMenu', () => { storageIdentifier="s3://10.5072/FK2/FNJFOR" isTabularFile={true} datasetRepository={new DatasetMockRepository()} + fileType={testFile.metadata.type.value} /> ) @@ -138,6 +142,7 @@ describe('EditFileMenu', () => { storageIdentifier="s3://10.5072/FK2/FNJFOR" isTabularFile={true} datasetRepository={new DatasetMockRepository()} + fileType={testFile.metadata.type.value} /> ) @@ -174,6 +179,7 @@ describe('EditFileMenu', () => { storageIdentifier="s3://10.5072/FK2/FNJFOR" isTabularFile={true} datasetRepository={new DatasetMockRepository()} + fileType={testFile.metadata.type.value} /> ) @@ -204,6 +210,7 @@ describe('EditFileMenu', () => { storageIdentifier="s3://10.5072/FK2/FNJFOR" isTabularFile={true} datasetRepository={new DatasetMockRepository()} + fileType={testFile.metadata.type.value} /> ) @@ -233,6 +240,7 @@ describe('EditFileMenu', () => { storageIdentifier="s3://10.5072/FK2/FNJFOR" isTabularFile={true} datasetRepository={new DatasetMockRepository()} + fileType={testFile.metadata.type.value} /> ) @@ -263,6 +271,7 @@ describe('EditFileMenu', () => { storageIdentifier="s3://10.5072/FK2/FNJFOR" isTabularFile={true} datasetRepository={new DatasetMockRepository()} + fileType={testFile.metadata.type.value} /> ) @@ -293,6 +302,7 @@ describe('EditFileMenu', () => { storageIdentifier="s3://10.5072/FK2/FNJFOR" isTabularFile={true} datasetRepository={new DatasetMockRepository()} + fileType={testFile.metadata.type.value} /> ) @@ -320,6 +330,7 @@ describe('EditFileMenu', () => { storageIdentifier="s3://10.5072/FK2/FNJFOR" isTabularFile={true} datasetRepository={new DatasetMockRepository()} + fileType={testFile.metadata.type.value} /> ) @@ -349,6 +360,7 @@ describe('EditFileMenu', () => { storageIdentifier="s3://10.5072/FK2/FNJFOR" isTabularFile={true} datasetRepository={new DatasetMockRepository()} + fileType={testFile.metadata.type.value} /> ) @@ -384,6 +396,7 @@ describe('EditFileMenu', () => { storageIdentifier="s3://10.5072/FK2/FNJFOR" isTabularFile={true} datasetRepository={new DatasetMockRepository()} + fileType={testFile.metadata.type.value} /> ) @@ -414,6 +427,7 @@ describe('EditFileMenu', () => { storageIdentifier="s3://10.5072/FK2/FNJFOR" isTabularFile={true} datasetRepository={new DatasetMockRepository()} + fileType={testFile.metadata.type.value} /> ) @@ -441,6 +455,7 @@ describe('EditFileMenu', () => { storageIdentifier="s3://10.5072/FK2/FNJFOR" isTabularFile={true} datasetRepository={new DatasetMockRepository()} + fileType={testFile.metadata.type.value} /> ) @@ -474,6 +489,7 @@ describe('EditFileMenu', () => { storageIdentifier="s3://10.5072/FK2/FNJFOR" isTabularFile={true} datasetRepository={new DatasetMockRepository()} + fileType={testFile.metadata.type.value} /> ) @@ -501,6 +517,7 @@ describe('EditFileMenu', () => { storageIdentifier="s3://10.5072/FK2/FNJFOR" isTabularFile={true} datasetRepository={new DatasetMockRepository()} + fileType={testFile.metadata.type.value} /> ) @@ -526,6 +543,7 @@ describe('EditFileMenu', () => { storageIdentifier="s3://10.5072/FK2/FNJFOR" isTabularFile={true} datasetRepository={new DatasetMockRepository()} + fileType={testFile.metadata.type.value} /> ) @@ -560,6 +578,7 @@ describe('EditFileMenu', () => { storageIdentifier="s3://10.5072/FK2/FNJFOR" isTabularFile={true} datasetRepository={new DatasetMockRepository()} + fileType={testFile.metadata.type.value} /> ) @@ -589,6 +608,7 @@ describe('EditFileMenu', () => { storageIdentifier="s3://10.5072/FK2/FNJFOR" isTabularFile={true} datasetRepository={new DatasetMockRepository()} + fileType={testFile.metadata.type.value} /> ) @@ -624,6 +644,7 @@ describe('EditFileMenu', () => { isTabularFile={true} datasetRepository={new DatasetMockRepository()} storageIdentifier="s3://10.5072/FK2/FNJFOR" + fileType={testFile.metadata.type.value} /> ) }) From 1afd0ca5a768a965cc4318d9585032fbe075f4cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Fri, 29 Aug 2025 17:34:41 -0300 Subject: [PATCH 32/51] test: improve some test to reach 95% --- .../api-token-section/ApiTokenSection.tsx | 8 ++-- .../sections/account/ApiTokenSection.spec.tsx | 24 +++++++++++ .../RequestAccessModal.spec.tsx | 17 +++++++- .../component/sections/file/useFile.spec.tsx | 42 +++++++++++++++++++ 4 files changed, 87 insertions(+), 4 deletions(-) create mode 100644 tests/component/sections/file/useFile.spec.tsx diff --git a/src/sections/account/api-token-section/ApiTokenSection.tsx b/src/sections/account/api-token-section/ApiTokenSection.tsx index f63edb090..90fe99a85 100644 --- a/src/sections/account/api-token-section/ApiTokenSection.tsx +++ b/src/sections/account/api-token-section/ApiTokenSection.tsx @@ -53,8 +53,8 @@ export const ApiTokenSection = ({ repository }: ApiTokenSectionProps) => { }) } - const copyToClipboard = () => { - navigator.clipboard.writeText(currentApiTokenInfo?.apiToken ?? '').catch( + const copyToClipboard = (currentApiToken: string) => { + navigator.clipboard.writeText(currentApiToken).catch( /* istanbul ignore next */ (error) => { console.error('Failed to copy text:', error) } @@ -104,7 +104,9 @@ export const ApiTokenSection = ({ repository }: ApiTokenSectionProps) => { {currentApiTokenInfo.apiToken}
- ) cy.findByText(clickMeText).should('have.attr', 'type').and('eq', 'submit') }) + + it('renders the button with the spacing styles when the spacing prop is provided', () => { + cy.mount() + cy.findByText(clickMeText).should('have.css', 'margin').and('not.eq', '0px') + }) }) diff --git a/packages/design-system/tests/component/progress-bar/ProgressBar.spec.tsx b/packages/design-system/tests/component/progress-bar/ProgressBar.spec.tsx index 6cd32d30e..11965ba27 100644 --- a/packages/design-system/tests/component/progress-bar/ProgressBar.spec.tsx +++ b/packages/design-system/tests/component/progress-bar/ProgressBar.spec.tsx @@ -6,4 +6,10 @@ describe('ProgressBar', () => { cy.findByRole('progressbar').should('exist') }) + + it('renders the ProgressBar without a now progress', () => { + cy.mount() + + cy.findByRole('progressbar').should('exist') + }) }) diff --git a/packages/design-system/tests/component/tabs/Tabs.spec.tsx b/packages/design-system/tests/component/tabs/Tabs.spec.tsx index d88c698a8..07d78fd5f 100644 --- a/packages/design-system/tests/component/tabs/Tabs.spec.tsx +++ b/packages/design-system/tests/component/tabs/Tabs.spec.tsx @@ -94,4 +94,40 @@ describe('Tabs', () => { cy.findByText('Tab 2').click() cy.get('@onSelect').should('have.been.calledWith', 'key-2') }) + + it('warns if activeKey is provided without onSelect', () => { + cy.stub(console, 'warn').as('consoleWarn') + cy.mount( + + + Content 1 + + + Content 2 + + + ) + cy.get('@consoleWarn').should( + 'have.been.calledWith', + 'Tabs component requires onSelect function when activeKey is provided' + ) + }) + + it('warns if neither activeKey nor defaultActiveKey is provided', () => { + cy.stub(console, 'warn').as('consoleWarn') + cy.mount( + + + Content 1 + + + Content 2 + + + ) + cy.get('@consoleWarn').should( + 'have.been.calledWith', + 'Tabs component requires either activeKey or defaultActiveKey' + ) + }) }) diff --git a/packages/design-system/tests/component/transfer-list/TransferList.spec.tsx b/packages/design-system/tests/component/transfer-list/TransferList.spec.tsx index 9e1e19a83..7d2f663e8 100644 --- a/packages/design-system/tests/component/transfer-list/TransferList.spec.tsx +++ b/packages/design-system/tests/component/transfer-list/TransferList.spec.tsx @@ -317,6 +317,54 @@ describe('TransferList', () => { }) }) + it('renders the TransferList disabled', () => { + cy.mount( + + ) + + cy.findByTestId('left-list-group').as('leftList') + cy.findByTestId('actions-column').as('actionsColumn') + cy.findByTestId('right-list-group').as('rightList') + + cy.get('@leftList').should('exist') + cy.get('@leftList').children().should('have.length', 3) + cy.get('@rightList').should('exist') + cy.get('@rightList').children().should('have.length', 2) + + cy.get('@actionsColumn').within(() => { + cy.findByLabelText('move all right').should('be.disabled') + cy.findByLabelText('move selected to right').should('be.disabled') + cy.findByLabelText('move selected to left').should('be.disabled') + cy.findByLabelText('move all left').should('be.disabled') + }) + + cy.get('@leftList').within(() => { + cy.findByLabelText('Item B').should('be.disabled') + cy.findByLabelText('Item D').should('be.disabled') + cy.findByLabelText('Item E').should('be.disabled') + }) + + cy.get('@rightList').within(() => { + cy.findByLabelText('Item A').should('be.disabled') + cy.findByLabelText('Item C').should('be.disabled') + }) + }) + describe('drag and drop', () => { it('should sort item A for Item B', () => { cy.mount( From 6778a6f8d3d0c6b5591a5ff96efb769a497716f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Tue, 30 Sep 2025 15:10:13 -0300 Subject: [PATCH 43/51] chore: storybook chromatic deployment fix --- .github/workflows/chromatic-design-system.yml | 28 ++++++++++++++---- .github/workflows/chromatic.yml | 29 +++++++++++++++---- package.json | 2 +- packages/design-system/package.json | 2 +- 4 files changed, 47 insertions(+), 14 deletions(-) diff --git a/.github/workflows/chromatic-design-system.yml b/.github/workflows/chromatic-design-system.yml index 386d297cc..89b936f70 100644 --- a/.github/workflows/chromatic-design-system.yml +++ b/.github/workflows/chromatic-design-system.yml @@ -14,21 +14,37 @@ jobs: runs-on: ubuntu-latest # Job steps steps: - - uses: actions/checkout@v1 + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.ref || github.ref }} + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm - name: Create .npmrc + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | cp .npmrc.example .npmrc - sed -i -e 's//${{ secrets.GITHUB_TOKEN }}/g' .npmrc - sed -i -e 's//${{ secrets.NPM_AUTH_TOKEN }}/g' .npmrc + sed -i -e "s##${GITHUB_TOKEN}#g" .npmrc + # Comment out npmjs auth token line if no token provided via secrets + sed -i -e 's#//registry.npmjs.org/:_authToken=#; auth token omitted in CI#g' .npmrc - name: Install dependencies - # 👇 Install dependencies with the same package manager used in the project (replace it as needed), e.g. yarn, npm, pnpm - run: npm install + # 👇 Use clean install for reproducibility + run: npm ci - name: Build Dataverse Design System working-directory: packages/design-system run: npm run build # 👇 Adds Chromatic as a step in the workflow - name: Publish to Chromatic - uses: chromaui/action@v1 + uses: chromaui/action@latest + env: + CHROMATIC_BRANCH: ${{ github.event.pull_request.head.ref || github.ref_name }} + CHROMATIC_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + CHROMATIC_SLUG: ${{ github.repository }} # Chromatic GitHub Action options with: # 👇 Chromatic projectToken, refer to the manage page to obtain it. diff --git a/.github/workflows/chromatic.yml b/.github/workflows/chromatic.yml index 74132bdf9..3c8ba93c2 100644 --- a/.github/workflows/chromatic.yml +++ b/.github/workflows/chromatic.yml @@ -14,24 +14,41 @@ jobs: runs-on: ubuntu-latest # Job steps steps: - - uses: actions/checkout@v1 + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + # Ensure correct ref on PRs for proper baseline detection + ref: ${{ github.event.pull_request.head.ref || github.ref }} + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm - name: Create .npmrc + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | cp .npmrc.example .npmrc - sed -i -e 's//${{ secrets.GITHUB_TOKEN }}/g' .npmrc - sed -i -e 's//${{ secrets.NPM_AUTH_TOKEN }}/g' .npmrc + sed -i -e "s##${GITHUB_TOKEN}#g" .npmrc + # Comment out npmjs auth token line if no token provided via secrets + sed -i -e 's#//registry.npmjs.org/:_authToken=#; auth token omitted in CI#g' .npmrc - name: Install dependencies - # 👇 Install dependencies with the same package manager used in the project (replace it as needed), e.g. yarn, npm, pnpm - run: npm install + # 👇 Use clean install for reproducible CI installs + run: npm ci # 👇 Adds Chromatic as a step in the workflow # Install design system dependencies - name: Build Dataverse Design System working-directory: packages/design-system run: npm run build - name: Publish to Chromatic - uses: chromaui/action@v1 + uses: chromaui/action@latest env: STORYBOOK_CHROMATIC_BUILD: 'true' + # Provide PR metadata to Chromatic for correct git/baseline context + CHROMATIC_BRANCH: ${{ github.event.pull_request.head.ref || github.ref_name }} + CHROMATIC_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + CHROMATIC_SLUG: ${{ github.repository }} # Chromatic GitHub Action options with: diff --git a/package.json b/package.json index e380c2d3b..ef7f6edfc 100644 --- a/package.json +++ b/package.json @@ -141,7 +141,7 @@ "babel-plugin-named-exports-order": "0.0.2", "chai": "4.3.7", "chai-as-promised": "7.1.1", - "chromatic": "6.24.1", + "chromatic": "^13.3.0", "concurrently": "8.0.1", "cypress": "15.2.0", "cypress-vite": "1.4.0", diff --git a/packages/design-system/package.json b/packages/design-system/package.json index ab9d86a4c..f4658fc47 100644 --- a/packages/design-system/package.json +++ b/packages/design-system/package.json @@ -57,7 +57,7 @@ "@testing-library/cypress": "10.1.0", "@vitejs/plugin-react": "4.3.1", "axe-playwright": "1.2.3", - "chromatic": "6.24.1", + "chromatic": "^13.3.0", "cypress": "15.2.0", "react": "18.2.0", "vite": "5.4.20", From ace1d9a48eb5a976d61fcbd576ae1a369cc084fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Tue, 30 Sep 2025 15:13:54 -0300 Subject: [PATCH 44/51] chore: unsync deps fix --- package-lock.json | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 16a5a068a..543455944 100644 --- a/package-lock.json +++ b/package-lock.json @@ -81,7 +81,7 @@ "babel-plugin-named-exports-order": "0.0.2", "chai": "4.3.7", "chai-as-promised": "7.1.1", - "chromatic": "6.24.1", + "chromatic": "^13.3.0", "concurrently": "8.0.1", "cypress": "15.2.0", "cypress-vite": "1.4.0", @@ -1954,9 +1954,9 @@ }, "node_modules/@iqss/dataverse-client-javascript": { "name": "@IQSS/dataverse-client-javascript", - "version": "2.0.0-alpha.66", - "resolved": "https://npm.pkg.github.com/download/@IQSS/dataverse-client-javascript/2.0.0-alpha.66/5eac3e19da454f634e409469958c848b70283c16", - "integrity": "sha512-YGDUC/nk2nqmlq5DPNNbnt5KTABZAk+HCLuw90zg/8hWVhU8RSc2fRDeSuc/CQsV/NmCSw6gzhr5FsCsKitdEQ==", + "version": "2.0.0-alpha.67", + "resolved": "https://npm.pkg.github.com/download/@IQSS/dataverse-client-javascript/2.0.0-alpha.67/0ee4f1c8e03eb3ef688d6b034e84003c9e155d3b", + "integrity": "sha512-uEAGtwXz7LYkBfWCBRktgb5d8oba6yPH9YWnVFhI40UqgdB1sQ/WWCDhZTn5LFsaQY+1XBzOoTjwrQ2HGz94og==", "license": "MIT", "dependencies": { "@types/node": "^18.15.11", @@ -10651,15 +10651,27 @@ } }, "node_modules/chromatic": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/chromatic/-/chromatic-6.24.1.tgz", - "integrity": "sha512-XbpdWWHvFpEHtcq1Km71UcuQ07effB+8q8L47E1Y7HJmJ4ZCoKCuPd8liNrbnvwEAxqfBZvTcONYU/3BPz2i5w==", + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/chromatic/-/chromatic-13.3.0.tgz", + "integrity": "sha512-OtXVKSFqGS1x6E6xYzmYX2iImSknbvo5CfTxP3ztFvXQhIAwhJzJuA3XpnIewER9gtWUNnV2DDi8/f9JyZJSfg==", "dev": true, "license": "MIT", "bin": { "chroma": "dist/bin.js", "chromatic": "dist/bin.js", "chromatic-cli": "dist/bin.js" + }, + "peerDependencies": { + "@chromatic-com/cypress": "^0.*.* || ^1.0.0", + "@chromatic-com/playwright": "^0.*.* || ^1.0.0" + }, + "peerDependenciesMeta": { + "@chromatic-com/cypress": { + "optional": true + }, + "@chromatic-com/playwright": { + "optional": true + } } }, "node_modules/ci-info": { @@ -30389,7 +30401,7 @@ "@testing-library/cypress": "10.1.0", "@vitejs/plugin-react": "4.3.1", "axe-playwright": "1.2.3", - "chromatic": "6.24.1", + "chromatic": "^13.3.0", "cypress": "15.2.0", "react": "18.2.0", "vite": "5.4.20", From 03cb7c59bb05a0cc5aee85e20f66cf43b3c4171c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Tue, 30 Sep 2025 15:31:05 -0300 Subject: [PATCH 45/51] chore: remove caret from chromatic dep --- package.json | 2 +- packages/design-system/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index ef7f6edfc..34008bc0b 100644 --- a/package.json +++ b/package.json @@ -141,7 +141,7 @@ "babel-plugin-named-exports-order": "0.0.2", "chai": "4.3.7", "chai-as-promised": "7.1.1", - "chromatic": "^13.3.0", + "chromatic": "13.3.0", "concurrently": "8.0.1", "cypress": "15.2.0", "cypress-vite": "1.4.0", diff --git a/packages/design-system/package.json b/packages/design-system/package.json index f4658fc47..cbd4bb2b2 100644 --- a/packages/design-system/package.json +++ b/packages/design-system/package.json @@ -57,7 +57,7 @@ "@testing-library/cypress": "10.1.0", "@vitejs/plugin-react": "4.3.1", "axe-playwright": "1.2.3", - "chromatic": "^13.3.0", + "chromatic": "13.3.0", "cypress": "15.2.0", "react": "18.2.0", "vite": "5.4.20", From a6e5921839be29af876bb9b8dfbd7acc1e2a9301 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Tue, 30 Sep 2025 16:39:07 -0300 Subject: [PATCH 46/51] test: code coverage --- .../top-fields-section/IdentifierField.tsx | 6 +- .../dataset-metadata/DatasetMetadata.spec.tsx | 71 +++++++++++++++++++ .../file-embargo/FileEmbargoDate.spec.tsx | 13 ++++ 3 files changed, 88 insertions(+), 2 deletions(-) diff --git a/src/sections/shared/form/EditCreateCollectionForm/collection-form/top-fields-section/IdentifierField.tsx b/src/sections/shared/form/EditCreateCollectionForm/collection-form/top-fields-section/IdentifierField.tsx index 3b2421ef9..58955cc4d 100644 --- a/src/sections/shared/form/EditCreateCollectionForm/collection-form/top-fields-section/IdentifierField.tsx +++ b/src/sections/shared/form/EditCreateCollectionForm/collection-form/top-fields-section/IdentifierField.tsx @@ -19,9 +19,11 @@ export const collectionNameToAlias = (name: string) => { // This is only to avoid difference snapshots in Chromatic builds, the real display origin will be the current window location const locationOrigin = - import.meta.env.STORYBOOK_CHROMATIC_BUILD === 'true' ? 'https://foo.com' : window.location.origin + import.meta.env.STORYBOOK_CHROMATIC_BUILD === 'true' + ? /* istanbul ignore next */ 'https://foo.com' + : window.location.origin -const BASENAME_URL = import.meta.env.BASE_URL ?? '' +const BASENAME_URL = import.meta.env.BASE_URL ?? /* istanbul ignore next */ '' interface IdentifierFieldProps { rules: UseControllerProps['rules'] diff --git a/tests/component/sections/dataset/dataset-metadata/DatasetMetadata.spec.tsx b/tests/component/sections/dataset/dataset-metadata/DatasetMetadata.spec.tsx index cec9d3e19..f11a2ac25 100644 --- a/tests/component/sections/dataset/dataset-metadata/DatasetMetadata.spec.tsx +++ b/tests/component/sections/dataset/dataset-metadata/DatasetMetadata.spec.tsx @@ -413,4 +413,75 @@ describe('DatasetMetadata', () => { cy.findByRole('button', { name: 'Citation Metadata' }).should('exist') cy.findByRole('button', { name: 'Geospatial Metadata' }).should('not.exist') }) + + it('adds name and description to extra fields of the citation metadata block', () => { + const mockDatasetWithExtraFields = DatasetMother.create({ + metadataBlocks: [ + { + name: MetadataBlockName.CITATION, + fields: { + title: 'Some Title', + subject: ['subject-one', 'subject-two'], + author: [ + { + authorName: 'Foo', + authorAffiliation: 'Bar' + }, + { + authorName: 'Another Foo', + authorAffiliation: 'Another Bar' + } + ], + datasetContact: [ + { + datasetContactName: 'John Doe', + datasetContactEmail: 'john@doe.com', + datasetContactAffiliation: 'Doe Inc.' + } + ], + dsDescription: [ + { + dsDescriptionValue: 'Description of the dataset' + } + ], + producer: [ + { + producerName: 'Foo', + producerAffiliation: 'XYZ', + producerURL: 'http://foo.com', + producerLogoURL: + 'https://beta.dataverse.org/resources/images/dataverse_project_logo.svg' + } + ], + // Extra fields + publicationDate: '2023-01-01', + alternativePersistentId: 'some-alternative-pid' + } + } + ] + }) + + cy.customMount( + + ) + + cy.get('.accordion > :nth-child(1)').within(() => { + cy.findByText(/Citation Metadata/i).should('exist') + + cy.findByText('Persistent Identifier') + .parent() + .siblings('div') + .should('contain', mockDatasetWithExtraFields.persistentId) + + cy.findByText('Publication Date').should('exist') + cy.findByText('2023-01-01').should('exist') + + cy.findByText('Previous Dataset Persistent ID').should('exist') + cy.findByText('some-alternative-pid').should('exist') + }) + }) }) diff --git a/tests/component/sections/file/file-embargo/FileEmbargoDate.spec.tsx b/tests/component/sections/file/file-embargo/FileEmbargoDate.spec.tsx index d093fbe20..6e919c988 100644 --- a/tests/component/sections/file/file-embargo/FileEmbargoDate.spec.tsx +++ b/tests/component/sections/file/file-embargo/FileEmbargoDate.spec.tsx @@ -60,4 +60,17 @@ describe('FileEmbargoDate', () => { cy.findByText(`Embargoed until`).should('exist') cy.get('time').should('have.text', dateString) }) + + it('renders the embargo date in short format', () => { + const embargoDate = new Date('2123-09-18') + const embargo = FileEmbargoMother.create({ dateAvailable: embargoDate }) + const status = DatasetPublishingStatus.RELEASED + + cy.customMount( + + ) + const dateString = DateHelper.toDisplayFormat(embargoDate) + cy.findByText(`Embargoed until`).should('exist') + cy.get('time').should('have.text', dateString) + }) }) From 0bbe8eb4d03de3c0733171280ad42cd4195854df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Tue, 30 Sep 2025 16:52:51 -0300 Subject: [PATCH 47/51] test: fix e2e flaky test --- .../create-dataset/CreateDataset.spec.tsx | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/tests/e2e-integration/e2e/sections/create-dataset/CreateDataset.spec.tsx b/tests/e2e-integration/e2e/sections/create-dataset/CreateDataset.spec.tsx index 8b154cae9..66996ee8c 100644 --- a/tests/e2e-integration/e2e/sections/create-dataset/CreateDataset.spec.tsx +++ b/tests/e2e-integration/e2e/sections/create-dataset/CreateDataset.spec.tsx @@ -45,11 +45,25 @@ describe('Create Dataset', () => { cy.contains('Agricultural Sciences; Arts and Humanities').should('exist') }) - it('shows template select when a template is available and prefill fields when a template is selected', () => { - cy.wrap(DatasetHelper.createDatasetTemplate(), { timeout: 10000 }).then(() => { + describe('dataset template selection', () => { + let datasetTemplateId: number + + beforeEach(async () => { + await DatasetHelper.createDatasetTemplate() + const templates = await DatasetHelper.getDatasetTemplates() + + const { id } = templates[0] + datasetTemplateId = id + }) + + afterEach(async () => { + await DatasetHelper.deleteDatasetTemplate(datasetTemplateId) + }) + + it('shows template select when a template is available and prefill fields when a template is selected', () => { cy.visit(CREATE_DATASET_PAGE_URL) - cy.wait(1000) + cy.wait(3_000) cy.findByTestId('dataset-template-select').should('exist').as('templateSelect') cy.findByText('None').should('exist') // No default template, None is shown @@ -94,13 +108,6 @@ describe('Create Dataset', () => { cy.findByText(DatasetLabelValue.DRAFT).should('exist') cy.findByText(DatasetLabelValue.UNPUBLISHED).should('exist') cy.contains('Agricultural Sciences; Arts and Humanities').should('exist') - - // Delete template after test - cy.wrap(DatasetHelper.getDatasetTemplates(), { timeout: 10000 }).then((templates) => { - const { id } = templates[0] - - cy.wrap(DatasetHelper.deleteDatasetTemplate(id)) - }) }) }) From e200af01de0e7daf05bded5badf437c740c6ef64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Tue, 30 Sep 2025 16:57:30 -0300 Subject: [PATCH 48/51] fix: avoid showing alert warning if dataset templates fetch fails --- src/sections/create-dataset/CreateDataset.tsx | 3 --- .../sections/create-dataset/CreateDataset.spec.tsx | 14 -------------- 2 files changed, 17 deletions(-) diff --git a/src/sections/create-dataset/CreateDataset.tsx b/src/sections/create-dataset/CreateDataset.tsx index 043a894ab..6a71ecd78 100644 --- a/src/sections/create-dataset/CreateDataset.tsx +++ b/src/sections/create-dataset/CreateDataset.tsx @@ -119,9 +119,6 @@ export function CreateDataset({ /> )} - {/* If there is an error loading dataset templates we notify the user but dont block them from creating a dataset */} - {errorGetDatasetTemplates && {errorGetDatasetTemplates}} - { cy.findAllByText('Template 2').should('exist').should('have.length', 2) // Template 2 is selected, we see two }) - - it('shows the warning alert when there is an error loading the templates', () => { - datasetRepository.getTemplates = cy.stub().rejects() - - cy.customMount( - - ) - cy.findByText(/Something went wrong getting the dataset templates./) - }) }) }) From 99312a98ecd85fe3c172628b18f676b61c12b3ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Wed, 1 Oct 2025 09:20:42 -0300 Subject: [PATCH 49/51] fix: test after merge --- .../dataset/dataset-metadata/DatasetMetadata.spec.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/component/sections/dataset/dataset-metadata/DatasetMetadata.spec.tsx b/tests/component/sections/dataset/dataset-metadata/DatasetMetadata.spec.tsx index ef91cc1ac..7ab8d1e3a 100644 --- a/tests/component/sections/dataset/dataset-metadata/DatasetMetadata.spec.tsx +++ b/tests/component/sections/dataset/dataset-metadata/DatasetMetadata.spec.tsx @@ -477,9 +477,10 @@ describe('DatasetMetadata', () => { cy.customMount( ) From 0eaecf556e98dff25f6bac5d7634b1b53ad0a218 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Wed, 1 Oct 2025 10:13:37 -0300 Subject: [PATCH 50/51] test: improve coverage --- .../PublicationStatusFilters.tsx | 2 +- .../edit-dataset-menu/EditDatasetMenu.spec.tsx | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/sections/account/my-data-section/my-data-filter-panel/publication-status-filters/PublicationStatusFilters.tsx b/src/sections/account/my-data-section/my-data-filter-panel/publication-status-filters/PublicationStatusFilters.tsx index 1e6e5335e..3dd28992d 100644 --- a/src/sections/account/my-data-section/my-data-filter-panel/publication-status-filters/PublicationStatusFilters.tsx +++ b/src/sections/account/my-data-section/my-data-filter-panel/publication-status-filters/PublicationStatusFilters.tsx @@ -52,7 +52,7 @@ export const PublicationStatusFilters = ({ {t(`${publicationStatus}`)} ({count}) } - checked={currentPublicationStatuses?.includes(publicationStatus) ?? false} + checked={currentPublicationStatuses.includes(publicationStatus)} disabled={statusCheckDisabled} /> ) diff --git a/tests/component/sections/dataset/dataset-action-buttons/edit-dataset-menu/EditDatasetMenu.spec.tsx b/tests/component/sections/dataset/dataset-action-buttons/edit-dataset-menu/EditDatasetMenu.spec.tsx index 1419a559d..c77b60097 100644 --- a/tests/component/sections/dataset/dataset-action-buttons/edit-dataset-menu/EditDatasetMenu.spec.tsx +++ b/tests/component/sections/dataset/dataset-action-buttons/edit-dataset-menu/EditDatasetMenu.spec.tsx @@ -248,4 +248,22 @@ describe('EditDatasetMenu', () => { cy.findByRole('button', { name: 'Edit Dataset' }).click() cy.findByRole('button', { name: 'Files (Upload)' }).should('not.exist') }) + + it('renders the Edit Private URL if user can manage file permissions', () => { + const dataset = DatasetMother.create({ + permissions: DatasetPermissionsMother.create({ + canManageDatasetPermissions: false, + canManageFilesPermissions: true + }), + locks: [], + hasValidTermsOfAccess: true + }) + + cy.mountAuthenticated( + + ) + + cy.findByRole('button', { name: 'Edit Dataset' }).click() + cy.findByRole('button', { name: 'Private URL' }).should('exist') + }) }) From d3df63cde1935e832e48c2f2a3ea002329ab37bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Saracca?= Date: Wed, 1 Oct 2025 11:01:44 -0300 Subject: [PATCH 51/51] test: fix --- .../edit-dataset-menu/EditDatasetMenu.spec.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/component/sections/dataset/dataset-action-buttons/edit-dataset-menu/EditDatasetMenu.spec.tsx b/tests/component/sections/dataset/dataset-action-buttons/edit-dataset-menu/EditDatasetMenu.spec.tsx index c77b60097..1523c6616 100644 --- a/tests/component/sections/dataset/dataset-action-buttons/edit-dataset-menu/EditDatasetMenu.spec.tsx +++ b/tests/component/sections/dataset/dataset-action-buttons/edit-dataset-menu/EditDatasetMenu.spec.tsx @@ -252,6 +252,7 @@ describe('EditDatasetMenu', () => { it('renders the Edit Private URL if user can manage file permissions', () => { const dataset = DatasetMother.create({ permissions: DatasetPermissionsMother.create({ + canUpdateDataset: true, canManageDatasetPermissions: false, canManageFilesPermissions: true }),