diff --git a/.github/workflows/deploy-beta-testing.yml b/.github/workflows/deploy-beta-testing.yml new file mode 100644 index 000000000..cad70ab87 --- /dev/null +++ b/.github/workflows/deploy-beta-testing.yml @@ -0,0 +1,96 @@ +name: 'Deploy to Beta Testing' + +on: + push: + branches: + - develop + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-node@v3 + with: + node-version: 18 + cache: 'npm' + + - name: Create .npmrc + run: | + cp .npmrc.example .npmrc + sed -i -e 's//${{ secrets.GITHUB_TOKEN }}/g' .npmrc + sed -i -e 's//${{ secrets.NPM_AUTH_TOKEN }}/g' .npmrc + + - name: Install Dependencies + run: npm install + + - name: Build Dataverse UI Library + working-directory: packages/design-system + run: npm run build + + - name: Create and populate .env file + env: + DATAVERSE_BACKEND_URL: ${{ secrets.BETA_DATAVERSE_BACKEND_URL }} + run: | + touch .env + echo VITE_DATAVERSE_BACKEND_URL="$DATAVERSE_BACKEND_URL" >> .env + shell: bash + + - name: Build with base path + run: npm run build -- --base=/spa + + - uses: actions/upload-artifact@v3 + with: + name: built-site + path: ./dist + + deploy-to-payara: + needs: build + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-java@v3 + with: + distribution: 'zulu' + java-version: '11' + + - uses: actions/download-artifact@v3 + with: + name: built-site + path: ./dist + + - name: Get branch name + id: branch-name + uses: tj-actions/branch-names@v6 + + - name: Build war file + working-directory: ./deployment/payara + run: mvn package "-Dversion=${{ steps.branch-name.outputs.current_branch }}" + + - name: Copy war file to remote instance + uses: appleboy/scp-action@master + with: + host: ${{ secrets.BETA_PAYARA_INSTANCE_HOST }} + username: ${{ secrets.BETA_PAYARA_INSTANCE_USERNAME }} + key: ${{ secrets.BETA_PAYARA_INSTANCE_SSH_PRIVATE_KEY }} + source: './deployment/payara/target/dataverse-frontend.war' + target: '/home/${{ secrets.BETA_PAYARA_INSTANCE_USERNAME }}' + overwrite: true + + - name: Execute payara war deployment remotely + uses: appleboy/ssh-action@v0.1.8 + with: + host: ${{ secrets.BETA_PAYARA_INSTANCE_HOST }} + username: ${{ secrets.BETA_PAYARA_INSTANCE_USERNAME }} + key: ${{ secrets.BETA_PAYARA_INSTANCE_SSH_PRIVATE_KEY }} + script: | + APPLICATION_NAME=dataverse-frontend + APPLICATION_WAR_PATH=deployment/payara/target/$APPLICATION_NAME.war + ASADMIN='/usr/local/payara6/bin/asadmin --user admin' + DATAVERSE_FRONTEND=`$ASADMIN list-applications |grep $APPLICATION_NAME |awk '{print $1}'` + $ASADMIN undeploy $DATAVERSE_FRONTEND + $ASADMIN deploy --name $APPLICATION_NAME --contextroot /spa $APPLICATION_WAR_PATH diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 941e7d8ba..df1e495d7 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -143,7 +143,7 @@ jobs: script: | APPLICATION_NAME=dataverse-frontend APPLICATION_WAR_PATH=deployment/payara/target/$APPLICATION_NAME.war - ASADMIN='/usr/local/payara5/bin/asadmin --user admin' + ASADMIN='/usr/local/payara6/bin/asadmin --user admin' DATAVERSE_FRONTEND=`$ASADMIN list-applications |grep $APPLICATION_NAME |awk '{print $1}'` $ASADMIN undeploy $DATAVERSE_FRONTEND $ASADMIN deploy --name $APPLICATION_NAME --contextroot /${{ github.event.inputs.basepath }} $APPLICATION_WAR_PATH diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 001a50659..f6a07776f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -49,7 +49,7 @@ jobs: run: ./run-env.sh "$E2E_DATAVERSE_IMAGE_TAG" - name: Wait for containers to be ready - run: timeout 300s sh -c 'while ! docker logs dev_dataverse_bootstrap 2>&1 | grep -q "your instance has been configured"; do sleep 2; done' + run: timeout 360s sh -c 'while ! docker logs dev_dataverse_bootstrap 2>&1 | grep -q "your instance has been configured"; do sleep 2; done' - name: Run e2e tests run: npm run test:e2e diff --git a/README.md b/README.md index c689fccac..eecad2e5f 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,10 @@ [![Tests](https://github.com/IQSS/dataverse-frontend/actions/workflows/test.yml/badge.svg)](https://github.com/IQSS/dataverse-frontend/actions/workflows/test.yml) [![Unit Tests Coverage](https://coveralls.io/repos/github/IQSS/dataverse-frontend/badge.svg?branch=develop)](https://coveralls.io/github/IQSS/dataverse-frontend?branch=develop) +## Demo videos + +- 2023-08-01: [View mode of the dataset page](https://groups.google.com/g/dataverse-community/c/cxZ3Bal_-uo/m/h3kh3iVNCwAJ) + ## Getting Started First install node >=16 and npm >=8. Recommended versions `node v19` and `npm v9`. @@ -180,6 +184,19 @@ It is important that the remote instance is correctly pre-configured, with the P A base path for the frontend application can be established on the remote server by setting the corresponding field in the workflow inputs. This mechanism prevents conflicts between the frontend application and any pre-existing deployed application running on Payara, which can potentially be a Dataverse backend. This way, only the routes with the base path included will redirect to the frontend application. +#### Beta Testing Environment + +To make the SPA Frontend accesible and testable by people interested in the project, there is a remote beta testing environment that includes the latest changes developed both for the frontend application and the Dataverse backend application (develop branches). + +This environment follows the "all-in-one" solution described above, where both applications coexist on a Payara server. + +Environment updates are carried out automatically through GitHub actions, present both in this repository and in the Dataverse backend repository, which deploy the develop branches when any change is pushed to them. + +The environment is accessible through the following URLs: + +- SPA: https://beta.dataverse.org/spa +- JSF: https://beta.dataverse.org + ## Changes from the Style Guide The design system and frontend in this repo are inspired by the Dataverse Project [Style Guide](https://guides.dataverse.org/en/latest/style/index.html), but the following changes have been made, especially for accessibility. diff --git a/package-lock.json b/package-lock.json index 8996279a2..88abdd9d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.0", "dependencies": { "@faker-js/faker": "7.6.0", - "@iqss/dataverse-client-javascript": "2.0.0-pr67.5dfc70a", + "@iqss/dataverse-client-javascript": "2.0.0-pr80.378cc1e", "@iqss/dataverse-design-system": "*", "@istanbuljs/nyc-config-typescript": "1.0.2", "@tanstack/react-table": "8.9.2", @@ -3408,9 +3408,9 @@ }, "node_modules/@iqss/dataverse-client-javascript": { "name": "@IQSS/dataverse-client-javascript", - "version": "2.0.0-pr67.5dfc70a", - "resolved": "https://npm.pkg.github.com/download/@IQSS/dataverse-client-javascript/2.0.0-pr67.5dfc70a/dc27a9a220a347fb9e5d87f6887c8e87427482c1", - "integrity": "sha512-RCBJcdv42hYvWuLDE19/AYpKZr1AL9/VIre4FJm0vd0NMd1CLoPyDjetuK70nkP8fDDWVJLTlyaer8f1RZAfSA==", + "version": "2.0.0-pr80.378cc1e", + "resolved": "https://npm.pkg.github.com/download/@IQSS/dataverse-client-javascript/2.0.0-pr80.378cc1e/2361fc91f5e8618751025871e860062307b1adf4", + "integrity": "sha512-5Omv7ZG+K+B8ddEg0X3MjqbAK/cOPnPtclpBe2sptYRH1zaX1kHwSSTTBDmfNfIZTJUOe7KEP0/FX6Xa6iRG6w==", "license": "MIT", "dependencies": { "@types/node": "^18.15.11", diff --git a/package.json b/package.json index fca69e954..4baaa9c9a 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ }, "dependencies": { "@faker-js/faker": "7.6.0", - "@iqss/dataverse-client-javascript": "2.0.0-pr67.5dfc70a", + "@iqss/dataverse-client-javascript": "2.0.0-pr80.378cc1e", "@iqss/dataverse-design-system": "*", "@istanbuljs/nyc-config-typescript": "1.0.2", "@tanstack/react-table": "8.9.2", diff --git a/public/locales/en/files.json b/public/locales/en/files.json index cdc9a211c..6c7e8dfa1 100644 --- a/public/locales/en/files.json +++ b/public/locales/en/files.json @@ -68,10 +68,10 @@ "title": "Filter by" }, "filterByType": { - "title": "Filter Type" + "title": "File Type" }, "filterByTag": { - "title": "Filter Tag" + "title": "File Tags" }, "filterByAccess": { "title": "Access", diff --git a/src/dataset/infrastructure/mappers/JSDatasetMapper.ts b/src/dataset/infrastructure/mappers/JSDatasetMapper.ts index f14f4beb3..53a3a627a 100644 --- a/src/dataset/infrastructure/mappers/JSDatasetMapper.ts +++ b/src/dataset/infrastructure/mappers/JSDatasetMapper.ts @@ -24,7 +24,12 @@ export class JSDatasetMapper { citation, JSDatasetMapper.toSummaryFields(jsDataset.metadataBlocks, summaryFieldsNames), jsDataset.license, - JSDatasetMapper.toMetadataBlocks(jsDataset.metadataBlocks) // TODO Add alternativePersistentId, publicationDate, citationDate + JSDatasetMapper.toMetadataBlocks( + jsDataset.metadataBlocks, + jsDataset.alternativePersistentId, + jsDataset.publicationDate, + jsDataset.citationDate + ) ).build() } @@ -143,7 +148,7 @@ export class JSDatasetMapper { extraFields.publicationDate = publicationDate } - if (publicationDate && citationDate !== publicationDate) { + if (citationDate && citationDate !== publicationDate) { extraFields.citationDate = citationDate } diff --git a/src/dataset/infrastructure/repositories/DatasetJSDataverseRepository.ts b/src/dataset/infrastructure/repositories/DatasetJSDataverseRepository.ts index 206719395..ea25c0aa2 100644 --- a/src/dataset/infrastructure/repositories/DatasetJSDataverseRepository.ts +++ b/src/dataset/infrastructure/repositories/DatasetJSDataverseRepository.ts @@ -1,7 +1,7 @@ import { DatasetRepository } from '../../domain/repositories/DatasetRepository' import { Dataset } from '../../domain/models/Dataset' import { - getDatasetByPersistentId, + getDataset, getDatasetCitation, getDatasetSummaryFieldNames, WriteError, @@ -13,7 +13,7 @@ import { JSDatasetMapper } from '../mappers/JSDatasetMapper' export class DatasetJSDataverseRepository implements DatasetRepository { getByPersistentId(persistentId: string, version?: string): Promise { - return getDatasetByPersistentId + return getDataset .execute(persistentId, this.versionToVersionId(version)) .then((jsDataset) => Promise.all([ diff --git a/src/files/domain/models/FilePaginationInfo.ts b/src/files/domain/models/FilePaginationInfo.ts index 1d6b2a3d6..5f0e1ddcc 100644 --- a/src/files/domain/models/FilePaginationInfo.ts +++ b/src/files/domain/models/FilePaginationInfo.ts @@ -2,14 +2,14 @@ export class FilePaginationInfo { constructor( public readonly page: number = 1, public readonly pageSize: number = 10, - public readonly total: number = 0 + public readonly totalFiles: number = 0 ) {} withTotal(total: number): FilePaginationInfo { return new FilePaginationInfo(this.page, this.pageSize, total) } goToPage(page: number): FilePaginationInfo { - return new FilePaginationInfo(page, this.pageSize, this.total) + return new FilePaginationInfo(page, this.pageSize, this.totalFiles) } goToPreviousPage(): FilePaginationInfo { @@ -27,11 +27,11 @@ export class FilePaginationInfo { const newPage = Math.ceil((this.page * oldPageSize) / newPageSize) return newPage > 0 ? newPage : 1 } - return new FilePaginationInfo(getNewPage(this.pageSize, pageSize), pageSize, this.total) + return new FilePaginationInfo(getNewPage(this.pageSize, pageSize), pageSize, this.totalFiles) } get totalPages(): number { - return Math.ceil(this.total / this.pageSize) + return Math.ceil(this.totalFiles / this.pageSize) } get hasPreviousPage(): boolean { diff --git a/src/metadata-block-info/domain/models/MetadataBlockInfo.ts b/src/metadata-block-info/domain/models/MetadataBlockInfo.ts index bffa2bcc8..7fa9e53a3 100644 --- a/src/metadata-block-info/domain/models/MetadataBlockInfo.ts +++ b/src/metadata-block-info/domain/models/MetadataBlockInfo.ts @@ -1,10 +1,13 @@ export interface MetadataBlockInfo { name: string - fields: Record + fields: MetadataBlockInfoFields } +export type MetadataBlockInfoFields = Record + export interface MetadataFieldInfo { displayFormat: string } export const METADATA_FIELD_DISPLAY_FORMAT_PLACEHOLDER = '#VALUE' +export const METADATA_FIELD_DISPLAY_FORMAT_NAME_PLACEHOLDER = '#NAME' diff --git a/src/metadata-block-info/infrastructure/MetadataBlockInfoJSDataverseRepository.ts b/src/metadata-block-info/infrastructure/MetadataBlockInfoJSDataverseRepository.ts deleted file mode 100644 index 6ce2e94c4..000000000 --- a/src/metadata-block-info/infrastructure/MetadataBlockInfoJSDataverseRepository.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { MetadataBlockInfoRepository } from '../domain/repositories/MetadataBlockInfoRepository' -import { MetadataBlockInfo } from '../domain/models/MetadataBlockInfo' -import { MetadataBlockName } from '../../dataset/domain/models/Dataset' - -export class MetadataBlockInfoJSDataverseRepository implements MetadataBlockInfoRepository { - // eslint-disable-next-line unused-imports/no-unused-vars - getByName(name: string): Promise { - // TODO implement using js-dataverse - return Promise.resolve({ - name: MetadataBlockName.CITATION, - fields: { - alternativePersistentId: { displayFormat: '' }, - publicationDate: { displayFormat: '' }, - citationDate: { displayFormat: '' }, - title: { displayFormat: '' }, - subject: { displayFormat: ';' }, - author: { displayFormat: '' }, - authorName: { displayFormat: '#VALUE' }, - authorAffiliation: { displayFormat: '(#VALUE)' }, - authorIdentifierScheme: { displayFormat: '- #VALUE:' }, - authorIdentifier: { displayFormat: '#VALUE' }, - datasetContact: { displayFormat: '#VALUE' }, - datasetContactName: { displayFormat: '#VALUE' }, - datasetContactAffiliation: { displayFormat: '(#VALUE)' }, - datasetContactEmail: { displayFormat: '[#VALUE](mailto:#VALUE)' }, - dsDescription: { displayFormat: '' }, - dsDescriptionValue: { displayFormat: '#VALUE' } - } - }) - } -} diff --git a/src/metadata-block-info/infrastructure/mappers/JSMetadataBlockInfoMapper.ts b/src/metadata-block-info/infrastructure/mappers/JSMetadataBlockInfoMapper.ts new file mode 100644 index 000000000..d345c9c66 --- /dev/null +++ b/src/metadata-block-info/infrastructure/mappers/JSMetadataBlockInfoMapper.ts @@ -0,0 +1,51 @@ +import { + MetadataBlock as JSMetadataBlockInfo, + MetadataFieldInfo as JSMetadataFieldInfo +} from '@iqss/dataverse-client-javascript' +import { MetadataBlockInfo, MetadataBlockInfoFields } from '../../domain/models/MetadataBlockInfo' + +export class JSMetadataBlockInfoMapper { + static toMetadataBlockInfo(jsMetadataBlockInfo: JSMetadataBlockInfo): MetadataBlockInfo { + return { + name: jsMetadataBlockInfo.name, + fields: this.toFields(jsMetadataBlockInfo.metadataFields) + } + } + + static toFields( + jsMetadataBlockInfoFields: Record + ): MetadataBlockInfoFields { + return Object.entries(jsMetadataBlockInfoFields).reduce( + (fields: MetadataBlockInfoFields, [key, value]) => { + fields[key] = { displayFormat: this.toDisplayFormat(value.displayFormat) } + return fields + }, + {} + ) + } + + static toDisplayFormat(jsDisplayFormat: string): string { + const link = 'href="#VALUE"' + if (jsDisplayFormat.includes(link)) { + return '[#VALUE](#VALUE)' + } + + const linkWithUrl = /]*>#VALUE<\/a>/ + const match = jsDisplayFormat.match(linkWithUrl) + if (match) { + return `[#VALUE](${match[1]}/#VALUE)` + } + + const emailFormat = '#EMAIL' + if (jsDisplayFormat === emailFormat) { + return '[#VALUE](mailto:#VALUE)' + } + + const imageFormat = '
' + if (jsDisplayFormat === imageFormat) { + return '![#NAME](#VALUE)' + } + + return jsDisplayFormat + } +} diff --git a/src/metadata-block-info/infrastructure/repositories/MetadataBlockInfoJSDataverseRepository.ts b/src/metadata-block-info/infrastructure/repositories/MetadataBlockInfoJSDataverseRepository.ts new file mode 100644 index 000000000..38e5bb4d4 --- /dev/null +++ b/src/metadata-block-info/infrastructure/repositories/MetadataBlockInfoJSDataverseRepository.ts @@ -0,0 +1,17 @@ +import { MetadataBlockInfoRepository } from '../../domain/repositories/MetadataBlockInfoRepository' +import { MetadataBlockInfo } from '../../domain/models/MetadataBlockInfo' +import { + getMetadataBlockByName, + MetadataBlock as JSMetadataBlockInfo +} from '@iqss/dataverse-client-javascript' +import { JSMetadataBlockInfoMapper } from '../mappers/JSMetadataBlockInfoMapper' + +export class MetadataBlockInfoJSDataverseRepository implements MetadataBlockInfoRepository { + getByName(name: string): Promise { + return getMetadataBlockByName + .execute(name) + .then((jsMetadataBlockInfo: JSMetadataBlockInfo) => + JSMetadataBlockInfoMapper.toMetadataBlockInfo(jsMetadataBlockInfo) + ) + } +} diff --git a/src/sections/dataset/DatasetFactory.tsx b/src/sections/dataset/DatasetFactory.tsx index a02427838..0111fe891 100644 --- a/src/sections/dataset/DatasetFactory.tsx +++ b/src/sections/dataset/DatasetFactory.tsx @@ -6,9 +6,9 @@ import { useAnonymized } from './anonymized/AnonymizedContext' import { AnonymizedProvider } from './anonymized/AnonymizedProvider' import { FileJSDataverseRepository } from '../../files/infrastructure/FileJSDataverseRepository' import { MetadataBlockInfoProvider } from './metadata-block-info/MetadataBlockProvider' -import { MetadataBlockInfoJSDataverseRepository } from '../../metadata-block-info/infrastructure/MetadataBlockInfoJSDataverseRepository' -import { SettingsProvider } from '../settings/SettingsProvider' +import { MetadataBlockInfoJSDataverseRepository } from '../../metadata-block-info/infrastructure/repositories/MetadataBlockInfoJSDataverseRepository' import { SettingJSDataverseRepository } from '../../settings/infrastructure/SettingJSDataverseRepository' +import { SettingsProvider } from '../settings/SettingsProvider' const datasetRepository = new DatasetJSDataverseRepository() const fileRepository = new FileJSDataverseRepository() diff --git a/src/sections/dataset/dataset-files/files-table/FilesTable.tsx b/src/sections/dataset/dataset-files/files-table/FilesTable.tsx index f34bd92ce..908d983ec 100644 --- a/src/sections/dataset/dataset-files/files-table/FilesTable.tsx +++ b/src/sections/dataset/dataset-files/files-table/FilesTable.tsx @@ -15,7 +15,7 @@ interface FilesTableProps { } export function FilesTable({ files, isLoading, paginationInfo }: FilesTableProps) { - const { table, rowSelection, selectAllRows, clearRowSelection } = useFilesTable( + const { table, fileSelection, selectAllFiles, clearFileSelection } = useFilesTable( files, paginationInfo ) @@ -26,14 +26,12 @@ export function FilesTable({ files, isLoading, paginationInfo }: FilesTableProps return ( <> - row.original)} + fileSelection={fileSelection} + selectAllRows={selectAllFiles} + totalFilesCount={paginationInfo.totalFiles} + clearRowSelection={clearFileSelection} /> + diff --git a/src/sections/dataset/dataset-files/files-table/FilesTableColumnsDefinition.tsx b/src/sections/dataset/dataset-files/files-table/FilesTableColumnsDefinition.tsx index 22e73b074..6397c7770 100644 --- a/src/sections/dataset/dataset-files/files-table/FilesTableColumnsDefinition.tsx +++ b/src/sections/dataset/dataset-files/files-table/FilesTableColumnsDefinition.tsx @@ -6,9 +6,7 @@ import { FileInfoHeader } from './file-info/FileInfoHeader' import { FileActionsHeader } from './file-actions/FileActionsHeader' import { FileActionsCell } from './file-actions/file-actions-cell/FileActionsCell' -export const createColumnsDefinition = ( - toggleAllRowsSelected: (event: unknown) => void -): ColumnDef[] => [ +export const columns: ColumnDef[] = [ { id: 'select', header: ({ table }) => ( @@ -16,7 +14,7 @@ export const createColumnsDefinition = ( {...{ checked: table.getIsAllRowsSelected(), indeterminate: table.getIsSomeRowsSelected(), - onChange: toggleAllRowsSelected, + onChange: table.getToggleAllRowsSelectedHandler(), disabled: table.getPageCount() === 0 }} /> diff --git a/src/sections/dataset/dataset-files/files-table/row-selection/RowSelectionMessage.tsx b/src/sections/dataset/dataset-files/files-table/row-selection/RowSelectionMessage.tsx index 26d028ec4..82d0c8791 100644 --- a/src/sections/dataset/dataset-files/files-table/row-selection/RowSelectionMessage.tsx +++ b/src/sections/dataset/dataset-files/files-table/row-selection/RowSelectionMessage.tsx @@ -1,10 +1,10 @@ -import { RowSelection } from './useRowSelection' +import { FileSelection } from './useFileSelection' import { Button } from '@iqss/dataverse-design-system' import { useTranslation } from 'react-i18next' import styles from './RowSelectionMessage.module.scss' interface RowSelectionMessageProps { - rowSelection: RowSelection + fileSelection: FileSelection totalFilesCount: number selectAllRows: () => void clearRowSelection: () => void @@ -14,13 +14,13 @@ const MINIMUM_SELECTED_FILES_TO_SHOW_MESSAGE = 0 const MINIMUM_FILES_TO_SHOW_MESSAGE = 10 export function RowSelectionMessage({ - rowSelection, + fileSelection, totalFilesCount, selectAllRows, clearRowSelection }: RowSelectionMessageProps) { const { t } = useTranslation('files') - const selectedFilesCount = Object.keys(rowSelection).length + const selectedFilesCount = Object.keys(fileSelection).length const showMessage = totalFilesCount > MINIMUM_FILES_TO_SHOW_MESSAGE && selectedFilesCount > MINIMUM_SELECTED_FILES_TO_SHOW_MESSAGE diff --git a/src/sections/dataset/dataset-files/files-table/row-selection/useFileSelection.ts b/src/sections/dataset/dataset-files/files-table/row-selection/useFileSelection.ts new file mode 100644 index 000000000..61bc82b40 --- /dev/null +++ b/src/sections/dataset/dataset-files/files-table/row-selection/useFileSelection.ts @@ -0,0 +1,103 @@ +import { useEffect, useState } from 'react' +import { FilePaginationInfo } from '../../../../../files/domain/models/FilePaginationInfo' +import { File } from '../../../../../files/domain/models/File' +import { Row } from '@tanstack/react-table' +import { RowSelection } from '../useFilesTable' + +export type FileSelection = { + [key: string]: File | undefined +} + +export function useFileSelection( + currentPageSelectedRowModel: Record>, + setCurrentPageRowSelection: (rowSelection: RowSelection) => void, + paginationInfo: FilePaginationInfo +) { + const [fileSelection, setFileSelection] = useState({}) + const updateFileSelection = () => { + const currentPageFileSelection = getCurrentPageFileSelection() + const currentPageIndexes = getCurrentPageIndexes() + + Object.keys(fileSelection).forEach((key) => { + const rowIndex = parseInt(key) + if (currentPageIndexes.includes(rowIndex)) { + if (!currentPageFileSelection[key]) { + delete fileSelection[key] + } + } + }) + + return { ...fileSelection, ...currentPageFileSelection } + } + const getCurrentPageIndexes = () => { + return Array.from( + { length: paginationInfo.pageSize }, + (_, i) => i + (paginationInfo.page - 1) * paginationInfo.pageSize + ) + } + const getCurrentPageFileSelection = () => { + const rowSelectionFixed: FileSelection = {} + const currentPageIndexes = getCurrentPageIndexes() + + Object.entries(currentPageSelectedRowModel).forEach(([string, Row]) => { + const rowIndex = parseInt(string) + rowSelectionFixed[currentPageIndexes[rowIndex]] = Row.original + }) + return rowSelectionFixed + } + const computeCurrentPageRowSelection = () => { + const rowSelectionOfCurrentPage: RowSelection = {} + const currentPageIndexes = getCurrentPageIndexes() + + Object.keys(fileSelection).forEach((key) => { + const rowIndex = parseInt(key) + if (currentPageIndexes.includes(rowIndex)) { + rowSelectionOfCurrentPage[currentPageIndexes.indexOf(rowIndex)] = true + } + }) + + return rowSelectionOfCurrentPage + } + const selectAllFiles = () => { + setCurrentPageRowSelection(createRowSelection(paginationInfo.pageSize)) + setFileSelection(createFileSelection(paginationInfo.totalFiles)) + } + const clearFileSelection = () => { + setCurrentPageRowSelection({}) + setFileSelection({}) + } + + useEffect(() => { + setFileSelection(updateFileSelection()) + }, [currentPageSelectedRowModel]) + + useEffect(() => { + setCurrentPageRowSelection(computeCurrentPageRowSelection()) + }, [paginationInfo]) + + return { + fileSelection, + selectAllFiles, + clearFileSelection + } +} + +export function createRowSelection(numberOfRows: number) { + const rowSelection: Record = {} + + for (let i = 0; i < numberOfRows; i++) { + rowSelection[String(i)] = true + } + + return rowSelection +} + +export function createFileSelection(numberOfRows: number) { + const fileSelection: FileSelection = {} + + for (let i = 0; i < numberOfRows; i++) { + fileSelection[String(i)] = undefined + } + + return fileSelection +} diff --git a/src/sections/dataset/dataset-files/files-table/row-selection/useRowSelection.ts b/src/sections/dataset/dataset-files/files-table/row-selection/useRowSelection.ts deleted file mode 100644 index 55c0a468e..000000000 --- a/src/sections/dataset/dataset-files/files-table/row-selection/useRowSelection.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { useEffect, useState } from 'react' -import { FilePaginationInfo } from '../../../../../files/domain/models/FilePaginationInfo' - -export type RowSelection = { - [key: string]: boolean -} - -export function useRowSelection( - currentPageRowSelection: RowSelection, - setCurrentPageRowSelection: (rowSelection: RowSelection) => void, - paginationInfo: FilePaginationInfo -) { - const [rowSelection, setRowSelection] = useState({}) - const updatedRowSelection = () => { - const currentPageRowSelectionFixed = getCurrentPageRowSelectionFixed() - const currentPageIndexes = getCurrentPageIndexes() - - Object.keys(rowSelection).forEach((key) => { - const rowIndex = parseInt(key) - if (currentPageIndexes.includes(rowIndex)) { - if (!currentPageRowSelectionFixed[key]) { - delete rowSelection[key] - } - } - }) - - return { ...rowSelection, ...currentPageRowSelectionFixed } - } - const getCurrentPageIndexes = () => { - return Array.from( - { length: paginationInfo.pageSize }, - (_, i) => i + (paginationInfo.page - 1) * paginationInfo.pageSize - ) - } - const getCurrentPageRowSelectionFixed = () => { - const rowSelectionFixed: RowSelection = {} - const currentPageIndexes = getCurrentPageIndexes() - - Object.keys(currentPageRowSelection).forEach((key) => { - const rowIndex = parseInt(key) - rowSelectionFixed[currentPageIndexes[rowIndex]] = currentPageRowSelection[key] - }) - return rowSelectionFixed - } - const getRowSelectionOfCurrentPage = () => { - const rowSelectionOfCurrentPage: RowSelection = {} - const currentPageIndexes = getCurrentPageIndexes() - - Object.keys(rowSelection).forEach((key) => { - const rowIndex = parseInt(key) - if (currentPageIndexes.includes(rowIndex)) { - rowSelectionOfCurrentPage[currentPageIndexes.indexOf(rowIndex)] = rowSelection[key] - } - }) - - return rowSelectionOfCurrentPage - } - const selectAllRows = () => { - setCurrentPageRowSelection(createRowSelection(paginationInfo.pageSize)) - setRowSelection(createRowSelection(paginationInfo.total)) - } - const clearRowSelection = () => { - setCurrentPageRowSelection({}) - setRowSelection({}) - } - const toggleAllRowsSelected = () => { - if (isAllRowsSelected()) { - clearRowSelection() - } else { - selectAllRows() - } - } - const isAllRowsSelected = () => { - return Object.keys(rowSelection).length === paginationInfo.total - } - - useEffect(() => { - setRowSelection(updatedRowSelection()) - }, [currentPageRowSelection]) - - useEffect(() => { - setCurrentPageRowSelection(getRowSelectionOfCurrentPage()) - }, [paginationInfo]) - - return { - rowSelection, - setRowSelection, - selectAllRows, - clearRowSelection, - toggleAllRowsSelected - } -} - -export function createRowSelection(numberOfRows: number) { - const rowSelection: Record = {} - - for (let i = 0; i < numberOfRows; i++) { - rowSelection[i as unknown as string] = true - } - - return rowSelection -} diff --git a/src/sections/dataset/dataset-files/files-table/useFilesTable.tsx b/src/sections/dataset/dataset-files/files-table/useFilesTable.tsx index bb524d4b4..52cd528fb 100644 --- a/src/sections/dataset/dataset-files/files-table/useFilesTable.tsx +++ b/src/sections/dataset/dataset-files/files-table/useFilesTable.tsx @@ -1,20 +1,27 @@ import { useEffect, useState } from 'react' import { File } from '../../../../files/domain/models/File' -import { getCoreRowModel, useReactTable } from '@tanstack/react-table' -import { createColumnsDefinition } from './FilesTableColumnsDefinition' +import { getCoreRowModel, Row, useReactTable } from '@tanstack/react-table' +import { columns } from './FilesTableColumnsDefinition' import { FilePaginationInfo } from '../../../../files/domain/models/FilePaginationInfo' -import { RowSelection, useRowSelection } from './row-selection/useRowSelection' +import { useFileSelection } from './row-selection/useFileSelection' + +export type RowSelection = { + [key: string]: boolean +} export function useFilesTable(files: File[], paginationInfo: FilePaginationInfo) { const [currentPageRowSelection, setCurrentPageRowSelection] = useState({}) - const { rowSelection, selectAllRows, clearRowSelection, toggleAllRowsSelected } = useRowSelection( - currentPageRowSelection, + const [currentPageSelectedRowModel, setCurrentPageSelectedRowModel] = useState< + Record> + >({}) + const { fileSelection, selectAllFiles, clearFileSelection } = useFileSelection( + currentPageSelectedRowModel, setCurrentPageRowSelection, paginationInfo ) const table = useReactTable({ data: files, - columns: createColumnsDefinition(toggleAllRowsSelected), + columns: columns, state: { rowSelection: currentPageRowSelection }, @@ -30,10 +37,14 @@ export function useFilesTable(files: File[], paginationInfo: FilePaginationInfo) table.setPageIndex(paginationInfo.page - 1) }, [paginationInfo]) + useEffect(() => { + setCurrentPageSelectedRowModel(table.getSelectedRowModel().rowsById) + }, [table.getSelectedRowModel().rowsById]) + return { table, - rowSelection, - selectAllRows, - clearRowSelection + fileSelection, + selectAllFiles, + clearFileSelection } } diff --git a/src/sections/dataset/dataset-files/files-table/zip-download-limit-message/ZipDownloadLimitMessage.tsx b/src/sections/dataset/dataset-files/files-table/zip-download-limit-message/ZipDownloadLimitMessage.tsx index a066dba7a..7e1c474ee 100644 --- a/src/sections/dataset/dataset-files/files-table/zip-download-limit-message/ZipDownloadLimitMessage.tsx +++ b/src/sections/dataset/dataset-files/files-table/zip-download-limit-message/ZipDownloadLimitMessage.tsx @@ -5,19 +5,18 @@ import { useSettings } from '../../../../settings/SettingsContext' import { SettingName } from '../../../../../settings/domain/models/Setting' import { ZipDownloadLimit } from '../../../../../settings/domain/models/ZipDownloadLimit' import { useEffect, useState } from 'react' +import { FileSelection } from '../row-selection/useFileSelection' interface ZipDownloadLimitMessageProps { - selectedFiles: File[] + fileSelection: FileSelection } const MINIMUM_FILES_TO_SHOW_MESSAGE = 1 -export function ZipDownloadLimitMessage({ selectedFiles }: ZipDownloadLimitMessageProps) { +export function ZipDownloadLimitMessage({ fileSelection }: ZipDownloadLimitMessageProps) { const { t } = useTranslation('files') const { getSettingByName } = useSettings() const [zipDownloadLimitInBytes, setZipDownloadLimitInBytes] = useState() - const selectionTotalSizeInBytes = getFilesTotalSizeInBytes(selectedFiles) - useEffect(() => { getSettingByName(SettingName.ZIP_DOWNLOAD_LIMIT) .then((zipDownloadLimit) => { @@ -28,9 +27,11 @@ export function ZipDownloadLimitMessage({ selectedFiles }: ZipDownloadLimitMessa }) }, [getSettingByName]) + // TODO - When selecting all files, the size should come from a call to a use case that returns the total size of the dataset files. Check issue https://github.com/IQSS/dataverse-frontend/issues/170 + const selectionTotalSizeInBytes = getFilesTotalSizeInBytes(Object.values(fileSelection)) const showMessage = zipDownloadLimitInBytes && - selectedFiles.length > MINIMUM_FILES_TO_SHOW_MESSAGE && + Object.values(fileSelection).length > MINIMUM_FILES_TO_SHOW_MESSAGE && selectionTotalSizeInBytes > zipDownloadLimitInBytes if (!showMessage) { @@ -48,8 +49,10 @@ export function ZipDownloadLimitMessage({ selectedFiles }: ZipDownloadLimitMessa ) } -function getFilesTotalSizeInBytes(files: File[]) { - return files.map((file) => file.size).reduce((bytes, size) => bytes + size.toBytes(), 0) +function getFilesTotalSizeInBytes(files: (File | undefined)[]) { + return files + .map((file) => file?.size) + .reduce((bytes, size) => bytes + (size ? size.toBytes() : 0), 0) } function bytesToHumanReadable(bytes: number) { diff --git a/src/sections/dataset/dataset-metadata/dataset-metadata-fields/DatasetMetadataField.tsx b/src/sections/dataset/dataset-metadata/dataset-metadata-fields/DatasetMetadataField.tsx index 71821add0..e8b6ef7a8 100644 --- a/src/sections/dataset/dataset-metadata/dataset-metadata-fields/DatasetMetadataField.tsx +++ b/src/sections/dataset/dataset-metadata/dataset-metadata-fields/DatasetMetadataField.tsx @@ -30,6 +30,7 @@ export function DatasetMetadataField({ metadataFieldName={metadataFieldName} /> diff --git a/src/sections/dataset/dataset-metadata/dataset-metadata-fields/DatasetMetadataFieldValue.tsx b/src/sections/dataset/dataset-metadata/dataset-metadata-fields/DatasetMetadataFieldValue.tsx index 1722b4159..4ec9b80a7 100644 --- a/src/sections/dataset/dataset-metadata/dataset-metadata-fields/DatasetMetadataFieldValue.tsx +++ b/src/sections/dataset/dataset-metadata/dataset-metadata-fields/DatasetMetadataFieldValue.tsx @@ -1,41 +1,34 @@ import { ANONYMIZED_FIELD_VALUE, DatasetMetadataFieldValue as DatasetMetadataFieldValueModel, - DatasetMetadataSubField + MetadataBlockName } from '../../../../dataset/domain/models/Dataset' -import { MarkdownComponent } from '../../markdown/MarkdownComponent' import { useAnonymized } from '../../anonymized/AnonymizedContext' import { useTranslation } from 'react-i18next' -import { useMetadataBlockInfo } from '../../metadata-block-info/MetadataBlockInfoContext' -import { - METADATA_FIELD_DISPLAY_FORMAT_PLACEHOLDER, - MetadataBlockInfo -} from '../../../../metadata-block-info/domain/models/MetadataBlockInfo' +import { DatasetMetadataFieldValueFormatted } from './DatasetMetadataFieldValueFormatted' interface DatasetMetadataFieldValueProps { + metadataBlockName: MetadataBlockName metadataFieldName: string metadataFieldValue: DatasetMetadataFieldValueModel } export function DatasetMetadataFieldValue({ + metadataBlockName, metadataFieldName, metadataFieldValue }: DatasetMetadataFieldValueProps) { const { anonymizedView } = useAnonymized() const isAnonymizedField = anonymizedView && metadataFieldValue == ANONYMIZED_FIELD_VALUE - const { metadataBlockInfo } = useMetadataBlockInfo() - if (isAnonymizedField) { return } return ( - ) } @@ -44,65 +37,3 @@ const AnonymizedFieldValue = () => { const { t } = useTranslation('dataset') return

{t('anonymizedFieldValue')}

} - -export function metadataFieldValueToString( - metadataFieldName: string, - metadataFieldValue: DatasetMetadataFieldValueModel, - metadataBlockInfo?: MetadataBlockInfo -): string { - const separator = metadataBlockInfo?.fields[metadataFieldName]?.displayFormat ?? '' - - if (isArrayOfObjects(metadataFieldValue)) { - return metadataFieldValue - .map((metadataSubField) => joinSubFields(metadataSubField, metadataBlockInfo)) - .join(' \n \n') - } - - if (Array.isArray(metadataFieldValue)) { - return metadataFieldValue.join(`${separator} `) - } - - if (isAnObject(metadataFieldValue)) { - return Object.values(metadataFieldValue).join(`${separator} `) - } - - return metadataFieldValue -} - -export function isArrayOfObjects(variable: unknown): variable is object[] { - if (!Array.isArray(variable)) { - return false - } - - return variable.every((item) => isAnObject(item)) -} - -function isAnObject(variable: unknown): variable is object { - return typeof variable === 'object' && variable !== null -} - -function joinSubFields( - metadataSubField: DatasetMetadataSubField, - metadataBlockInfo?: MetadataBlockInfo -) { - return Object.entries(metadataSubField) - .map(([subFieldName, subFieldValue]) => - formatSubFieldValue(subFieldValue, metadataBlockInfo?.fields[subFieldName]?.displayFormat) - ) - .join(' ') -} - -function formatSubFieldValue( - subFieldValue: string | undefined, - displayFormat: string | undefined -): string { - if (subFieldValue === undefined) { - return '' - } - - if (displayFormat === undefined) { - return subFieldValue - } - - return displayFormat.replaceAll(METADATA_FIELD_DISPLAY_FORMAT_PLACEHOLDER, subFieldValue) -} diff --git a/src/sections/dataset/dataset-metadata/dataset-metadata-fields/DatasetMetadataFieldValueFormatted.tsx b/src/sections/dataset/dataset-metadata/dataset-metadata-fields/DatasetMetadataFieldValueFormatted.tsx new file mode 100644 index 000000000..b3cb7d7a7 --- /dev/null +++ b/src/sections/dataset/dataset-metadata/dataset-metadata-fields/DatasetMetadataFieldValueFormatted.tsx @@ -0,0 +1,100 @@ +import { useMetadataBlockInfo } from '../../metadata-block-info/MetadataBlockInfoContext' +import { + METADATA_FIELD_DISPLAY_FORMAT_NAME_PLACEHOLDER, + METADATA_FIELD_DISPLAY_FORMAT_PLACEHOLDER, + MetadataBlockInfo +} from '../../../../metadata-block-info/domain/models/MetadataBlockInfo' +import { MarkdownComponent } from '../../markdown/MarkdownComponent' +import { + DatasetMetadataFieldValue as DatasetMetadataFieldValueModel, + DatasetMetadataSubField, + MetadataBlockName +} from '../../../../dataset/domain/models/Dataset' +import { useTranslation } from 'react-i18next' + +interface DatasetMetadataFieldValueFormattedProps { + metadataBlockName: MetadataBlockName + metadataFieldName: string + metadataFieldValue: DatasetMetadataFieldValueModel +} +export function DatasetMetadataFieldValueFormatted({ + metadataBlockName, + metadataFieldName, + metadataFieldValue +}: DatasetMetadataFieldValueFormattedProps) { + const { t } = useTranslation(metadataBlockName) + const { metadataBlockInfo } = useMetadataBlockInfo() + const valueFormatted = metadataFieldValueToDisplayFormat( + metadataFieldName, + metadataFieldValue, + metadataBlockInfo + ) + const valueFormattedWithNamesTranslated = valueFormatted.replaceAll( + METADATA_FIELD_DISPLAY_FORMAT_NAME_PLACEHOLDER, + t(`${metadataBlockName}.datasetField.${metadataFieldName}.name`) + ) + + return +} + +export function metadataFieldValueToDisplayFormat( + metadataFieldName: string, + metadataFieldValue: DatasetMetadataFieldValueModel, + metadataBlockInfo?: MetadataBlockInfo +): string { + const separator = metadataBlockInfo?.fields[metadataFieldName]?.displayFormat ?? '' + + if (isArrayOfObjects(metadataFieldValue)) { + return metadataFieldValue + .map((metadataSubField) => joinSubFields(metadataSubField, metadataBlockInfo)) + .join(' \n \n') + } + + if (Array.isArray(metadataFieldValue)) { + return metadataFieldValue.join(`${separator} `) + } + + if (isAnObject(metadataFieldValue)) { + return joinObjectValues(metadataFieldValue, separator) + } + + return metadataFieldValue +} + +export function isArrayOfObjects(variable: unknown): variable is object[] { + return Array.isArray(variable) && variable.every(isAnObject) +} + +function isAnObject(variable: unknown): variable is object { + return typeof variable === 'object' && variable !== null +} + +function joinObjectValues(obj: object, separator: string): string { + return Object.values(obj).join(separator) +} + +function joinSubFields( + metadataSubField: DatasetMetadataSubField, + metadataBlockInfo?: MetadataBlockInfo +): string { + return Object.entries(metadataSubField) + .map(([subFieldName, subFieldValue]) => + formatSubFieldValue(subFieldValue, metadataBlockInfo?.fields[subFieldName]?.displayFormat) + ) + .join(' ') +} + +function formatSubFieldValue( + subFieldValue: string | undefined, + displayFormat: string | undefined +): string { + if (subFieldValue === undefined) { + return '' + } + + if (displayFormat === undefined) { + return subFieldValue + } + + return displayFormat.replaceAll(METADATA_FIELD_DISPLAY_FORMAT_PLACEHOLDER, subFieldValue) +} diff --git a/tests/component/dataset/domain/models/DatasetMother.ts b/tests/component/dataset/domain/models/DatasetMother.ts index 0e84fa720..4030c52c0 100644 --- a/tests/component/dataset/domain/models/DatasetMother.ts +++ b/tests/component/dataset/domain/models/DatasetMother.ts @@ -68,13 +68,13 @@ export class DatasetMother { authorName: faker.lorem.sentence(), authorAffiliation: faker.lorem.sentence(), authorIdentifierScheme: faker.lorem.sentence(), - authorIdentifier: faker.lorem.sentence() + authorIdentifier: faker.lorem.word() }, { authorName: faker.lorem.sentence(), authorAffiliation: faker.lorem.sentence(), authorIdentifierScheme: faker.lorem.sentence(), - authorIdentifier: faker.lorem.sentence() + authorIdentifier: faker.lorem.word() } ], datasetContact: [ @@ -87,6 +87,13 @@ export class DatasetMother { { dsDescriptionValue: faker.lorem.sentence() } + ], + producer: [ + { + producerName: faker.lorem.sentence(), + producerURL: faker.internet.url(), + producerLogoURL: faker.image.imageUrl() + } ] } }, diff --git a/tests/component/dataset/infrastructure/mappers/JSDatasetMapper.spec.ts b/tests/component/dataset/infrastructure/mappers/JSDatasetMapper.spec.ts new file mode 100644 index 000000000..4ad8930f1 --- /dev/null +++ b/tests/component/dataset/infrastructure/mappers/JSDatasetMapper.spec.ts @@ -0,0 +1,214 @@ +import chai from 'chai' +import chaiAsPromised from 'chai-as-promised' +import { JSDatasetMapper } from '../../../../../src/dataset/infrastructure/mappers/JSDatasetMapper' +import { DatasetVersionState } from '@iqss/dataverse-client-javascript' +import { + CitationMetadataBlock, + DatasetMetadataBlock +} from '@iqss/dataverse-client-javascript/dist/datasets/domain/models/Dataset' +import { DatasetStatus, DatasetVersion } from '../../../../../src/dataset/domain/models/Dataset' + +chai.use(chaiAsPromised) +const expect = chai.expect + +const jsDataset = { + id: 505, + persistentId: 'doi:10.5072/FK2/B4B2MJ', + versionId: 101, + versionInfo: { + state: DatasetVersionState.DRAFT, + majorNumber: 0, + minorNumber: 0, + createTime: new Date('2023-09-07T13:40:04.000Z'), + lastUpdateTime: new Date('2023-09-07T13:40:04.000Z'), + releaseTime: undefined + }, + metadataBlocks: [ + { + name: 'citation', + fields: { + title: "Darwin's Finches", + author: [{ authorName: 'Finch, Fiona', authorAffiliation: 'Birds Inc.' }], + datasetContact: [ + { datasetContactName: 'Finch, Fiona', datasetContactEmail: 'finch@mailinator.com' } + ], + dsDescription: [ + { + dsDescriptionValue: + "Darwin's finches (also known as the Galápagos finches) are a group of about fifteen species of passerine birds." + } + ], + subject: ['Medicine, Health and Life Sciences'] + } + } as CitationMetadataBlock + ] as [CitationMetadataBlock, ...DatasetMetadataBlock[]], + license: { + name: 'CC0 1.0', + uri: 'http://creativecommons.org/publicdomain/zero/1.0', + iconUri: 'https://licensebuttons.net/p/zero/1.0/88x31.png' + } +} +const citation = + 'Finch, Fiona, 2023, "Darwin\'s Finches", https://doi.org/10.5072/FK2/B4B2MJ, Root, DRAFT VERSION' +const datasetSummaryFields = ['dsDescription', 'subject', 'keyword', 'publication', 'notesText'] +const expectedDataset = { + persistentId: 'doi:10.5072/FK2/B4B2MJ', + version: new DatasetVersion(0, 0, DatasetStatus.DRAFT), + citation: + 'Finch, Fiona, 2023, "Darwin\'s Finches", https://doi.org/10.5072/FK2/B4B2MJ, Root, DRAFT VERSION', + labels: [ + { semanticMeaning: 'dataset', value: 'Draft' }, + { semanticMeaning: 'warning', value: 'Unpublished' } + ], + summaryFields: [ + { + name: 'citation', + fields: { + dsDescription: [ + { + dsDescriptionValue: + "Darwin's finches (also known as the Galápagos finches) are a group of about fifteen species of passerine birds." + } + ], + subject: ['Medicine, Health and Life Sciences'] + } + } + ], + license: { + name: 'CC0 1.0', + uri: 'http://creativecommons.org/publicdomain/zero/1.0', + iconUri: 'https://licensebuttons.net/p/zero/1.0/88x31.png' + }, + metadataBlocks: [ + { + name: 'citation', + fields: { + title: "Darwin's Finches", + author: [{ authorName: 'Finch, Fiona', authorAffiliation: 'Birds Inc.' }], + datasetContact: [ + { datasetContactName: 'Finch, Fiona', datasetContactEmail: 'finch@mailinator.com' } + ], + dsDescription: [ + { + dsDescriptionValue: + "Darwin's finches (also known as the Galápagos finches) are a group of about fifteen species of passerine birds." + } + ], + subject: ['Medicine, Health and Life Sciences'] + } + } + ] +} + +describe('JS Dataset Mapper', () => { + it('maps jsDataset model to the domain Dataset model', () => { + expect(expectedDataset).to.deep.equal( + JSDatasetMapper.toDataset(jsDataset, citation, datasetSummaryFields) + ) + }) + + it('maps jsDataset model to the domain Dataset model when alternativePersistentId is provided', () => { + const jsDatasetWithAlternativePersistentId = { + ...jsDataset, + alternativePersistentId: 'doi:10.5072/FK2/B4B2MY' + } + const expectedDatasetWithAlternativePersistentId = { + ...expectedDataset, + metadataBlocks: [ + { + name: 'citation', + fields: { + title: "Darwin's Finches", + author: [{ authorName: 'Finch, Fiona', authorAffiliation: 'Birds Inc.' }], + datasetContact: [ + { datasetContactName: 'Finch, Fiona', datasetContactEmail: 'finch@mailinator.com' } + ], + dsDescription: [ + { + dsDescriptionValue: + "Darwin's finches (also known as the Galápagos finches) are a group of about fifteen species of passerine birds." + } + ], + subject: ['Medicine, Health and Life Sciences'], + alternativePersistentId: 'doi:10.5072/FK2/B4B2MY' + } + } + ] + } + + expect(expectedDatasetWithAlternativePersistentId).to.deep.equal( + JSDatasetMapper.toDataset( + jsDatasetWithAlternativePersistentId, + citation, + datasetSummaryFields + ) + ) + }) + + it('maps jsDataset model to the domain Dataset model when citationDate is provided', () => { + const jsDatasetWithCitationDate = { + ...jsDataset, + citationDate: '2023-02-12' + } + const expectedDatasetWithCitationDate = { + ...expectedDataset, + metadataBlocks: [ + { + name: 'citation', + fields: { + title: "Darwin's Finches", + author: [{ authorName: 'Finch, Fiona', authorAffiliation: 'Birds Inc.' }], + datasetContact: [ + { datasetContactName: 'Finch, Fiona', datasetContactEmail: 'finch@mailinator.com' } + ], + dsDescription: [ + { + dsDescriptionValue: + "Darwin's finches (also known as the Galápagos finches) are a group of about fifteen species of passerine birds." + } + ], + subject: ['Medicine, Health and Life Sciences'], + citationDate: '2023-02-12' + } + } + ] + } + + expect(expectedDatasetWithCitationDate).to.deep.equal( + JSDatasetMapper.toDataset(jsDatasetWithCitationDate, citation, datasetSummaryFields) + ) + }) + + it('maps jsDataset model to the domain Dataset model when publicationDate is provided', () => { + const jsDatasetWithPublicationDate = { + ...jsDataset, + publicationDate: '2023-02-12' + } + const expectedDatasetWithPublicationDate = { + ...expectedDataset, + metadataBlocks: [ + { + name: 'citation', + fields: { + title: "Darwin's Finches", + author: [{ authorName: 'Finch, Fiona', authorAffiliation: 'Birds Inc.' }], + datasetContact: [ + { datasetContactName: 'Finch, Fiona', datasetContactEmail: 'finch@mailinator.com' } + ], + dsDescription: [ + { + dsDescriptionValue: + "Darwin's finches (also known as the Galápagos finches) are a group of about fifteen species of passerine birds." + } + ], + subject: ['Medicine, Health and Life Sciences'], + publicationDate: '2023-02-12' + } + } + ] + } + expect(expectedDatasetWithPublicationDate).to.deep.equal( + JSDatasetMapper.toDataset(jsDatasetWithPublicationDate, citation, datasetSummaryFields) + ) + }) +}) diff --git a/tests/component/metadata-block-info/domain/models/MetadataBlockInfoMother.ts b/tests/component/metadata-block-info/domain/models/MetadataBlockInfoMother.ts index 791b5b9a1..c31293d05 100644 --- a/tests/component/metadata-block-info/domain/models/MetadataBlockInfoMother.ts +++ b/tests/component/metadata-block-info/domain/models/MetadataBlockInfoMother.ts @@ -19,9 +19,12 @@ export class MetadataBlockInfoMother { datasetContact: { displayFormat: '#VALUE' }, datasetContactName: { displayFormat: '#VALUE' }, datasetContactAffiliation: { displayFormat: '(#VALUE)' }, - datasetContactEmail: { displayFormat: '#VALUE' }, + datasetContactEmail: { displayFormat: '[#VALUE](mailto:#VALUE)' }, dsDescription: { displayFormat: '' }, - dsDescriptionValue: { displayFormat: '#VALUE' } + dsDescriptionValue: { displayFormat: '#VALUE' }, + producerURL: { displayFormat: '[#VALUE](#VALUE)' }, + producerLogoURL: { displayFormat: '![#NAME](#VALUE)' }, + dateOfCollectionStart: { displayFormat: '#NAME: #VALUE ' } }, ...props } diff --git a/tests/component/sections/dataset/dataset-files/DatasetFiles.spec.tsx b/tests/component/sections/dataset/dataset-files/DatasetFiles.spec.tsx index 37c28c548..2049c0c7d 100644 --- a/tests/component/sections/dataset/dataset-files/DatasetFiles.spec.tsx +++ b/tests/component/sections/dataset/dataset-files/DatasetFiles.spec.tsx @@ -9,8 +9,11 @@ import { } from '../../../../../src/files/domain/models/FileCriteria' import { FilesCountInfoMother } from '../../../files/domain/models/FilesCountInfoMother' import { FilePaginationInfo } from '../../../../../src/files/domain/models/FilePaginationInfo' -import { FileType } from '../../../../../src/files/domain/models/File' +import { FileSizeUnit, FileType } from '../../../../../src/files/domain/models/File' import styles from '../../../../../src/sections/dataset/dataset-files/files-table/FilesTable.module.scss' +import { SettingMother } from '../../../settings/domain/models/SettingMother' +import { ZipDownloadLimit } from '../../../../../src/settings/domain/models/ZipDownloadLimit' +import { SettingsContext } from '../../../../../src/sections/settings/SettingsContext' const testFiles = FileMother.createMany(10) const datasetPersistentId = 'test-dataset-persistent-id' @@ -161,6 +164,36 @@ describe('DatasetFiles', () => { cy.findByText('1 file is currently selected.').should('exist') }) + + it('renders the zip download limit message when selecting rows from different pages', () => { + const getSettingByName = cy + .stub() + .resolves(SettingMother.createZipDownloadLimit(new ZipDownloadLimit(1, FileSizeUnit.BYTES))) + + cy.customMount( + + + + ) + + cy.get('table > tbody > tr:nth-child(2) > td:nth-child(1) > input[type=checkbox]').click() + cy.findByRole('button', { name: 'Next' }).click() + cy.get('table > tbody > tr:nth-child(3) > td:nth-child(1) > input[type=checkbox]').click() + + cy.findByText( + /exceeds the zip limit of 1.0 B. Please unselect some files to continue./ + ).should('exist') + + cy.get('table > tbody > tr:nth-child(3) > td:nth-child(1) > input[type=checkbox]').click() + + cy.findByText( + /exceeds the zip limit of 1.0 B. Please unselect some files to continue./ + ).should('not.exist') + }) }) describe('Calling use cases', () => { @@ -215,7 +248,7 @@ describe('DatasetFiles', () => { /> ) - cy.findByRole('button', { name: 'Filter Type: All' }).click() + cy.findByRole('button', { name: 'File Type: All' }).click() cy.findByText('Image (485)').should('exist').click() cy.wrap(fileRepository.getAllByDatasetPersistentId).should( 'be.calledWith', @@ -255,7 +288,7 @@ describe('DatasetFiles', () => { /> ) - cy.findByRole('button', { name: 'Filter Tag: All' }).click() + cy.findByRole('button', { name: 'File Tags: All' }).click() cy.findByText('Document (5)').should('exist').click() cy.wrap(fileRepository.getAllByDatasetPersistentId).should( 'be.calledWith', diff --git a/tests/component/sections/dataset/dataset-files/file-criteria-form/FileCriteriaFilterByTag.spec.tsx b/tests/component/sections/dataset/dataset-files/file-criteria-form/FileCriteriaFilterByTag.spec.tsx index 96bbcffcf..d2acad803 100644 --- a/tests/component/sections/dataset/dataset-files/file-criteria-form/FileCriteriaFilterByTag.spec.tsx +++ b/tests/component/sections/dataset/dataset-files/file-criteria-form/FileCriteriaFilterByTag.spec.tsx @@ -30,7 +30,7 @@ describe('FilesCriteriaFilterByTag', () => { /> ) - cy.findByRole('button', { name: 'Filter Tag: All' }).click() + cy.findByRole('button', { name: 'File Tags: All' }).click() cy.findByText('All').should('exist') cy.findByText('Document (5)').should('exist') @@ -48,11 +48,11 @@ describe('FilesCriteriaFilterByTag', () => { /> ) - cy.findByRole('button', { name: 'Filter Tag: All' }).click() + cy.findByRole('button', { name: 'File Tags: All' }).click() cy.findByText('Document (5)').click() cy.wrap(onCriteriaChange).should('be.calledWith', defaultCriteria.withFilterByTag('document')) - cy.findByRole('button', { name: 'Filter Tag: Document' }).click() + cy.findByRole('button', { name: 'File Tags: Document' }).click() cy.findByText('Data (10)').click() cy.wrap(onCriteriaChange).should('be.calledWith', defaultCriteria.withFilterByTag('data')) }) @@ -69,7 +69,7 @@ describe('FilesCriteriaFilterByTag', () => { /> ) - cy.findByRole('button', { name: 'Filter Tag: Document' }).click() + cy.findByRole('button', { name: 'File Tags: Document' }).click() cy.findByText('All').should('exist').click() cy.wrap(onCriteriaChange).should('be.calledWith', defaultCriteria.withFilterByType(undefined)) }) @@ -85,15 +85,15 @@ describe('FilesCriteriaFilterByTag', () => { /> ) - cy.findByRole('button', { name: 'Filter Tag: All' }).click() + cy.findByRole('button', { name: 'File Tags: All' }).click() cy.findByText('All').should('have.class', styles['selected-option']) cy.findByRole('button', { name: 'Document (5)' }).click() - cy.findByRole('button', { name: 'Filter Tag: Document' }).click() + cy.findByRole('button', { name: 'File Tags: Document' }).click() cy.findByText('Document (5)').should('have.class', styles['selected-option']) cy.findByRole('button', { name: 'Data (10)' }).click() - cy.findByRole('button', { name: 'Filter Tag: Data' }).click() + cy.findByRole('button', { name: 'File Tags: Data' }).click() cy.findByText('Data (10)').should('have.class', styles['selected-option']) }) @@ -111,6 +111,6 @@ describe('FilesCriteriaFilterByTag', () => { /> ) - cy.findByRole('button', { name: 'Filter Tag: All' }).should('not.exist') + cy.findByRole('button', { name: 'File Tags: All' }).should('not.exist') }) }) diff --git a/tests/component/sections/dataset/dataset-files/file-criteria-form/FileCriteriaFilterByType.spec.tsx b/tests/component/sections/dataset/dataset-files/file-criteria-form/FileCriteriaFilterByType.spec.tsx index 3751936b6..e6c72496f 100644 --- a/tests/component/sections/dataset/dataset-files/file-criteria-form/FileCriteriaFilterByType.spec.tsx +++ b/tests/component/sections/dataset/dataset-files/file-criteria-form/FileCriteriaFilterByType.spec.tsx @@ -30,7 +30,7 @@ describe('FilesCriteriaFilterByType', () => { /> ) - cy.findByRole('button', { name: 'Filter Type: All' }).click() + cy.findByRole('button', { name: 'File Type: All' }).click() cy.findByText('All').should('exist') cy.findByText('Image (5)').should('exist') @@ -48,11 +48,11 @@ describe('FilesCriteriaFilterByType', () => { /> ) - cy.findByRole('button', { name: 'Filter Type: All' }).click() + cy.findByRole('button', { name: 'File Type: All' }).click() cy.findByText('Image (5)').click() cy.wrap(onCriteriaChange).should('be.calledWith', defaultCriteria.withFilterByType('image')) - cy.findByRole('button', { name: 'Filter Type: Image' }).click() + cy.findByRole('button', { name: 'File Type: Image' }).click() cy.findByText('Text (10)').click() cy.wrap(onCriteriaChange).should('be.calledWith', defaultCriteria.withFilterByType('text')) }) @@ -69,7 +69,7 @@ describe('FilesCriteriaFilterByType', () => { /> ) - cy.findByRole('button', { name: 'Filter Type: Image' }).click() + cy.findByRole('button', { name: 'File Type: Image' }).click() cy.findByText('All').should('exist').click() cy.wrap(onCriteriaChange).should('be.calledWith', defaultCriteria.withFilterByType(undefined)) }) @@ -85,15 +85,15 @@ describe('FilesCriteriaFilterByType', () => { /> ) - cy.findByRole('button', { name: 'Filter Type: All' }).click() + cy.findByRole('button', { name: 'File Type: All' }).click() cy.findByText('All').should('have.class', styles['selected-option']) cy.findByRole('button', { name: 'Image (5)' }).click() - cy.findByRole('button', { name: 'Filter Type: Image' }).click() + cy.findByRole('button', { name: 'File Type: Image' }).click() cy.findByText('Image (5)').should('have.class', styles['selected-option']) cy.findByRole('button', { name: 'Text (10)' }).click() - cy.findByRole('button', { name: 'Filter Type: Text' }).click() + cy.findByRole('button', { name: 'File Type: Text' }).click() cy.findByText('Text (10)').should('have.class', styles['selected-option']) }) @@ -109,6 +109,6 @@ describe('FilesCriteriaFilterByType', () => { /> ) - cy.findByRole('button', { name: 'Filter Type: Image' }).should('not.exist') + cy.findByRole('button', { name: 'File Type: Image' }).should('not.exist') }) }) diff --git a/tests/component/sections/dataset/dataset-files/file-criteria-form/FileCriteriaFilters.spec.tsx b/tests/component/sections/dataset/dataset-files/file-criteria-form/FileCriteriaFilters.spec.tsx index 986860a8e..88ec100ed 100644 --- a/tests/component/sections/dataset/dataset-files/file-criteria-form/FileCriteriaFilters.spec.tsx +++ b/tests/component/sections/dataset/dataset-files/file-criteria-form/FileCriteriaFilters.spec.tsx @@ -31,9 +31,9 @@ describe('FilesCriteriaFilters', () => { cy.findByText('Filter by').should('exist') - cy.findByRole('button', { name: 'Filter Type: All' }).should('exist') + cy.findByRole('button', { name: 'File Type: All' }).should('exist') cy.findByRole('button', { name: 'Access: All' }).should('exist') - cy.findByRole('button', { name: 'Filter Tag: All' }).should('exist') + cy.findByRole('button', { name: 'File Tags: All' }).should('exist') }) it('does not render filters by type options when there are no filters to be applied', () => { @@ -50,8 +50,8 @@ describe('FilesCriteriaFilters', () => { cy.findByText('Filter by').should('not.exist') - cy.findByRole('button', { name: 'Filter Type: All' }).should('not.exist') + cy.findByRole('button', { name: 'File Type: All' }).should('not.exist') cy.findByRole('button', { name: 'Access: All' }).should('not.exist') - cy.findByRole('button', { name: 'Filter Tag: All' }).should('not.exist') + cy.findByRole('button', { name: 'File Tags: All' }).should('not.exist') }) }) diff --git a/tests/component/sections/dataset/dataset-files/file-criteria-form/FileCriteriaForm.spec.tsx b/tests/component/sections/dataset/dataset-files/file-criteria-form/FileCriteriaForm.spec.tsx index a548f0eeb..04f856069 100644 --- a/tests/component/sections/dataset/dataset-files/file-criteria-form/FileCriteriaForm.spec.tsx +++ b/tests/component/sections/dataset/dataset-files/file-criteria-form/FileCriteriaForm.spec.tsx @@ -58,9 +58,9 @@ describe('FileCriteriaForm', () => { ) cy.findByRole('button', { name: /Sort/ }).should('not.exist') - cy.findByRole('button', { name: 'Filter Type: All' }).should('not.exist') + cy.findByRole('button', { name: 'File Type: All' }).should('not.exist') cy.findByRole('button', { name: 'Access: All' }).should('not.exist') - cy.findByRole('button', { name: 'Filter Tag: All' }).should('not.exist') + cy.findByRole('button', { name: 'File Tags: All' }).should('not.exist') cy.findByLabelText('Search').should('not.exist') }) @@ -85,9 +85,9 @@ describe('FileCriteriaForm', () => { /> ) - cy.findByRole('button', { name: 'Filter Type: All' }).should('exist') + cy.findByRole('button', { name: 'File Type: All' }).should('exist') cy.findByRole('button', { name: 'Access: All' }).should('exist') - cy.findByRole('button', { name: 'Filter Tag: All' }).should('exist') + cy.findByRole('button', { name: 'File Tags: All' }).should('exist') cy.findByText('Filter by').should('exist') }) @@ -120,9 +120,9 @@ describe('FileCriteriaForm', () => { cy.findByRole('button', { name: /Sort/ }).click() cy.findByText('Oldest').click() - cy.findByRole('button', { name: 'Filter Type: Image' }).should('exist') + cy.findByRole('button', { name: 'File Type: Image' }).should('exist') cy.findByRole('button', { name: 'Access: Public' }).should('exist') - cy.findByRole('button', { name: 'Filter Tag: Document' }).should('exist') + cy.findByRole('button', { name: 'File Tags: Document' }).should('exist') }) it('saves global criteria when the filter by type option changes', () => { @@ -139,12 +139,12 @@ describe('FileCriteriaForm', () => { /> ) - cy.findByRole('button', { name: 'Filter Type: Image' }).click() + cy.findByRole('button', { name: 'File Type: Image' }).click() cy.findByText('Text (10)').click() - cy.findByRole('button', { name: 'Filter Type: Text' }).should('exist') + cy.findByRole('button', { name: 'File Type: Text' }).should('exist') cy.findByRole('button', { name: 'Access: Public' }).should('exist') - cy.findByRole('button', { name: 'Filter Tag: Document' }).should('exist') + cy.findByRole('button', { name: 'File Tags: Document' }).should('exist') }) it('saves global criteria when the filter by access option changes', () => { @@ -164,9 +164,9 @@ describe('FileCriteriaForm', () => { cy.findByRole('button', { name: 'Access: Public' }).click() cy.findByText('Restricted (10)').click() - cy.findByRole('button', { name: 'Filter Type: Image' }).should('exist') + cy.findByRole('button', { name: 'File Type: Image' }).should('exist') cy.findByRole('button', { name: 'Access: Restricted' }).should('exist') - cy.findByRole('button', { name: 'Filter Tag: Document' }).should('exist') + cy.findByRole('button', { name: 'File Tags: Document' }).should('exist') }) it('saves global criteria when the filter by tag option changes', () => { @@ -183,12 +183,12 @@ describe('FileCriteriaForm', () => { /> ) - cy.findByRole('button', { name: 'Filter Tag: Document' }).click() + cy.findByRole('button', { name: 'File Tags: Document' }).click() cy.findByText('Data (10)').click() - cy.findByRole('button', { name: 'Filter Type: Image' }).should('exist') + cy.findByRole('button', { name: 'File Type: Image' }).should('exist') cy.findByRole('button', { name: 'Access: Public' }).should('exist') - cy.findByRole('button', { name: 'Filter Tag: Data' }).should('exist') + cy.findByRole('button', { name: 'File Tags: Data' }).should('exist') }) it('saves global criteria when the search input changes', () => { @@ -208,9 +208,9 @@ describe('FileCriteriaForm', () => { cy.findByLabelText('Search').clear().type('new search') - cy.findByRole('button', { name: 'Filter Type: Image' }).should('exist') + cy.findByRole('button', { name: 'File Type: Image' }).should('exist') cy.findByRole('button', { name: 'Access: Public' }).should('exist') - cy.findByRole('button', { name: 'Filter Tag: Document' }).should('exist') + cy.findByRole('button', { name: 'File Tags: Document' }).should('exist') cy.findByLabelText('Search').should('have.value', 'new search') }) diff --git a/tests/component/sections/dataset/dataset-files/files-table/FilesTable.spec.tsx b/tests/component/sections/dataset/dataset-files/files-table/FilesTable.spec.tsx index 7643c251d..72742c9f7 100644 --- a/tests/component/sections/dataset/dataset-files/files-table/FilesTable.spec.tsx +++ b/tests/component/sections/dataset/dataset-files/files-table/FilesTable.spec.tsx @@ -39,30 +39,36 @@ describe('FilesTable', () => { }) describe('Row selection', () => { - it('selects all rows when the header checkbox is clicked', () => { + it('selects all rows in the current page when the header checkbox is clicked', () => { cy.customMount( ) + cy.wait(1000) // wait for the table to load + cy.get('table > thead > tr > th > input[type=checkbox]').click() - cy.findByText('200 files are currently selected.').should('exist') + cy.findByText('10 files are currently selected.').should('exist') - cy.findByRole('button', { name: 'Select all 200 files in this dataset.' }).should('not.exist') + cy.findByRole('button', { name: 'Select all 200 files in this dataset.' }).should('exist') }) - it('clears row selection when the header checkbox is clicked', () => { + it.only('clears row selection for the current page when the header checkbox is clicked', () => { cy.customMount( ) - cy.get('table > thead > tr > th > input[type=checkbox]').click() + cy.wait(1000) // wait for the table to load + + cy.get('table > tbody > tr:nth-child(2) > td:nth-child(1) > input[type=checkbox]').click() + + cy.findByRole('button', { name: 'Select all 200 files in this dataset.' }).click() cy.findByText('200 files are currently selected.').should('exist') cy.get('table > thead > tr > th > input[type=checkbox]').click() - cy.findByText(/files are currently selected./).should('not.exist') + cy.findByText('190 files are currently selected.').should('exist') }) it("selects all rows when the 'Select all' button is clicked", () => { diff --git a/tests/component/sections/dataset/dataset-files/files-table/row-selection/RowSelectionMessage.spec.tsx b/tests/component/sections/dataset/dataset-files/files-table/row-selection/RowSelectionMessage.spec.tsx index e5eb4b997..5584bdb76 100644 --- a/tests/component/sections/dataset/dataset-files/files-table/row-selection/RowSelectionMessage.spec.tsx +++ b/tests/component/sections/dataset/dataset-files/files-table/row-selection/RowSelectionMessage.spec.tsx @@ -1,5 +1,5 @@ import { RowSelectionMessage } from '../../../../../../../src/sections/dataset/dataset-files/files-table/row-selection/RowSelectionMessage' -import { createRowSelection } from '../../../../../../../src/sections/dataset/dataset-files/files-table/row-selection/useRowSelection' +import { createRowSelection } from '../../../../../../../src/sections/dataset/dataset-files/files-table/row-selection/useFileSelection' let selectAllRows = () => {} let clearRowSelection = () => {} @@ -12,7 +12,7 @@ describe('RowSelectionMessage', () => { it('renders the message when there are more than 10 files and some row is selected', () => { cy.customMount( { it('does not render the message when there are less than 10 files', () => { cy.customMount( { it('does not render the message when there are more than 10 files but no row is selected', () => { cy.customMount( { it('renders the plural form of the message when there is more than 1 row selected', () => { cy.customMount( { it("calls selectAllRows when the 'Select all' button is clicked", () => { cy.customMount( { it("calls clearRowSelection when the 'Clear selection.' button is clicked", () => { cy.customMount( { - it('should render the component with no message if conditions are not met', () => { + it('should not render if there is less than 1 file selected', () => { const getSettingByName = cy .stub() .resolves(SettingMother.createZipDownloadLimit(zipDownloadLimit)) @@ -19,7 +19,9 @@ describe('ZipDownloadLimitMessage', () => { cy.customMount( ) @@ -27,13 +29,32 @@ describe('ZipDownloadLimitMessage', () => { cy.findByText(/The overall size of the files selected/).should('not.exist') }) - it('should render the component with the appropriate message if conditions are met', () => { + it('should not render if the zipDownloadLimit is not exceeded', () => { + const getSettingByName = cy + .stub() + .resolves(SettingMother.createZipDownloadLimit(zipDownloadLimit)) + + cy.customMount( + + + + ) + + cy.findByText(/The overall size of the files selected/).should('not.exist') + }) + + it('should render if there is more than 1 file and they exceed the zipDownloadLimit', () => { const getSettingByName = cy .stub() .resolves(SettingMother.createZipDownloadLimit(zipDownloadLimit)) cy.customMount( - + ) @@ -46,13 +67,13 @@ describe('ZipDownloadLimitMessage', () => { const getSettingByName = cy .stub() .resolves(SettingMother.createZipDownloadLimit(zipDownloadLimit)) - const files = [ - FileMother.create({ size: new FileSize(1000000, FileSizeUnit.PETABYTES) }), - FileMother.create({ size: new FileSize(1000000, FileSizeUnit.PETABYTES) }) - ] + const fileSelection = { + '1': FileMother.create({ size: new FileSize(1000000, FileSizeUnit.PETABYTES) }), + '2': FileMother.create({ size: new FileSize(1000000, FileSizeUnit.PETABYTES) }) + } cy.customMount( - + ) diff --git a/tests/component/sections/dataset/dataset-metadata/DatasetMetadata.spec.tsx b/tests/component/sections/dataset/dataset-metadata/DatasetMetadata.spec.tsx index 7cd582a20..de70cdffa 100644 --- a/tests/component/sections/dataset/dataset-metadata/DatasetMetadata.spec.tsx +++ b/tests/component/sections/dataset/dataset-metadata/DatasetMetadata.spec.tsx @@ -7,13 +7,70 @@ import { import { AnonymizedContext } from '../../../../../src/sections/dataset/anonymized/AnonymizedContext' import { isArrayOfObjects, - metadataFieldValueToString -} from '../../../../../src/sections/dataset/dataset-metadata/dataset-metadata-fields/DatasetMetadataFieldValue' + metadataFieldValueToDisplayFormat +} from '../../../../../src/sections/dataset/dataset-metadata/dataset-metadata-fields/DatasetMetadataFieldValueFormatted' import { MetadataBlockInfoProvider } from '../../../../../src/sections/dataset/metadata-block-info/MetadataBlockProvider' import { MetadataBlockInfoRepository } from '../../../../../src/metadata-block-info/domain/repositories/MetadataBlockInfoRepository' import { MetadataBlockInfoMother } from '../../../metadata-block-info/domain/models/MetadataBlockInfoMother' +import { METADATA_FIELD_DISPLAY_FORMAT_NAME_PLACEHOLDER } from '../../../../../src/metadata-block-info/domain/models/MetadataBlockInfo' + +const extractLinksFromText = (text: string): { text: string; link: string }[] => { + const linkFormat = /(? { + const matchResult = match.match(/\[(.*?)\]\((.*?)\)/) + if (matchResult) { + const [, text, link] = matchResult + return { text, link } + } + return null + }) + .filter((match) => match !== null) as { text: string; link: string }[] +} + +const extractImagesFromText = (text: string): string[] => { + return text.match(/!\[(.*?)\]\((.*?)\)/g) || [] +} describe('DatasetMetadata', () => { + const checkMetadataFieldValue = (metadataFieldName: string, metadataFieldValue: string) => { + const extractedLinks = extractLinksFromText(metadataFieldValue) + const extractedImages = extractImagesFromText(metadataFieldValue) + const notPlainText = extractedLinks.length > 0 || extractedImages + + if (notPlainText) { + if (extractedLinks) { + extractedLinks.forEach(({ text, link }) => { + const translatedText = text.replaceAll( + METADATA_FIELD_DISPLAY_FORMAT_NAME_PLACEHOLDER, + metadataFieldName + ) + cy.findByText(translatedText).should('exist') + cy.findByText(translatedText).should('have.attr', 'href', link) + }) + } + if (extractedImages) { + extractedImages.forEach((image) => { + const [, altText, imageUrl] = image.match(/!\[(.*?)\]\((.*?)\)/) || [] + const translatedAltText = altText.replaceAll( + METADATA_FIELD_DISPLAY_FORMAT_NAME_PLACEHOLDER, + metadataFieldName + ) + cy.findByAltText(translatedAltText).should('exist') + cy.findByAltText(translatedAltText).should('have.attr', 'src', imageUrl) + }) + } + } else { + cy.findByText(metadataFieldValue).should('exist') + } + } + it('renders the metadata blocks sections titles correctly', () => { const mockDataset = DatasetMother.create() const mockMetadataBlocks = mockDataset.metadataBlocks @@ -130,7 +187,9 @@ describe('DatasetMetadata', () => { } Object.entries(metadataBlock.fields).forEach(([metadataFieldName, metadataFieldValue]) => { - const metadataFieldValueString = metadataFieldValueToString( + const metadataFieldNameTranslated = t[metadataBlock.name].datasetField[metadataFieldName] + .name as string + const metadataFieldValueString = metadataFieldValueToDisplayFormat( metadataFieldName, metadataFieldValue, metadataBlockInfoMock @@ -138,15 +197,12 @@ describe('DatasetMetadata', () => { if (isArrayOfObjects(metadataFieldValue)) { metadataFieldValueString.split(' \n \n').forEach((fieldValue) => { - cy.findAllByText(fieldValue).should('exist') + checkMetadataFieldValue(metadataFieldNameTranslated, fieldValue) }) return } - const fieldValue = cy.findAllByText(metadataFieldValueString, { - exact: false - }) - fieldValue.should('exist') + checkMetadataFieldValue(metadataFieldNameTranslated, metadataFieldValueString) }) }) }) diff --git a/tests/component/sections/dataset/dataset-summary/SummaryFields.spec.tsx b/tests/component/sections/dataset/dataset-summary/SummaryFields.spec.tsx index 4124e08b1..903d9664e 100644 --- a/tests/component/sections/dataset/dataset-summary/SummaryFields.spec.tsx +++ b/tests/component/sections/dataset/dataset-summary/SummaryFields.spec.tsx @@ -3,8 +3,8 @@ import { SummaryFields } from '../../../../../src/sections/dataset/dataset-summa import { DatasetMother } from '../../../dataset/domain/models/DatasetMother' import { isArrayOfObjects, - metadataFieldValueToString -} from '../../../../../src/sections/dataset/dataset-metadata/dataset-metadata-fields/DatasetMetadataFieldValue' + metadataFieldValueToDisplayFormat +} from '../../../../../src/sections/dataset/dataset-metadata/dataset-metadata-fields/DatasetMetadataFieldValueFormatted' import { MetadataBlockInfoMother } from '../../../metadata-block-info/domain/models/MetadataBlockInfoMother' import { MetadataBlockInfoRepository } from '../../../../../src/metadata-block-info/domain/repositories/MetadataBlockInfoRepository' import { MetadataBlockInfoProvider } from '../../../../../src/sections/dataset/metadata-block-info/MetadataBlockProvider' @@ -38,7 +38,7 @@ describe('DatasetSummary', () => { ) summaryFieldDescription.should('exist') - const summaryFieldValueString = metadataFieldValueToString( + const summaryFieldValueString = metadataFieldValueToDisplayFormat( summaryFieldName, summaryFieldValue, metadataBlockInfoMock diff --git a/tests/e2e-integration/integration/DataverseApiHelper.ts b/tests/e2e-integration/integration/DataverseApiHelper.ts index 742069b78..745729f85 100644 --- a/tests/e2e-integration/integration/DataverseApiHelper.ts +++ b/tests/e2e-integration/integration/DataverseApiHelper.ts @@ -14,13 +14,18 @@ export class DataverseApiHelper { } // eslint-disable-next-line @typescript-eslint/no-explicit-any - static async request(url: string, method: string, data?: any): Promise { + static async request( + url: string, + method: string, + data?: any, + contentType?: string + ): Promise { const config: AxiosRequestConfig = { url: `${this.API_URL}${url}`, method: method, headers: { 'X-Dataverse-key': this.API_TOKEN, - 'Content-Type': 'application/json' + 'Content-Type': contentType ? contentType : 'application/json' }, // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment data: data diff --git a/tests/e2e-integration/integration/datasets/DatasetHelper.ts b/tests/e2e-integration/integration/datasets/DatasetHelper.ts index 270d57fae..e1699112d 100644 --- a/tests/e2e-integration/integration/datasets/DatasetHelper.ts +++ b/tests/e2e-integration/integration/datasets/DatasetHelper.ts @@ -21,4 +21,16 @@ export class DatasetHelper extends DataverseApiHelper { static async createPrivateUrl(id: string): Promise<{ token: string }> { return this.request<{ token: string }>(`/datasets/${id}/privateUrl`, 'POST') } + + static async setCitationDateFieldType( + persistentId: string, + fieldType: string + ): Promise<{ status: string }> { + return this.request<{ status: string }>( + `/datasets/:persistentId/citationdate?persistentId=${persistentId}`, + 'PUT', + fieldType, + 'text/plain' + ) + } } diff --git a/tests/e2e-integration/integration/datasets/DatasetJSDataverseRepository.spec.ts b/tests/e2e-integration/integration/datasets/DatasetJSDataverseRepository.spec.ts index 0d9dd37ba..2ae873d48 100644 --- a/tests/e2e-integration/integration/datasets/DatasetJSDataverseRepository.spec.ts +++ b/tests/e2e-integration/integration/datasets/DatasetJSDataverseRepository.spec.ts @@ -8,6 +8,13 @@ import { DatasetStatus, DatasetVersion } from '../../../../src/dataset/domain/mo chai.use(chaiAsPromised) const expect = chai.expect +function getCurrentDateInYYYYMMDDFormat() { + const date = new Date() + return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String( + date.getDate() + ).padStart(2, '0')}` +} + const datasetData = (persistentId: string) => { const persistentIdUrl = `https://doi.org/${persistentId.replace('doi:', '')}` return { @@ -86,6 +93,8 @@ describe('Dataset JSDataverse Repository', () => { expect(dataset.metadataBlocks).to.deep.equal(datasetExpected.metadataBlocks) expect(dataset.summaryFields).to.deep.equal(datasetExpected.summaryFields) expect(dataset.version).to.deep.equal(datasetExpected.version) + expect(dataset.metadataBlocks[0].fields.publicationDate).not.to.exist + expect(dataset.metadataBlocks[0].fields.citationDate).not.to.exist }) }) @@ -103,9 +112,13 @@ describe('Dataset JSDataverse Repository', () => { } const datasetExpected = datasetData(dataset.persistentId) const newVersion = new DatasetVersion(1, 0, DatasetStatus.RELEASED) - + const expectedPublicationDate = getCurrentDateInYYYYMMDDFormat() expect(dataset.getTitle()).to.deep.equal(datasetExpected.title) expect(dataset.version).to.deep.equal(newVersion) + expect(dataset.metadataBlocks[0].fields.publicationDate).to.deep.equal( + expectedPublicationDate + ) + expect(dataset.metadataBlocks[0].fields.citationDate).not.to.exist }) }) @@ -139,4 +152,26 @@ describe('Dataset JSDataverse Repository', () => { expect(dataset.version).to.deep.equal(datasetExpected.version) }) }) + + it('gets the dataset after changing the citation date field type', async () => { + const datasetResponse = await DatasetHelper.createDataset() + + await DatasetHelper.publishDataset(datasetResponse.persistentId) + await IntegrationTestsUtils.wait(1500) + + await DatasetHelper.setCitationDateFieldType(datasetResponse.persistentId, 'dateOfDeposit') + + await datasetRepository + .getByPersistentId(datasetResponse.persistentId, '1.0') + .then((dataset) => { + if (!dataset) { + throw new Error('Dataset not found') + } + const expectedPublicationDate = getCurrentDateInYYYYMMDDFormat() + expect(dataset.metadataBlocks[0].fields.publicationDate).to.deep.equal( + expectedPublicationDate + ) + expect(dataset.metadataBlocks[0].fields.citationDate).not.to.exist + }) + }) }) diff --git a/tests/e2e-integration/integration/metadata-block-info/MetadataBlockInfoCitationExample.ts b/tests/e2e-integration/integration/metadata-block-info/MetadataBlockInfoCitationExample.ts new file mode 100644 index 000000000..86ae7db65 --- /dev/null +++ b/tests/e2e-integration/integration/metadata-block-info/MetadataBlockInfoCitationExample.ts @@ -0,0 +1,83 @@ +export const MetadataBlockInfoCitationExample = { + name: 'citation', + fields: { + title: { displayFormat: '' }, + subtitle: { displayFormat: '' }, + alternativeTitle: { displayFormat: '' }, + alternativeURL: { displayFormat: '[#VALUE](#VALUE)' }, + otherId: { displayFormat: ':' }, + otherIdAgency: { displayFormat: '#VALUE' }, + otherIdValue: { displayFormat: '#VALUE' }, + author: { displayFormat: '' }, + authorName: { displayFormat: '#VALUE' }, + authorAffiliation: { displayFormat: '(#VALUE)' }, + authorIdentifierScheme: { displayFormat: '- #VALUE:' }, + authorIdentifier: { displayFormat: '#VALUE' }, + datasetContact: { displayFormat: '' }, + datasetContactName: { displayFormat: '#VALUE' }, + datasetContactAffiliation: { displayFormat: '(#VALUE)' }, + datasetContactEmail: { displayFormat: '[#VALUE](mailto:#VALUE)' }, + dsDescription: { displayFormat: '' }, + dsDescriptionValue: { displayFormat: '#VALUE' }, + dsDescriptionDate: { displayFormat: '(#VALUE)' }, + subject: { displayFormat: '' }, + keyword: { displayFormat: '' }, + keywordValue: { displayFormat: '#VALUE' }, + keywordVocabulary: { displayFormat: '(#VALUE)' }, + keywordVocabularyURI: { displayFormat: '[#VALUE](#VALUE)' }, + topicClassification: { displayFormat: '' }, + topicClassValue: { displayFormat: '#VALUE' }, + topicClassVocab: { displayFormat: '(#VALUE)' }, + topicClassVocabURI: { displayFormat: '[#VALUE](#VALUE)' }, + publication: { displayFormat: '' }, + publicationCitation: { displayFormat: '#VALUE' }, + publicationIDType: { displayFormat: '#VALUE: ' }, + publicationIDNumber: { displayFormat: '#VALUE' }, + publicationURL: { displayFormat: '[#VALUE](#VALUE)' }, + notesText: { displayFormat: '' }, + language: { displayFormat: '' }, + producer: { displayFormat: '' }, + producerName: { displayFormat: '#VALUE' }, + producerAffiliation: { displayFormat: '(#VALUE)' }, + producerAbbreviation: { displayFormat: '(#VALUE)' }, + producerURL: { displayFormat: '[#VALUE](#VALUE)' }, + producerLogoURL: { displayFormat: '![#NAME](#VALUE)' }, + productionDate: { displayFormat: '' }, + productionPlace: { displayFormat: '' }, + contributor: { displayFormat: ':' }, + contributorType: { displayFormat: '#VALUE ' }, + contributorName: { displayFormat: '#VALUE' }, + grantNumber: { displayFormat: ':' }, + grantNumberAgency: { displayFormat: '#VALUE' }, + grantNumberValue: { displayFormat: '#VALUE' }, + distributor: { displayFormat: '' }, + distributorName: { displayFormat: '#VALUE' }, + distributorAffiliation: { displayFormat: '(#VALUE)' }, + distributorAbbreviation: { displayFormat: '(#VALUE)' }, + distributorURL: { displayFormat: '[#VALUE](#VALUE)' }, + distributorLogoURL: { displayFormat: '![#NAME](#VALUE)' }, + distributionDate: { displayFormat: '' }, + depositor: { displayFormat: '' }, + dateOfDeposit: { displayFormat: '' }, + timePeriodCovered: { displayFormat: ';' }, + timePeriodCoveredStart: { displayFormat: '#NAME: #VALUE ' }, + timePeriodCoveredEnd: { displayFormat: '#NAME: #VALUE ' }, + dateOfCollection: { displayFormat: ';' }, + dateOfCollectionStart: { displayFormat: '#NAME: #VALUE ' }, + dateOfCollectionEnd: { displayFormat: '#NAME: #VALUE ' }, + kindOfData: { displayFormat: '' }, + series: { displayFormat: ':' }, + seriesName: { displayFormat: '#VALUE' }, + seriesInformation: { displayFormat: '#VALUE' }, + software: { displayFormat: ',' }, + softwareName: { displayFormat: '#VALUE' }, + softwareVersion: { displayFormat: '#NAME: #VALUE' }, + relatedMaterial: { displayFormat: '' }, + relatedDatasets: { displayFormat: '' }, + otherReferences: { displayFormat: '' }, + dataSources: { displayFormat: '' }, + originOfSources: { displayFormat: '' }, + characteristicOfSources: { displayFormat: '' }, + accessToSources: { displayFormat: '' } + } +} diff --git a/tests/e2e-integration/integration/metadata-block-info/MetadataBlockInfoJSDataverseRepository.spec.ts b/tests/e2e-integration/integration/metadata-block-info/MetadataBlockInfoJSDataverseRepository.spec.ts new file mode 100644 index 000000000..5eb1d0636 --- /dev/null +++ b/tests/e2e-integration/integration/metadata-block-info/MetadataBlockInfoJSDataverseRepository.spec.ts @@ -0,0 +1,25 @@ +import chai from 'chai' +import chaiAsPromised from 'chai-as-promised' +import { IntegrationTestsUtils } from '../IntegrationTestsUtils' +import { MetadataBlockInfoJSDataverseRepository } from '../../../../src/metadata-block-info/infrastructure/repositories/MetadataBlockInfoJSDataverseRepository' +import { MetadataBlockInfoCitationExample } from './MetadataBlockInfoCitationExample' + +chai.use(chaiAsPromised) +const expect = chai.expect + +const metadataBlockInfoExpected = MetadataBlockInfoCitationExample +const metadataBlockInfoRepository = new MetadataBlockInfoJSDataverseRepository() +describe('Metadata Block Info JSDataverse Repository', () => { + before(() => IntegrationTestsUtils.setup()) + beforeEach(() => IntegrationTestsUtils.login()) + + it('gets the metadataBlockInfo by name', async () => { + await metadataBlockInfoRepository.getByName('citation').then((metadataBlockInfo) => { + if (!metadataBlockInfo) { + throw new Error('Metadata Block Info not found') + } + + expect(metadataBlockInfo).to.deep.equal(metadataBlockInfoExpected) + }) + }) +})