diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index af6770f38..5388afa9c 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -3,7 +3,7 @@ > All contributions to this project will be released under the Apache License, Version 2.0. > By submitting a pull request or filing a bug, issue, or > feature request, you are agreeing to comply with this waiver of copyright interest. -> Details can be found in our [LICENSE](LICENSE). +> Details can be found in our [LICENSE](/LICENSE). Thank you for your interest in contributing to Dataverse Frontend! We are open to contributions from everyone. You don't need permission to participate. Just jump in. If you have questions, please reach out using one or more of the channels @@ -81,7 +81,7 @@ guidelines to help: ### How to start After you’ve forked the Dataverse Frontend repository, you should follow the Getting Started instructions in the -[Developer Guide](DEVELOPER_GUIDE.md) to get your local environment up and running. +[Developer Guide](/DEVELOPER_GUIDE.md) to get your local environment up and running. ### GitHub reviews & assignments diff --git a/package-lock.json b/package-lock.json index 7513fce0b..6af6ac5b3 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-pr169.aa49f06", + "@iqss/dataverse-client-javascript": "2.0.0-pr187.f29c0e6", "@iqss/dataverse-design-system": "*", "@istanbuljs/nyc-config-typescript": "1.0.2", "@tanstack/react-table": "8.9.2", @@ -19,12 +19,14 @@ "@types/node": "16.18.12", "@types/react": "18.0.27", "@types/react-dom": "18.0.10", + "async-mutex": "0.5.0", "bootstrap": "5.2.3", "classnames": "2.5.1", "html-react-parser": "3.0.16", "i18next": "22.4.9", "i18next-browser-languagedetector": "7.0.1", "i18next-http-backend": "2.1.1", + "js-md5": "0.8.3", "lodash": "^4.17.21", "moment-timezone": "0.5.43", "react-bootstrap": "2.7.2", @@ -3672,9 +3674,9 @@ }, "node_modules/@iqss/dataverse-client-javascript": { "name": "@IQSS/dataverse-client-javascript", - "version": "2.0.0-pr169.aa49f06", - "resolved": "https://npm.pkg.github.com/download/@IQSS/dataverse-client-javascript/2.0.0-pr169.aa49f06/d8061b0af0068e530c6ef78b89e0a4ce668df4b3", - "integrity": "sha512-2D3wxWA87kU8EXltK7pBMGX9OoK7aecX869zbalbdtSNcPz9ATbZOOhDYbfaU+J526AyNTVQ6xlPTY5hWIRFvQ==", + "version": "2.0.0-pr187.f29c0e6", + "resolved": "https://npm.pkg.github.com/download/@IQSS/dataverse-client-javascript/2.0.0-pr187.f29c0e6/324f487a2b1437df668e34159404161888279638", + "integrity": "sha512-0OX9nmh7dY3Gg5euE7buCqTeyh+1B+GhFcDz2gJoND4oM3kIZalYS+bLsEoEekR2o25agP6b+ANyQ5kvZeFuig==", "license": "MIT", "dependencies": { "@types/node": "^18.15.11", @@ -17815,6 +17817,21 @@ "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", "dev": true }, + "node_modules/async-mutex": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", + "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/async-mutex/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "license": "0BSD" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -29214,6 +29231,11 @@ "@sideway/pinpoint": "^2.0.0" } }, + "node_modules/js-md5": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/js-md5/-/js-md5-0.8.3.tgz", + "integrity": "sha512-qR0HB5uP6wCuRMrWPTrkMaev7MJZwJuuw4fnwAzRgP4J4/F8RwtodOKpGp4XpqsLBFzzgqIO42efFAyz2Et6KQ==" + }, "node_modules/js-sdsl": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.2.tgz", diff --git a/package.json b/package.json index 894d361f3..446462aaa 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-pr169.aa49f06", + "@iqss/dataverse-client-javascript": "2.0.0-pr187.f29c0e6", "@iqss/dataverse-design-system": "*", "@istanbuljs/nyc-config-typescript": "1.0.2", "@tanstack/react-table": "8.9.2", @@ -44,7 +44,9 @@ "typescript": "4.9.5", "use-deep-compare": "1.2.1", "vite-plugin-istanbul": "4.0.1", - "web-vitals": "2.1.4" + "web-vitals": "2.1.4", + "js-md5": "0.8.3", + "async-mutex": "0.5.0" }, "scripts": { "start": "vite --base=/spa", diff --git a/src/collection/domain/models/CollectionPreview.ts b/src/collection/domain/models/CollectionPreview.ts new file mode 100644 index 000000000..c7842f611 --- /dev/null +++ b/src/collection/domain/models/CollectionPreview.ts @@ -0,0 +1,11 @@ +export interface CollectionPreview { + id: string + name: string + isReleased: boolean + releaseOrCreateDate: Date + parentCollectionId?: string + parentCollectionName?: string + description?: string + affiliation?: string + thumbnail?: string +} diff --git a/src/dataset/domain/models/Dataset.ts b/src/dataset/domain/models/Dataset.ts index c2a16c9a4..caa850de1 100644 --- a/src/dataset/domain/models/Dataset.ts +++ b/src/dataset/domain/models/Dataset.ts @@ -217,10 +217,6 @@ export enum DatasetNonNumericVersion { export enum DatasetNonNumericVersionSearchParam { DRAFT = 'DRAFT' } -// TODO: Maybe add this to some routing related folder or file -export enum QueryParamsKeys { - VERSION = 'version' -} export class DatasetVersionNumber { constructor(public readonly majorNumber?: number, public readonly minorNumber?: number) {} @@ -231,6 +227,12 @@ export class DatasetVersionNumber { } return `${this.majorNumber}.${this.minorNumber}` } + toSearchParam(): string { + if (this.majorNumber === undefined || this.minorNumber === undefined) { + return DatasetNonNumericVersionSearchParam.DRAFT + } + return this.toString() + } } export class DatasetVersion { diff --git a/src/dataset/infrastructure/repositories/DatasetJSDataverseRepository.ts b/src/dataset/infrastructure/repositories/DatasetJSDataverseRepository.ts index 13b19e963..b6fb8e0a9 100644 --- a/src/dataset/infrastructure/repositories/DatasetJSDataverseRepository.ts +++ b/src/dataset/infrastructure/repositories/DatasetJSDataverseRepository.ts @@ -132,15 +132,11 @@ export class DatasetJSDataverseRepository implements DatasetRepository { .execute(persistentId, version, includeDeaccessioned) .then((jsDataset) => this.fetchDatasetDetails(jsDataset, version)) .then((datasetDetails) => { - if (datasetDetails.jsDatasetPermissions.canEditDataset) { - return this.fetchDownloadSizes(persistentId, version).then((downloadSizes) => { - datasetDetails.jsDatasetFilesTotalOriginalDownloadSize = downloadSizes[0] - datasetDetails.jsDatasetFilesTotalArchivalDownloadSize = downloadSizes[1] - return datasetDetails - }) - } else { + return this.fetchDownloadSizes(persistentId, version).then((downloadSizes) => { + datasetDetails.jsDatasetFilesTotalOriginalDownloadSize = downloadSizes[0] + datasetDetails.jsDatasetFilesTotalArchivalDownloadSize = downloadSizes[1] return datasetDetails - } + }) }) .then((datasetDetails) => { return JSDatasetMapper.toDataset( diff --git a/src/files/domain/models/FilePreview.ts b/src/files/domain/models/FilePreview.ts index 74426be1e..19e8426d9 100644 --- a/src/files/domain/models/FilePreview.ts +++ b/src/files/domain/models/FilePreview.ts @@ -1,7 +1,10 @@ import { FileMetadata } from './FileMetadata' import { FileIngest } from './FileIngest' import { FileAccess } from './FileAccess' -import { DatasetPublishingStatus } from '../../../dataset/domain/models/Dataset' +import { + DatasetPublishingStatus, + DatasetVersionNumber +} from '../../../dataset/domain/models/Dataset' import { FilePermissions } from './FilePermissions' export interface FilePreview { @@ -12,6 +15,7 @@ export interface FilePreview { ingest: FileIngest metadata: FileMetadata permissions: FilePermissions + datasetVersionNumber?: DatasetVersionNumber releaseOrCreateDate?: Date someDatasetVersionHasBeenReleased?: boolean datasetPersistentId?: string diff --git a/src/files/domain/models/FileUploadState.ts b/src/files/domain/models/FileUploadState.ts index 854cb3035..672e9c42b 100644 --- a/src/files/domain/models/FileUploadState.ts +++ b/src/files/domain/models/FileUploadState.ts @@ -1,4 +1,6 @@ +import { UploadedFileDTO } from '@iqss/dataverse-client-javascript' import { FileSize, FileSizeUnit } from './FileMetadata' +import { UploadedFileDTOMapper } from '../../infrastructure/mappers/UploadedFileDTOMapper' export interface FileUploadState { progress: number @@ -17,6 +19,7 @@ export interface FileUploadState { description?: string tags: string[] restricted: boolean + checksumValue?: string } export interface FileUploaderState { @@ -94,6 +97,18 @@ export class FileUploadTools { return { state: oldState.state, uploaded: this.toUploaded(oldState.state) } } + static checksum( + file: File, + checksumValue: string, + oldState: FileUploaderState + ): FileUploaderState { + const fileUploadState = oldState.state.get(this.key(file)) + if (fileUploadState) { + fileUploadState.checksumValue = checksumValue + } + return { state: oldState.state, uploaded: this.toUploaded(oldState.state) } + } + static failed(file: File, oldState: FileUploaderState): FileUploaderState { const fileUploadState = oldState.state.get(this.key(file)) if (fileUploadState) { @@ -127,6 +142,21 @@ export class FileUploadTools { return { state: oldState.state, uploaded: this.toUploaded(oldState.state) } } + static mapToUploadedFilesDTOs(state: FileUploadState[]): UploadedFileDTO[] { + return state.map((uploadedFile) => + UploadedFileDTOMapper.toUploadedFileDTO( + uploadedFile.fileName, + uploadedFile.description, + uploadedFile.fileDir, + uploadedFile.tags, + uploadedFile.restricted, + uploadedFile.storageId as string, + uploadedFile.checksumValue as string, + uploadedFile.fileType === '' ? 'application/octet-stream' : uploadedFile.fileType // some browsers (e.g., chromium for .java files) fail to detect the mime type for some files and leave the fileType as an empty string, we use the default value 'application/octet-stream' in that case + ) + ) + } + private static toNewState( file: File, oldState: FileUploaderState diff --git a/src/files/domain/repositories/FileRepository.ts b/src/files/domain/repositories/FileRepository.ts index 9c2c30478..6888bd3a1 100644 --- a/src/files/domain/repositories/FileRepository.ts +++ b/src/files/domain/repositories/FileRepository.ts @@ -7,7 +7,7 @@ import { FilePaginationInfo } from '../models/FilePaginationInfo' import { FilePreview } from '../models/FilePreview' import { FilesWithCount } from '../models/FilesWithCount' import { FileHolder } from '../models/FileHolder' -import { FileUploadState } from '../models/FileUploadState' +import { UploadedFileDTO } from '@iqss/dataverse-client-javascript' export interface FileRepository { getAllByDatasetPersistentId: ( @@ -42,10 +42,5 @@ export interface FileRepository { abortController: AbortController, storageIdSetter: (storageId: string) => void ) => Promise - addUploadedFiles: (datasetId: number | string, files: FileUploadState[]) => Promise - addUploadedFile: ( - datasetId: number | string, - file: FileHolder, - storageId: string - ) => Promise + addUploadedFiles: (datasetId: number | string, files: UploadedFileDTO[]) => Promise } diff --git a/src/files/domain/useCases/addUploadedFiles.ts b/src/files/domain/useCases/addUploadedFiles.ts index b1ef42065..360a8497a 100644 --- a/src/files/domain/useCases/addUploadedFiles.ts +++ b/src/files/domain/useCases/addUploadedFiles.ts @@ -1,31 +1,16 @@ -import { FileUploadState } from '../models/FileUploadState' +import { UploadedFileDTO } from '@iqss/dataverse-client-javascript' import { FileRepository } from '../repositories/FileRepository' export function addUploadedFiles( fileRepository: FileRepository, datasetId: number | string, - files: FileUploadState[], + files: UploadedFileDTO[], done: () => void ): void { fileRepository .addUploadedFiles(datasetId, files) + .then(done) .catch((error: Error) => { throw new Error(error.message) }) - .finally(done) -} - -export function addUploadedFile( - fileRepository: FileRepository, - datasetId: number | string, - file: File, - storageId: string, - done: () => void -): void { - fileRepository - .addUploadedFile(datasetId, { file: file }, storageId) - .catch((error: Error) => { - throw new Error(error.message) - }) - .finally(done) } diff --git a/src/files/infrastructure/FileJSDataverseRepository.ts b/src/files/infrastructure/FileJSDataverseRepository.ts index 335450244..2b204a17f 100644 --- a/src/files/infrastructure/FileJSDataverseRepository.ts +++ b/src/files/infrastructure/FileJSDataverseRepository.ts @@ -15,6 +15,8 @@ import { getFileDownloadCount, getFileUserPermissions, uploadFile as jsUploadFile, + addUploadedFilesToDataset, + UploadedFileDTO, ReadError } from '@iqss/dataverse-client-javascript' import { FileCriteria } from '../domain/models/FileCriteria' @@ -31,7 +33,6 @@ import { FilePermissions } from '../domain/models/FilePermissions' import { JSFilePermissionsMapper } from './mappers/JSFilePermissionsMapper' import { FilesWithCount } from '../domain/models/FilesWithCount' import { FileHolder } from '../domain/models/FileHolder' -import { FileUploadState } from '../domain/models/FileUploadState' const includeDeaccessioned = true @@ -300,13 +301,7 @@ export class FileJSDataverseRepository implements FileRepository { }) } - addUploadedFiles(_datasetId: number | string, _files: FileUploadState[]): Promise { - // TODO: not yet implemented - return new Promise(() => {}) - } - - addUploadedFile(_datasetId: number | string, _file: FileHolder): Promise { - return new Promise(() => {}) - // return addUploadedFilesToDataset.execute(datasetId, file.file) + addUploadedFiles(datasetId: number | string, uploadedFiles: UploadedFileDTO[]): Promise { + return addUploadedFilesToDataset.execute(datasetId, uploadedFiles) } } diff --git a/src/files/infrastructure/mappers/JSFileMapper.ts b/src/files/infrastructure/mappers/JSFileMapper.ts index 680e441d2..e28bebfe3 100644 --- a/src/files/infrastructure/mappers/JSFileMapper.ts +++ b/src/files/infrastructure/mappers/JSFileMapper.ts @@ -32,6 +32,7 @@ export class JSFileMapper { id: this.toFileId(jsFile.id), name: this.toFileName(jsFile.name), datasetPublishingStatus: datasetVersion.publishingStatus, + datasetVersionNumber: datasetVersion.number, access: JSFileAccessMapper.toFileAccess(jsFile.restricted), ingest: JSFileIngestMapper.toFileIngest(), metadata: JSFileMetadataMapper.toFileMetadata(jsFile, downloadsCount, thumbnail, tabularData), diff --git a/src/files/infrastructure/mappers/UploadedFileDTOMapper.ts b/src/files/infrastructure/mappers/UploadedFileDTOMapper.ts new file mode 100644 index 000000000..238027eba --- /dev/null +++ b/src/files/infrastructure/mappers/UploadedFileDTOMapper.ts @@ -0,0 +1,26 @@ +import { UploadedFileDTO } from '@iqss/dataverse-client-javascript' + +export class UploadedFileDTOMapper { + static toUploadedFileDTO( + fileName: string, + description: string | undefined, + fileDir: string, + tags: string[], + restricted: boolean, + storageId: string, + checksumValue: string, + fileType: string + ): UploadedFileDTO { + return { + fileName: fileName, + description: description, + directoryLabel: fileDir, + categories: tags, + restrict: restricted, + storageId: storageId, + checksumValue: checksumValue, + checksumType: 'md5', + mimeType: fileType + } + } +} diff --git a/src/sections/Route.enum.ts b/src/sections/Route.enum.ts index 47abe7297..ee104c035 100644 --- a/src/sections/Route.enum.ts +++ b/src/sections/Route.enum.ts @@ -19,3 +19,8 @@ export const RouteWithParams = { CREATE_COLLECTION: (ownerCollectionId?: string) => `/collections/${ownerCollectionId ?? ROOT_COLLECTION_ALIAS}/create` } + +export enum QueryParamKey { + VERSION = 'version', + PERSISTENT_ID = 'persistentId' +} diff --git a/src/sections/collection/datasets-list/DatasetsList.tsx b/src/sections/collection/datasets-list/DatasetsList.tsx index a72202af2..a0240a9cd 100644 --- a/src/sections/collection/datasets-list/DatasetsList.tsx +++ b/src/sections/collection/datasets-list/DatasetsList.tsx @@ -56,7 +56,7 @@ export function DatasetsList({ datasetRepository, page, collectionId }: Datasets {datasets.map((dataset) => ( - + ))} {accumulatedDatasets.map((dataset) => ( - + ))} )} diff --git a/src/sections/collection/datasets-list/collection-card/CollectionCard.module.scss b/src/sections/collection/datasets-list/collection-card/CollectionCard.module.scss new file mode 100644 index 000000000..3bc6554fc --- /dev/null +++ b/src/sections/collection/datasets-list/collection-card/CollectionCard.module.scss @@ -0,0 +1,70 @@ +@use 'sass:color'; +@import "node_modules/@iqss/dataverse-design-system/src/lib/assets/styles/design-tokens/colors.module"; +@import "node_modules/@iqss/dataverse-design-system/src/lib/assets/styles/design-tokens/typography.module"; + +.container { + margin: 6px 0; + padding: 4px 10px; + border: 1px solid $dv-collection-border-color; +} + +.header { + display: flex; + justify-content: space-between; +} + +.title { + display: flex;gap: 8px; +} + +.icon { + margin-top: 2px; + color: $dv-collection-border-color; + font-size: 1.3em; + line-height: 1.1; + + > div >span { + margin-right: 0; + } +} + +.thumbnail { + width: 48px; + margin: 8px 12px 6px 0; + font-size: 2.8em; + + img { + vertical-align: top; + } +} + +.info { + display: flex; + color: $dv-subtext-color; +} + +.card-info-container { + display: flex; + font-size: $dv-font-size-sm; +} + +.description { + display: -webkit-box; -webkit-line-clamp: 3; line-clamp: 3; -webkit-box-orient: vertical; + flex-direction: column; + width: 100%; + overflow: hidden; + color: black; +} + +.date { + color: $dv-subtext-color; + +} + +.affiliation { + color: $dv-subtext-color; +} + +.badge { + margin-right: 0.5em; +} \ No newline at end of file diff --git a/src/sections/collection/datasets-list/collection-card/CollectionCard.tsx b/src/sections/collection/datasets-list/collection-card/CollectionCard.tsx new file mode 100644 index 000000000..c7c15691a --- /dev/null +++ b/src/sections/collection/datasets-list/collection-card/CollectionCard.tsx @@ -0,0 +1,24 @@ +import { CollectionCardHeader } from './CollectionCardHeader' +import { CollectionCardThumbnail } from './CollectionCardThumbnail' +import { CollectionCardInfo } from './CollectionCardInfo' +import { Stack } from '@iqss/dataverse-design-system' +import { CollectionPreview } from '../../../../collection/domain/models/CollectionPreview' +import styles from './CollectionCard.module.scss' + +interface CollectionCardProps { + collectionPreview: CollectionPreview +} + +export function CollectionCard({ collectionPreview }: CollectionCardProps) { + return ( +
+ +
+ + + + +
+
+ ) +} diff --git a/src/sections/collection/datasets-list/collection-card/CollectionCardHeader.tsx b/src/sections/collection/datasets-list/collection-card/CollectionCardHeader.tsx new file mode 100644 index 000000000..14f075034 --- /dev/null +++ b/src/sections/collection/datasets-list/collection-card/CollectionCardHeader.tsx @@ -0,0 +1,39 @@ +import { LinkToPage } from '../../../shared/link-to-page/LinkToPage' +import { Route } from '../../../Route.enum' +import { CollectionPreview } from '../../../../collection/domain/models/CollectionPreview' +import styles from './CollectionCard.module.scss' +import { Badge, Icon, IconName } from '@iqss/dataverse-design-system' +import { DvObjectType } from '../../../../shared/hierarchy/domain/models/UpwardHierarchyNode' + +interface CollectionCardHeaderProps { + collectionPreview: CollectionPreview +} + +export function CollectionCardHeader({ collectionPreview }: CollectionCardHeaderProps) { + return ( + <> +
+
+ + {collectionPreview.name} + + {collectionPreview.affiliation && ( + ({collectionPreview.affiliation}) + )} + {!collectionPreview.isReleased && ( +
+ Unpublished +
+ )} +
+ +
+ +
+
+ + ) +} diff --git a/src/sections/collection/datasets-list/collection-card/CollectionCardHelper.ts b/src/sections/collection/datasets-list/collection-card/CollectionCardHelper.ts new file mode 100644 index 000000000..28d49fe70 --- /dev/null +++ b/src/sections/collection/datasets-list/collection-card/CollectionCardHelper.ts @@ -0,0 +1,17 @@ +import { + DatasetLabel, + DatasetLabelSemanticMeaning, + DatasetLabelValue +} from '../../../../dataset/domain/models/Dataset' +export class CollectionCardHelper { + static getLabel(isReleased: boolean) { + const labels: DatasetLabel[] = [] + + if (!isReleased) { + labels.push( + new DatasetLabel(DatasetLabelSemanticMeaning.WARNING, DatasetLabelValue.UNPUBLISHED) + ) + } + return labels + } +} diff --git a/src/sections/collection/datasets-list/collection-card/CollectionCardInfo.tsx b/src/sections/collection/datasets-list/collection-card/CollectionCardInfo.tsx new file mode 100644 index 000000000..93c9b44e7 --- /dev/null +++ b/src/sections/collection/datasets-list/collection-card/CollectionCardInfo.tsx @@ -0,0 +1,35 @@ +import styles from './CollectionCard.module.scss' +import { DateHelper } from '../../../../shared/helpers/DateHelper' +import { Stack } from '@iqss/dataverse-design-system' +import { LinkToPage } from '../../../shared/link-to-page/LinkToPage' +import { Route } from '../../../Route.enum' +import { CollectionPreview } from '../../../../collection/domain/models/CollectionPreview' +import { DvObjectType } from '../../../../shared/hierarchy/domain/models/UpwardHierarchyNode' + +interface CollectionCardInfoProps { + collectionPreview: CollectionPreview +} + +export function CollectionCardInfo({ collectionPreview }: CollectionCardInfoProps) { + return ( +
+ + + + {DateHelper.toDisplayFormat(collectionPreview.releaseOrCreateDate)} + + {collectionPreview.parentCollectionName && collectionPreview.parentCollectionId && ( + + {collectionPreview.parentCollectionName} + + )} + + +

{collectionPreview.description}

+
+
+ ) +} diff --git a/src/sections/collection/datasets-list/collection-card/CollectionCardThumbnail.tsx b/src/sections/collection/datasets-list/collection-card/CollectionCardThumbnail.tsx new file mode 100644 index 000000000..b5eac9791 --- /dev/null +++ b/src/sections/collection/datasets-list/collection-card/CollectionCardThumbnail.tsx @@ -0,0 +1,33 @@ +import styles from './CollectionCard.module.scss' +import { LinkToPage } from '../../../shared/link-to-page/LinkToPage' +import { Route } from '../../../Route.enum' +import { Icon, IconName } from '@iqss/dataverse-design-system' +import { CollectionPreview } from '../../../../collection/domain/models/CollectionPreview' +import { DvObjectType } from '../../../../shared/hierarchy/domain/models/UpwardHierarchyNode' + +interface CollectionCardCardThumbnailProps { + collectionPreview: CollectionPreview +} + +export function CollectionCardThumbnail({ collectionPreview }: CollectionCardCardThumbnailProps) { + return ( +
+ + {collectionPreview.thumbnail ? ( + {collectionPreview.name} + ) : ( +
+ +
+ )} +
+
+ ) +} diff --git a/src/sections/collection/datasets-list/file-card/FileCardInfo.tsx b/src/sections/collection/datasets-list/file-card/FileCardInfo.tsx index c0cbafdd6..2e11ea917 100644 --- a/src/sections/collection/datasets-list/file-card/FileCardInfo.tsx +++ b/src/sections/collection/datasets-list/file-card/FileCardInfo.tsx @@ -8,6 +8,7 @@ import { FileChecksum } from '../../../dataset/dataset-files/files-table/file-in import { FileTabularData } from '../../../dataset/dataset-files/files-table/file-info/file-info-cell/file-info-data/FileTabularData' import { FileCardHelper } from './FileCardHelper' import { DvObjectType } from '../../../../shared/hierarchy/domain/models/UpwardHierarchyNode' +import { FileLabels } from '../../../file/file-labels/FileLabels' interface FileCardInfoProps { filePreview: FilePreview @@ -37,6 +38,7 @@ export function FileCardInfo({ filePreview, persistentId }: FileCardInfoProps) { +

{filePreview.metadata.description}

diff --git a/src/sections/dataset/Dataset.tsx b/src/sections/dataset/Dataset.tsx index 4d5b405dc..71e682288 100644 --- a/src/sections/dataset/Dataset.tsx +++ b/src/sections/dataset/Dataset.tsx @@ -23,7 +23,7 @@ import { DatasetAlerts } from './dataset-alerts/DatasetAlerts' import { DatasetFilesScrollable } from './dataset-files/DatasetFilesScrollable' import useCheckPublishCompleted from './useCheckPublishCompleted' import useUpdateDatasetAlerts from './useUpdateDatasetAlerts' -import { Route } from '../Route.enum' +import { QueryParamKey, Route } from '../Route.enum' import { MetadataBlockInfoRepository } from '../../metadata-block-info/domain/repositories/MetadataBlockInfoRepository' interface DatasetProps { @@ -60,7 +60,7 @@ export function Dataset({ useEffect(() => { if (publishInProgress && publishCompleted && dataset) { - navigate(`${Route.DATASETS}?persistentId=${dataset.persistentId}`, { + navigate(`${Route.DATASETS}?${QueryParamKey.PERSISTENT_ID}=${dataset.persistentId}`, { state: {}, replace: true }) 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 76bacc56f..f40440d1f 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 @@ -26,7 +26,11 @@ export function AccessDatasetMenu({ }: AccessDatasetMenuProps) { const { t } = useTranslation('dataset') + const flesToDownloadSizeIsZero = + fileDownloadSizes.map(({ value }) => value).reduce((acc, curr) => acc + curr, 0) === 0 + if ( + flesToDownloadSizeIsZero || !permissions.canDownloadFiles || (version.publishingStatus === DatasetPublishingStatus.DEACCESSIONED && !permissions.canUpdateDataset) 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 bdee13419..bff76e189 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 @@ -1,13 +1,17 @@ import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router-dom' -import { Dataset } from '../../../../dataset/domain/models/Dataset' +import { + Dataset, + DatasetNonNumericVersionSearchParam, + DatasetPublishingStatus +} from '../../../../dataset/domain/models/Dataset' import { DropdownButton, DropdownButtonItem } from '@iqss/dataverse-design-system' import { EditDatasetPermissionsMenu } from './EditDatasetPermissionsMenu' import { DeleteDatasetButton } from './DeleteDatasetButton' import { DeaccessionDatasetButton } from './DeaccessionDatasetButton' import { useNotImplementedModal } from '../../../not-implemented/NotImplementedModalContext' import { useSession } from '../../../session/SessionContext' -import { Route } from '../../../Route.enum' +import { QueryParamKey, Route } from '../../../Route.enum' interface EditDatasetMenuProps { dataset: Dataset @@ -29,12 +33,19 @@ export function EditDatasetMenu({ dataset }: EditDatasetMenuProps) { const navigate = useNavigate() const handleOnSelect = (eventKey: EditDatasetMenuItems | string | null) => { + const searchParams = new URLSearchParams() + searchParams.set(QueryParamKey.PERSISTENT_ID, dataset.persistentId) + + if (dataset.version.publishingStatus === DatasetPublishingStatus.DRAFT) { + searchParams.set(QueryParamKey.VERSION, DatasetNonNumericVersionSearchParam.DRAFT) + } + if (eventKey === EditDatasetMenuItems.FILES_UPLOAD) { - navigate(`${Route.UPLOAD_DATASET_FILES}?persistentId=${dataset.persistentId}`) + navigate(`${Route.UPLOAD_DATASET_FILES}?${searchParams.toString()}`) return } if (eventKey === EditDatasetMenuItems.METADATA) { - navigate(`${Route.EDIT_DATASET_METADATA}?persistentId=${dataset.persistentId}`) + navigate(`${Route.EDIT_DATASET_METADATA}?${searchParams.toString()}`) return } showModal() diff --git a/src/sections/dataset/dataset-files/dataset-upload-files-button/DatasetUploadFilesButton.tsx b/src/sections/dataset/dataset-files/dataset-upload-files-button/DatasetUploadFilesButton.tsx index db5c23a48..158910c5a 100644 --- a/src/sections/dataset/dataset-files/dataset-upload-files-button/DatasetUploadFilesButton.tsx +++ b/src/sections/dataset/dataset-files/dataset-upload-files-button/DatasetUploadFilesButton.tsx @@ -4,7 +4,11 @@ import { PlusLg } from 'react-bootstrap-icons' import { Button } from '@iqss/dataverse-design-system' import { useSession } from '../../../session/SessionContext' import { useDataset } from '../../DatasetContext' -import { Route } from '../../../Route.enum' +import { QueryParamKey, Route } from '../../../Route.enum' +import { + DatasetNonNumericVersionSearchParam, + DatasetPublishingStatus +} from '../../../../dataset/domain/models/Dataset' import styles from './DatasetUploadFilesButton.module.scss' export function DatasetUploadFilesButton() { @@ -18,7 +22,14 @@ export function DatasetUploadFilesButton() { } const handleClick = () => { - navigate(`${Route.UPLOAD_DATASET_FILES}?persistentId=${dataset.persistentId}`) + const searchParams = new URLSearchParams() + searchParams.set(QueryParamKey.PERSISTENT_ID, dataset.persistentId) + + if (dataset.version.publishingStatus === DatasetPublishingStatus.DRAFT) { + searchParams.set(QueryParamKey.VERSION, DatasetNonNumericVersionSearchParam.DRAFT) + } + + navigate(`${Route.UPLOAD_DATASET_FILES}?${searchParams.toString()}`) } return ( diff --git a/src/sections/dataset/dataset-files/files-table/file-info/file-info-cell/FileInfoCell.tsx b/src/sections/dataset/dataset-files/files-table/file-info/file-info-cell/FileInfoCell.tsx index c664a7070..29ec191ca 100644 --- a/src/sections/dataset/dataset-files/files-table/file-info/file-info-cell/FileInfoCell.tsx +++ b/src/sections/dataset/dataset-files/files-table/file-info/file-info-cell/FileInfoCell.tsx @@ -13,13 +13,17 @@ import { FileDescription } from './file-info-data/FileDescription' import { FileLabels } from '../../../../../file/file-labels/FileLabels' export function FileInfoCell({ file }: { file: FilePreview }) { + if (!file.datasetVersionNumber) { + console.log('FileInfoCell error: FilePreview object must contain datasetVersionNumber') + return null + } return (
- +
diff --git a/src/sections/dataset/dataset-files/files-table/file-info/file-info-cell/file-info-data/FileTitle.tsx b/src/sections/dataset/dataset-files/files-table/file-info/file-info-cell/file-info-data/FileTitle.tsx index 40d744eda..e1fcd8422 100644 --- a/src/sections/dataset/dataset-files/files-table/file-info/file-info-cell/file-info-data/FileTitle.tsx +++ b/src/sections/dataset/dataset-files/files-table/file-info/file-info-cell/file-info-data/FileTitle.tsx @@ -1,15 +1,20 @@ import { LinkToPage } from '../../../../../../shared/link-to-page/LinkToPage' import { Route } from '../../../../../../Route.enum' +import { DatasetVersionNumber } from '../../../../../../../dataset/domain/models/Dataset' import { DvObjectType } from '../../../../../../../shared/hierarchy/domain/models/UpwardHierarchyNode' interface FileTitleProps { id: number name: string + datasetVersionNumber: DatasetVersionNumber } -export function FileTitle({ id, name }: FileTitleProps) { +export function FileTitle({ id, name, datasetVersionNumber }: FileTitleProps) { return ( - + {name} ) diff --git a/src/sections/dataset/publish-dataset/PublishDatasetModal.tsx b/src/sections/dataset/publish-dataset/PublishDatasetModal.tsx index 60c18ae42..be695a836 100644 --- a/src/sections/dataset/publish-dataset/PublishDatasetModal.tsx +++ b/src/sections/dataset/publish-dataset/PublishDatasetModal.tsx @@ -8,15 +8,14 @@ import { useSession } from '../../session/SessionContext' import { License } from '../dataset-summary/License' import { DatasetNonNumericVersionSearchParam, - defaultLicense, - QueryParamsKeys + defaultLicense } from '../../../dataset/domain/models/Dataset' import { SubmissionStatus } from '../../shared/form/DatasetMetadataForm/useSubmitDataset' import { usePublishDataset } from './usePublishDataset' import { PublishDatasetHelpText } from './PublishDatasetHelpText' import styles from './PublishDatasetModal.module.scss' import { useNavigate } from 'react-router-dom' -import { Route } from '../../Route.enum' +import { QueryParamKey, Route } from '../../Route.enum' interface PublishDatasetModalProps { show: boolean @@ -52,7 +51,7 @@ export function PublishDatasetModal({ function onPublishSucceed() { navigate( - `${Route.DATASETS}?persistentId=${persistentId}&${QueryParamsKeys.VERSION}=${DatasetNonNumericVersionSearchParam.DRAFT}`, + `${Route.DATASETS}?${QueryParamKey.PERSISTENT_ID}=${persistentId}&${QueryParamKey.VERSION}=${DatasetNonNumericVersionSearchParam.DRAFT}`, { state: { publishInProgress: true }, replace: true diff --git a/src/sections/edit-dataset-metadata/EditDatasetMetadataFactory.tsx b/src/sections/edit-dataset-metadata/EditDatasetMetadataFactory.tsx index 82a90b8d1..8ee976e94 100644 --- a/src/sections/edit-dataset-metadata/EditDatasetMetadataFactory.tsx +++ b/src/sections/edit-dataset-metadata/EditDatasetMetadataFactory.tsx @@ -4,6 +4,7 @@ import { EditDatasetMetadata } from './EditDatasetMetadata' import { DatasetProvider } from '../dataset/DatasetProvider' import { DatasetJSDataverseRepository } from '../../dataset/infrastructure/repositories/DatasetJSDataverseRepository' import { MetadataBlockInfoJSDataverseRepository } from '../../metadata-block-info/infrastructure/repositories/MetadataBlockInfoJSDataverseRepository' +import { searchParamVersionToDomainVersion } from '../../Router' const datasetRepository = new DatasetJSDataverseRepository() const metadataBlockInfoRepository = new MetadataBlockInfoJSDataverseRepository() @@ -17,9 +18,13 @@ export class EditDatasetMetadataFactory { function EditDatasetMetadataWithParams() { const [searchParams] = useSearchParams() const persistentId = searchParams.get('persistentId') ?? undefined + const searchParamVersion = searchParams.get('version') ?? undefined + const version = searchParamVersionToDomainVersion(searchParamVersion) return ( - + void + upload: (file: File) => void cancelTitle: string info: string selectText: string @@ -30,21 +30,14 @@ export function FileUploader({ const addFiles = (selectedFiles: FileList | null) => { if (selectedFiles && selectedFiles.length > 0) { - setFiles((alreadyAdded) => { - const selectedFilesArray = Array.from(selectedFiles) - const selectedFilesSet = new Set(selectedFilesArray.map((x) => FileUploadTools.key(x))) - const alreadyAddedFiltered = alreadyAdded.filter( - /* istanbul ignore next */ - (x) => !selectedFilesSet.has(FileUploadTools.key(x)) - ) - return [...alreadyAddedFiltered, ...selectedFilesArray] - }) + Array.from(selectedFiles).forEach((file) => addFile(file)) } } const addFile = (file: File) => { if (!files.some((x) => FileUploadTools.key(x) === FileUploadTools.key(file))) { setFiles((oldFiles) => [...oldFiles, file]) + upload(file) } } @@ -106,15 +99,11 @@ export function FileUploader({ } useEffect(() => { - upload(files) - }, [files, upload]) - - useEffect(() => { - setFiles((newFiles) => - newFiles.filter((x) => { - const res = !FileUploadTools.get(x, fileUploaderState).removed + setFiles((currentFiles) => + currentFiles.filter((file) => { + const res = !FileUploadTools.get(file, fileUploaderState).removed if (!res) { - cleanFileState(x) + cleanFileState(file) } return res }) diff --git a/src/sections/upload-dataset-files/UploadDatasetFiles.tsx b/src/sections/upload-dataset-files/UploadDatasetFiles.tsx index 7a21e05ed..6846a8a1c 100644 --- a/src/sections/upload-dataset-files/UploadDatasetFiles.tsx +++ b/src/sections/upload-dataset-files/UploadDatasetFiles.tsx @@ -1,5 +1,8 @@ import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' +import { md5 } from 'js-md5' +import { Semaphore } from 'async-mutex' +import { useNavigate } from 'react-router-dom' import { FileRepository } from '../../files/domain/repositories/FileRepository' import { useLoading } from '../loading/LoadingContext' import { useDataset } from '../dataset/DatasetContext' @@ -9,7 +12,9 @@ import { FileUploader } from './FileUploader' import { FileUploadState, FileUploadTools } from '../../files/domain/models/FileUploadState' import { uploadFile } from '../../files/domain/useCases/uploadFile' import { UploadedFiles } from './uploaded-files-list/UploadedFiles' -import { addUploadedFile, addUploadedFiles } from '../../files/domain/useCases/addUploadedFiles' +import { addUploadedFiles } from '../../files/domain/useCases/addUploadedFiles' +import { Route } from '../Route.enum' +import { Stack } from '@iqss/dataverse-design-system' interface UploadDatasetFilesProps { fileRepository: FileRepository @@ -21,34 +26,39 @@ export const UploadDatasetFiles = ({ fileRepository: fileRepository }: UploadDat const { t } = useTranslation('uploadDatasetFiles') const [fileUploaderState, setState] = useState(FileUploadTools.createNewState([])) const [uploadingToCancelMap, setUploadingToCancelMap] = useState(new Map void>()) - const [semaphore, setSemaphore] = useState(new Set()) + const navigate = useNavigate() - const sleep = (delay: number) => new Promise((res) => setTimeout(res, delay)) const limit = 6 + const semaphore = new Semaphore(limit) - const acquireSemaphore = async (file: File) => { - const key = FileUploadTools.key(file) - setSemaphore((x) => (x.size >= limit ? x : x.add(key))) - while (!semaphore.has(key)) { - await sleep(500) - setSemaphore((x) => (x.size >= limit ? x : x.add(key))) - } - } - - const releaseSemaphore = (file: File) => { - setSemaphore((x) => { + const fileUploadFailed = (file: File) => { + setUploadingToCancelMap((x) => { x.delete(FileUploadTools.key(file)) return x }) + semaphore.release(1) } const fileUploadFinished = (file: File) => { - const key = FileUploadTools.key(file) - setUploadingToCancelMap((x) => { - x.delete(key) - return x - }) - releaseSemaphore(file) + const hash = md5.create() + const reader = file.stream().getReader() + reader + .read() + .then(async function updateHash({ done, value }) { + if (done) { + FileUploadTools.checksum(file, hash.hex(), fileUploaderState) + } else { + hash.update(value) + await updateHash(await reader.read()) + } + }) + .finally(() => { + setUploadingToCancelMap((x) => { + x.delete(FileUploadTools.key(file)) + return x + }) + semaphore.release(1) + }) } const canUpload = (file: File) => @@ -71,17 +81,10 @@ export const UploadDatasetFiles = ({ fileRepository: fileRepository }: UploadDat () => { setState(FileUploadTools.done(file, fileUploaderState)) fileUploadFinished(file) - addUploadedFile( - fileRepository, - dataset?.persistentId as string, - file, - FileUploadTools.get(file, fileUploaderState).storageId as string, - () => {} - ) }, () => { setState(FileUploadTools.failed(file, fileUploaderState)) - fileUploadFinished(file) + fileUploadFailed(file) }, (now) => setState(FileUploadTools.progress(file, now, fileUploaderState)), (storageId) => setState(FileUploadTools.storageId(file, storageId, fileUploaderState)) @@ -89,13 +92,9 @@ export const UploadDatasetFiles = ({ fileRepository: fileRepository }: UploadDat setUploadingToCancelMap((x) => x.set(key, cancel)) } - const upload = async (files: File[]) => { - for (const file of files) { - if (canUpload(file)) { - await acquireSemaphore(file) - uploadOneFile(file) - } - } + const upload = async (file: File) => { + await semaphore.acquire(1) + uploadOneFile(file) } const cleanup = (file: File) => { @@ -108,7 +107,6 @@ export const UploadDatasetFiles = ({ fileRepository: fileRepository }: UploadDat x.delete(key) return x }) - releaseSemaphore(file) } const cancelUpload = (file: File) => { @@ -141,8 +139,12 @@ export const UploadDatasetFiles = ({ fileRepository: fileRepository }: UploadDat const addFiles = (state: FileUploadState[]) => { setIsLoading(true) - const done = () => setIsLoading(false) - addUploadedFiles(fileRepository, dataset?.persistentId as string, state, done) + const done = () => { + setIsLoading(false) + navigate(`${Route.DATASETS}?persistentId=${dataset?.persistentId as string}&version=:draft`) + } + const uploadedFiles = FileUploadTools.mapToUploadedFilesDTOs(state) + addUploadedFiles(fileRepository, dataset?.persistentId as string, uploadedFiles, done) cleanAllState() } @@ -169,23 +171,25 @@ export const UploadDatasetFiles = ({ fileRepository: fileRepository }: UploadDat actionItemText={t('breadcrumbActionItem')} />
- - + + + +
)} diff --git a/src/sections/upload-dataset-files/UploadDatasetFilesFactory.tsx b/src/sections/upload-dataset-files/UploadDatasetFilesFactory.tsx index 20dd69293..ed2853273 100644 --- a/src/sections/upload-dataset-files/UploadDatasetFilesFactory.tsx +++ b/src/sections/upload-dataset-files/UploadDatasetFilesFactory.tsx @@ -4,6 +4,7 @@ import { DatasetJSDataverseRepository } from '../../dataset/infrastructure/repos import { FileJSDataverseRepository } from '../../files/infrastructure/FileJSDataverseRepository' import { DatasetProvider } from '../dataset/DatasetProvider' import { UploadDatasetFiles } from './UploadDatasetFiles' +import { searchParamVersionToDomainVersion } from '../../Router' const datasetRepository = new DatasetJSDataverseRepository() const fileRepository = new FileJSDataverseRepository() @@ -17,9 +18,13 @@ export class UploadDatasetFilesFactory { function UploadDatasetFilesWithSearchParams() { const [searchParams] = useSearchParams() const persistentId = searchParams.get('persistentId') ?? undefined + const searchParamVersion = searchParams.get('version') ?? undefined + const version = searchParamVersionToDomainVersion(searchParamVersion) return ( - + ) diff --git a/src/sections/upload-dataset-files/uploaded-files-list/UploadedFiles.tsx b/src/sections/upload-dataset-files/uploaded-files-list/UploadedFiles.tsx index afd54715d..0dc60a345 100644 --- a/src/sections/upload-dataset-files/uploaded-files-list/UploadedFiles.tsx +++ b/src/sections/upload-dataset-files/uploaded-files-list/UploadedFiles.tsx @@ -79,14 +79,13 @@ export function UploadedFiles({ updateFiles([file]) } const updateSelected = (file: FileUploadState) => { - setSelected((current) => { - if (current.has(file)) { - current.delete(file) - } else { - current.add(file) - } - return new Set(current) - }) + const newSelected = new Set(selected) + if (newSelected.has(file)) { + newSelected.delete(file) + } else { + newSelected.add(file) + } + setSelected(newSelected) } const save = () => { addFiles(fileUploadState) diff --git a/src/stories/collection/datasets-list/CollectionCard.stories.tsx b/src/stories/collection/datasets-list/CollectionCard.stories.tsx new file mode 100644 index 000000000..ddb26a65a --- /dev/null +++ b/src/stories/collection/datasets-list/CollectionCard.stories.tsx @@ -0,0 +1,37 @@ +import { Meta, StoryObj } from '@storybook/react' +import { WithI18next } from '../../WithI18next' +import { CollectionCard } from '../../../sections/collection/datasets-list/collection-card/CollectionCard' +import { CollectionPreviewMother } from '../../../../tests/component/collection/domain/models/CollectionPreviewMother' +import { FakerHelper } from '../../../../tests/component/shared/FakerHelper' + +const meta: Meta = { + title: 'Sections/Collection Page/CollectionCard', + component: CollectionCard, + decorators: [WithI18next] +} + +export default meta +type Story = StoryObj + +export const Default: Story = { + render: () => +} + +export const RequiredOnly: Story = { + render: () => ( + + ) +} +export const WithLongDescription: Story = { + render: () => { + const collectionPreview = CollectionPreviewMother.create({ + description: FakerHelper.paragraph(20) + }) + + return + } +} + +export const Unpublished: Story = { + render: () => +} diff --git a/src/stories/collection/datasets-list/FileCard.stories.tsx b/src/stories/collection/datasets-list/FileCard.stories.tsx index b66b2abe6..4c832cb30 100644 --- a/src/stories/collection/datasets-list/FileCard.stories.tsx +++ b/src/stories/collection/datasets-list/FileCard.stories.tsx @@ -2,8 +2,12 @@ import { Meta, StoryObj } from '@storybook/react' import { WithI18next } from '../../WithI18next' import { FileCard } from '../../../sections/collection/datasets-list/file-card/FileCard' import { FilePreviewMother } from '../../../../tests/component/files/domain/models/FilePreviewMother' -import { FileMetadataMother } from '../../../../tests/component/files/domain/models/FileMetadataMother' +import { + FileLabelMother, + FileMetadataMother +} from '../../../../tests/component/files/domain/models/FileMetadataMother' import { FakerHelper } from '../../../../tests/component/shared/FakerHelper' +import { DatasetPublishingStatus } from '../../../dataset/domain/models/Dataset' const meta: Meta = { title: 'Sections/Collection Page/FileCard', @@ -46,3 +50,16 @@ export const ReleasedWithDraft: Story = { ) } +export const WithAllLabels: Story = { + render: () => { + const filePreview = FilePreviewMother.createDefault({ + datasetPublishingStatus: DatasetPublishingStatus.DRAFT, + someDatasetVersionHasBeenReleased: false, + metadata: FileMetadataMother.createDefault({ + description: FakerHelper.paragraph(5), + labels: FileLabelMother.createMany(4) + }) + }) + return + } +} diff --git a/src/stories/file/FileMockRepository.ts b/src/stories/file/FileMockRepository.ts index 48e7064b9..9bc8aba55 100644 --- a/src/stories/file/FileMockRepository.ts +++ b/src/stories/file/FileMockRepository.ts @@ -13,7 +13,7 @@ import { FilePreview } from '../../files/domain/models/FilePreview' import { FakerHelper } from '../../../tests/component/shared/FakerHelper' import { FilesWithCount } from '../../files/domain/models/FilesWithCount' import { FileHolder } from '../../files/domain/models/FileHolder' -import { FileUploadState } from '../../files/domain/models/FileUploadState' +import { UploadedFileDTO } from '@iqss/dataverse-client-javascript' export class FileMockRepository implements FileRepository { constructor(public readonly fileMock?: File) {} @@ -107,19 +107,7 @@ export class FileMockRepository implements FileRepository { return res() } - addUploadedFiles(_datasetId: number | string, _files: FileUploadState[]): Promise { - return new Promise((resolve) => { - setTimeout(() => { - resolve() - }, FakerHelper.loadingTimout()) - }) - } - - addUploadedFile( - _datasetId: number | string, - _file: FileHolder, - _storageId: string - ): Promise { + addUploadedFiles(_datasetId: number | string, _files: UploadedFileDTO[]): Promise { return new Promise((resolve) => { setTimeout(() => { resolve() diff --git a/tests/component/collection/domain/models/CollectionPreviewMother.ts b/tests/component/collection/domain/models/CollectionPreviewMother.ts new file mode 100644 index 000000000..62e9509a3 --- /dev/null +++ b/tests/component/collection/domain/models/CollectionPreviewMother.ts @@ -0,0 +1,75 @@ +import { faker } from '@faker-js/faker' +import { FakerHelper } from '../../../shared/FakerHelper' +import { CollectionPreview } from '../../../../../src/collection/domain/models/CollectionPreview' + +export class CollectionPreviewMother { + static create(props?: Partial): CollectionPreview { + return { + id: faker.datatype.uuid(), + name: faker.lorem.words(3), + isReleased: faker.datatype.boolean(), + releaseOrCreateDate: faker.date.recent(), + parentCollectionId: faker.datatype.boolean() ? faker.datatype.uuid() : undefined, + parentCollectionName: faker.datatype.boolean() ? faker.lorem.words(3) : undefined, + description: faker.datatype.boolean() + ? `${faker.lorem.paragraph()} **${faker.lorem.sentence()}** ${faker.lorem.paragraph()}` + : undefined, + affiliation: faker.datatype.boolean() ? faker.lorem.words(3) : undefined, + ...props + } + } + + static createRealistic(): CollectionPreview { + return CollectionPreviewMother.create({ + id: 'science', + isReleased: true, + name: 'Scientific Research Collection', + releaseOrCreateDate: new Date('2021-01-01'), + parentCollectionId: 'parentId', + parentCollectionName: 'University Parent Collection', + description: 'We do all the science.', + affiliation: 'Scientific Research University' + }) + } + + static createWithOnlyRequiredFields(props?: Partial): CollectionPreview { + return CollectionPreviewMother.create({ + id: faker.datatype.uuid(), + name: FakerHelper.collectionName(), + isReleased: faker.datatype.boolean(), + affiliation: undefined, + description: undefined, + ...props + }) + } + + static createComplete(): CollectionPreview { + return CollectionPreviewMother.create({ + id: faker.datatype.uuid(), + isReleased: faker.datatype.boolean(), + name: FakerHelper.collectionName(), + parentCollectionId: faker.datatype.uuid(), + parentCollectionName: faker.lorem.words(3), + releaseOrCreateDate: FakerHelper.pastDate(), + description: FakerHelper.paragraph(), + affiliation: FakerHelper.affiliation() + }) + } + static createUnpublished(): CollectionPreview { + return CollectionPreviewMother.createWithOnlyRequiredFields({ + isReleased: false, + affiliation: FakerHelper.affiliation() + }) + } + static createWithDescription(): CollectionPreview { + return CollectionPreviewMother.createWithOnlyRequiredFields({ + description: FakerHelper.paragraph() + }) + } + + static createWithAffiliation(): CollectionPreview { + return CollectionPreviewMother.createWithOnlyRequiredFields({ + affiliation: FakerHelper.affiliation() + }) + } +} diff --git a/tests/component/files/domain/models/FilePreviewMother.ts b/tests/component/files/domain/models/FilePreviewMother.ts index 8aa610584..ec68a476f 100644 --- a/tests/component/files/domain/models/FilePreviewMother.ts +++ b/tests/component/files/domain/models/FilePreviewMother.ts @@ -17,6 +17,7 @@ export class FilePreviewMother { ingest: FileIngestMother.create(), metadata: FileMetadataMother.create(), permissions: FilePermissionsMother.create(), + datasetVersionNumber: DatasetVersionMother.create().number, ...props } } @@ -33,6 +34,7 @@ export class FilePreviewMother { ingest: FileIngestMother.createIngestNone(), metadata: FileMetadataMother.createDefault(), permissions: FilePermissionsMother.createWithGrantedPermissions(), + datasetVersionNumber: DatasetVersionMother.create().number, datasetName: 'Dataset Name', someDatasetVersionHasBeenReleased: true, ...props diff --git a/tests/component/sections/collection/datasets-list/collection-card/CollectionCard.spec.tsx b/tests/component/sections/collection/datasets-list/collection-card/CollectionCard.spec.tsx new file mode 100644 index 000000000..839866f0b --- /dev/null +++ b/tests/component/sections/collection/datasets-list/collection-card/CollectionCard.spec.tsx @@ -0,0 +1,18 @@ +import { DateHelper } from '../../../../../../src/shared/helpers/DateHelper' +import { CollectionPreviewMother } from '../../../../collection/domain/models/CollectionPreviewMother' +import { CollectionCard } from '../../../../../../src/sections/collection/datasets-list/collection-card/CollectionCard' + +describe('CollectionCard', () => { + it('should render the card', () => { + const collectionPreview = CollectionPreviewMother.createRealistic() + + cy.customMount() + + cy.contains(DateHelper.toDisplayFormat(collectionPreview.releaseOrCreateDate)).should('exist') + collectionPreview.description && cy.findByText(collectionPreview.description).should('exist') + collectionPreview.parentCollectionName && + cy.findByText(collectionPreview.parentCollectionName).should('exist') + collectionPreview.affiliation && cy.contains(collectionPreview.affiliation).should('exist') + collectionPreview.name && cy.contains(collectionPreview.name).should('exist') + }) +}) 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 64c20869b..00f2dd5c1 100644 --- a/tests/component/sections/dataset/dataset-action-buttons/DatasetActionButtons.spec.tsx +++ b/tests/component/sections/dataset/dataset-action-buttons/DatasetActionButtons.spec.tsx @@ -1,18 +1,28 @@ +import { DatasetRepository } from '../../../../../src/dataset/domain/repositories/DatasetRepository' +import { FileSizeUnit } from '../../../../../src/files/domain/models/FileMetadata' import { DatasetActionButtons } from '../../../../../src/sections/dataset/dataset-action-buttons/DatasetActionButtons' import { + DatasetFileDownloadSizeMother, DatasetMother, DatasetPermissionsMother, DatasetVersionMother } from '../../../dataset/domain/models/DatasetMother' +const datasetRepository: DatasetRepository = {} as DatasetRepository + describe('DatasetActionButtons', () => { it('renders the DatasetActionButtons with the Publish button', () => { const dataset = DatasetMother.create({ version: DatasetVersionMother.createDraftAsLatestVersionWithSomeVersionHasBeenReleased(), - permissions: DatasetPermissionsMother.createWithAllAllowed() + permissions: DatasetPermissionsMother.createWithAllAllowed(), + fileDownloadSizes: [ + DatasetFileDownloadSizeMother.createOriginal({ value: 2000, unit: FileSizeUnit.BYTES }) + ] }) - cy.mountAuthenticated() + cy.mountAuthenticated( + + ) cy.findByRole('group', { name: 'Dataset Action Buttons' }).should('exist') cy.findByRole('button', { name: 'Access Dataset' }).should('exist') @@ -28,10 +38,15 @@ describe('DatasetActionButtons', () => { canDownloadFiles: true, canUpdateDataset: true, canPublishDataset: false - }) + }), + fileDownloadSizes: [ + DatasetFileDownloadSizeMother.createOriginal({ value: 2000, unit: FileSizeUnit.BYTES }) + ] }) - cy.mountAuthenticated() + cy.mountAuthenticated( + + ) cy.findByRole('group', { name: 'Dataset Action Buttons' }).should('exist') cy.findByRole('button', { name: 'Access Dataset' }).should('exist') 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 501e51966..9cc568f9e 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 @@ -139,4 +139,26 @@ describe('AccessDatasetMenu', () => { .should('exist') .should('have.attr', 'href', downloadUrls.archival) }) + + it('does not render the AccessDatasetMenu if the file download sizes are zero', () => { + const version = DatasetVersionMother.createReleased() + const permissions = DatasetPermissionsMother.createWithFilesDownloadAllowed() + const fileDownloadSizes = [ + DatasetFileDownloadSizeMother.createOriginal({ value: 0, unit: FileSizeUnit.BYTES }), + DatasetFileDownloadSizeMother.createArchival({ + value: 0, + unit: FileSizeUnit.BYTES + }) + ] + cy.customMount( + + ) + cy.findByRole('button', { name: 'Access Dataset' }).should('not.exist') + }) }) diff --git a/tests/component/sections/dataset/dataset-files/DatasetFilesScrollable.spec.tsx b/tests/component/sections/dataset/dataset-files/DatasetFilesScrollable.spec.tsx index 674c70782..972aeae2c 100644 --- a/tests/component/sections/dataset/dataset-files/DatasetFilesScrollable.spec.tsx +++ b/tests/component/sections/dataset/dataset-files/DatasetFilesScrollable.spec.tsx @@ -101,7 +101,9 @@ describe('DatasetFilesScrollable', () => { cy.findByText('10 of 200 Files displayed').should('exist') first10Files.forEach((file) => { - cy.findByText(file.name).should('exist').should('have.attr', 'href', `/files?id=${file.id}`) + cy.findByText(file.name) + .should('exist') + .should('have.attr', 'href', `/files?id=${file.id}&datasetVersion=1.0`) }) }) @@ -135,7 +137,9 @@ describe('DatasetFilesScrollable', () => { cy.findByText(`${ONLY_4_FILES_COUNT} Files`).should('exist') only4Files.files.forEach((file) => { - cy.findByText(file.name).should('exist').should('have.attr', 'href', `/files?id=${file.id}`) + cy.findByText(file.name) + .should('exist') + .should('have.attr', 'href', `/files?id=${file.id}&datasetVersion=1.0`) }) cy.findByTestId('table-row-loading-skeleton').should('not.exist') }) diff --git a/tests/component/sections/dataset/dataset-files/files-table/files-info/file-info-cell/file-info-data/FileTitle.spec.tsx b/tests/component/sections/dataset/dataset-files/files-table/files-info/file-info-cell/file-info-data/FileTitle.spec.tsx index 0db129a0d..f669996d8 100644 --- a/tests/component/sections/dataset/dataset-files/files-table/files-info/file-info-cell/file-info-data/FileTitle.spec.tsx +++ b/tests/component/sections/dataset/dataset-files/files-table/files-info/file-info-cell/file-info-data/FileTitle.spec.tsx @@ -1,12 +1,19 @@ import { FileTitle } from '../../../../../../../../../src/sections/dataset/dataset-files/files-table/file-info/file-info-cell/file-info-data/FileTitle' +import { DatasetVersionMother } from '../../../../../../../dataset/domain/models/DatasetMother' describe('FileTitle', () => { it('renders the link and name correctly', () => { const id = 12345 const name = 'file-name.txt' - cy.customMount() + cy.customMount( + + ) - cy.findByRole('link', { name: name }).should('have.attr', 'href', `/files?id=${id}`) + cy.findByRole('link', { name: name }).should( + 'have.attr', + 'href', + `/files?id=${id}&datasetVersion=${DatasetVersionMother.create().number.toSearchParam()}` + ) }) }) diff --git a/tests/component/sections/upload-dataset-files/UploadDatasetFiles.spec.tsx b/tests/component/sections/upload-dataset-files/UploadDatasetFiles.spec.tsx index 7de43e55a..5a30bc95d 100644 --- a/tests/component/sections/upload-dataset-files/UploadDatasetFiles.spec.tsx +++ b/tests/component/sections/upload-dataset-files/UploadDatasetFiles.spec.tsx @@ -217,58 +217,6 @@ describe('UploadDatasetFiles', () => { cy.findByText('Select files to add').should('exist') }) - it('prevents more than 6 simultaneous uploads', () => { - const testDataset = DatasetMother.create() - - mountWithDataset(, testDataset) - - cy.findByTestId('drag-and-drop').as('dnd') - cy.get('@dnd').should('exist') - - const filenames: string[] = [ - 'users1.json', - 'users2.json', - 'users3.json', - 'users4.json', - 'users5.json', - 'users6.json', - 'users7.json', - 'users8.json', - 'users9.json', - 'users10.json' - ] - filenames.forEach((element) => { - cy.get('@dnd').selectFile( - { fileName: element, contents: [{ name: 'John Doe' }] }, - { action: 'drag-drop' } - ) - }) - cy.findAllByTitle('Cancel upload').should('have.length', 10) - cy.findAllByRole('progressbar').should('have.length', 6) - cy.findByText('Select files to add').should('exist') - const filenames2: string[] = [ - 'users11.json', - 'users12.json', - 'users13.json', - 'users14.json', - 'users15.json', - 'users16.json', - 'users17.json', - 'users18.json', - 'users19.json', - 'users20.json' - ] - filenames2.forEach((element) => { - cy.get('@dnd').selectFile( - { fileName: element, contents: [{ name: 'John Doe' }] }, - { action: 'drag-drop' } - ) - }) - cy.findByText('users20.json').should('exist') - cy.findAllByRole('progressbar').should('have.length', 6) - cy.findByText('Select files to add').should('exist') - }) - it('saves uploaded files', () => { const testDataset = DatasetMother.create() diff --git a/tests/e2e-integration/integration/files/FileUpload.spec.ts b/tests/e2e-integration/integration/files/FileUpload.spec.ts new file mode 100644 index 000000000..64b2ed688 --- /dev/null +++ b/tests/e2e-integration/integration/files/FileUpload.spec.ts @@ -0,0 +1,186 @@ +import { TestsUtils } from '../../shared/TestsUtils' +import { FileJSDataverseRepository } from '../../../../src/files/infrastructure/FileJSDataverseRepository' +import { DatasetJSDataverseRepository } from '../../../../src/dataset/infrastructure/repositories/DatasetJSDataverseRepository' +import { DatasetHelper } from '../../shared/datasets/DatasetHelper' +import { FileHelper } from '../../shared/files/FileHelper' +import { DatasetNonNumericVersion } from '../../../../src/dataset/domain/models/Dataset' +import chaiAsPromised from 'chai-as-promised' + +chai.use(chaiAsPromised) +const expect = chai.expect + +const fileRepository = new FileJSDataverseRepository() +const datasetRepository = new DatasetJSDataverseRepository() + +describe('DirectUpload', () => { + before(() => { + TestsUtils.setup() + }) + + beforeEach(() => { + TestsUtils.login() + }) + + it('should upload file and add it to the dataset', async () => { + const dataset = await DatasetHelper.create().then((datasetResponse) => + datasetRepository.getByPersistentId( + datasetResponse.persistentId, + DatasetNonNumericVersion.DRAFT + ) + ) + if (!dataset) throw new Error('Dataset not found') + + const singlePartFile = FileHelper.createSinglePartFileBlob() + const controller = new AbortController() + let storageId: string | undefined = undefined + + await fileRepository.uploadFile( + dataset.persistentId, + { file: singlePartFile }, + () => {}, + controller, + (sId) => { + storageId = sId + } + ) + + expect(storageId).to.be.not.undefined + if (storageId == undefined) { + throw new Error('storageId is undefined') + } + + await fileRepository.addUploadedFiles(dataset.persistentId, [ + { + fileName: 'test.json', + description: 'description text', + directoryLabel: '', + categories: ['tag'], + restrict: false, + storageId: storageId, + checksumValue: 'abc123', + checksumType: 'md5', + mimeType: 'application/json' + } + ]) + + const files = await fileRepository.getAllByDatasetPersistentId( + dataset.persistentId, + dataset.version + ) + expect(files).to.be.not.empty + expect(files[0].name).to.be.equal('test.json') + + await DatasetHelper.destroy(dataset.persistentId) + }) + + it('should upload 2 files and add it to the dataset', async () => { + const dataset = await DatasetHelper.create().then((datasetResponse) => + datasetRepository.getByPersistentId( + datasetResponse.persistentId, + DatasetNonNumericVersion.DRAFT + ) + ) + if (!dataset) throw new Error('Dataset not found') + + const singlePartFile1 = FileHelper.createSinglePartFileBlob() + const singlePartFile2 = FileHelper.createSinglePartFileBlob() + let storageId1: string | undefined = undefined + let storageId2: string | undefined = undefined + + const upload1 = fileRepository.uploadFile( + dataset.persistentId, + { file: singlePartFile1 }, + () => {}, + new AbortController(), + (sId) => { + storageId1 = sId + } + ) + + const upload2 = fileRepository.uploadFile( + dataset.persistentId, + { file: singlePartFile2 }, + () => {}, + new AbortController(), + (sId) => { + storageId2 = sId + } + ) + + await upload1 + await upload2 + + expect(storageId1).to.be.not.undefined + expect(storageId2).to.be.not.undefined + if (storageId1 == undefined) { + throw new Error('storageId1 is undefined') + } + if (storageId2 == undefined) { + throw new Error('storageId2 is undefined') + } + + await fileRepository.addUploadedFiles(dataset.persistentId, [ + { + fileName: 'test1.json', + description: 'description text', + directoryLabel: '', + categories: ['tag'], + restrict: false, + storageId: storageId1, + checksumValue: 'abc123', + checksumType: 'md5', + mimeType: 'application/json' + }, + { + fileName: 'test2.json', + description: 'description text', + directoryLabel: '', + categories: ['tag'], + restrict: false, + storageId: storageId2, + checksumValue: 'def456', + checksumType: 'md5', + mimeType: 'application/json' + } + ]) + + const files = await fileRepository.getAllByDatasetPersistentId( + dataset.persistentId, + dataset.version + ) + expect(files).to.be.not.empty + expect(files[0].name).to.be.equal('test1.json') + expect(files[1].name).to.be.equal('test2.json') + + await DatasetHelper.destroy(dataset.persistentId) + }) + + it('should not finish uploading file to destinations when user cancels immediately', async () => { + const dataset = await DatasetHelper.create().then((datasetResponse) => + datasetRepository.getByPersistentId( + datasetResponse.persistentId, + DatasetNonNumericVersion.DRAFT + ) + ) + if (!dataset) throw new Error('Dataset not found') + + const multipartFile = FileHelper.createMultipartFileBlob() + const controller = new AbortController() + + const upload = fileRepository.uploadFile( + dataset.persistentId, + { file: multipartFile }, + () => {}, + controller, + () => {} + ) + controller.abort() + await upload + .then(() => { + throw new Error('upload succeeded') + }) + .catch(() => {}) + + await DatasetHelper.destroy(dataset.persistentId) + }) +}) diff --git a/tests/e2e-integration/shared/files/FileHelper.ts b/tests/e2e-integration/shared/files/FileHelper.ts index fbf783dbb..a36958217 100644 --- a/tests/e2e-integration/shared/files/FileHelper.ts +++ b/tests/e2e-integration/shared/files/FileHelper.ts @@ -156,4 +156,30 @@ export class FileHelper extends DataverseApiHelper { static async delete(id: number) { return this.request(`/files/${id}`, 'DELETE') } + + static createSinglePartFileBlob(): File { + try { + return FileHelper.createFileBlobWithSize(1000, 'singlepart-file') + } catch (error) { + throw new Error(`Error while creating test singlepart file`) + } + } + + static createMultipartFileBlob(): File { + try { + return FileHelper.createFileBlobWithSize(1273741824, 'multipart-file') + } catch (error) { + throw new Error(`Error while creating test multipart file`) + } + } + + private static createFileBlobWithSize(fileSizeInBytes: number, fileName: string): File { + const blob = FileHelper.createBlobWithSize(fileSizeInBytes) + return new File([blob], fileName, { type: 'text/plain' }) + } + + private static createBlobWithSize(size: number): Blob { + const arrayBuffer = new ArrayBuffer(size) + return new Blob([arrayBuffer]) + } }