diff --git a/.eslintrc.json b/.eslintrc.json index 3177f5517..dbdefa329 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -42,9 +42,19 @@ "unused-imports/no-unused-imports": "error", "unused-imports/no-unused-vars": [ "warn", - { "vars": "all", "varsIgnorePattern": "^_", "args": "after-used", "argsIgnorePattern": "^_" } + { + "vars": "all", + "varsIgnorePattern": "^_", + "args": "after-used", + "argsIgnorePattern": "^_" + } + ], + "@typescript-eslint/no-empty-function": [ + "error", + { + "allow": ["arrowFunctions"] + } ], - "@typescript-eslint/no-empty-function": ["error", { "allow": ["arrowFunctions"] }], "react/react-in-jsx-scope": "off", "prettier/prettier": [ "error", diff --git a/README.md b/README.md index 3be0907cd..c6e758b18 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,45 @@ First install node >=16 and npm >=8. Recommended versions `node v19` and `npm v9`. +### Create a `.npmrc` file and add a token + +To install the [@iqss/dataverse-client-javascript](https://github.com/IQSS/dataverse-client-javascript/pkgs/npm/dataverse-client-javascript) +from the GitHub registry, necessary for connecting with the Dataverse API, follow these steps to create an `.npmrc` file in +the root of your project using your GitHub token. + +1. **Copy `.npmrc.example`** + + Duplicate the `.npmrc.example` file in your project and save it as `.npmrc`. + +2. **Replace the Token** + + Open the newly created `.npmrc` file and replace `YOUR_GITHUB_TOKEN` with your actual GitHub token. + + ```plaintext + legacy-peer-deps=true + + //npm.pkg.github.com/:_authToken= + @iqss:registry=https://npm.pkg.github.com/ + ``` + +#### How to Get a GitHub Token + +If you don't have a GitHub token yet, follow these steps: + +1. Go to your GitHub account settings. + +2. Navigate to "Developer settings" -> "Personal access tokens." + +3. Click "Personal access tokens" -> "Tokens (classic)" -> "Generate new token (classic)". + +4. Give the token a name and select the "read:packages" scope. + +5. Copy the generated token. + +6. Replace `YOUR_GITHUB_AUTH_TOKEN` in the `.npmrc` file with the copied token. + +Now, you should be able to install the Dataverse JavaScript client using npm. + ### `npm install` Run this command to install the dependencies. You may see a message about vulnerabilities after running this command. \ diff --git a/dev.Dockerfile b/dev.Dockerfile index 5e474faf8..fb0b8ee2e 100644 --- a/dev.Dockerfile +++ b/dev.Dockerfile @@ -10,6 +10,7 @@ RUN npm run build WORKDIR /usr/src/app COPY package.json ./ +COPY package-lock.json ./ COPY .npmrc ./ RUN npm install diff --git a/package-lock.json b/package-lock.json index 9cab908e7..a73e91aca 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-pr97.418bf5e", + "@iqss/dataverse-client-javascript": "2.0.0-pr99.c36f1db", "@iqss/dataverse-design-system": "*", "@istanbuljs/nyc-config-typescript": "1.0.2", "@tanstack/react-table": "8.9.2", @@ -24,6 +24,7 @@ "i18next": "22.4.9", "i18next-browser-languagedetector": "7.0.1", "i18next-http-backend": "2.1.1", + "moment-timezone": "0.5.43", "react-bootstrap": "2.7.2", "react-bootstrap-icons": "1.10.3", "react-i18next": "12.1.5", @@ -3588,9 +3589,9 @@ }, "node_modules/@iqss/dataverse-client-javascript": { "name": "@IQSS/dataverse-client-javascript", - "version": "2.0.0-pr97.418bf5e", - "resolved": "https://npm.pkg.github.com/download/@IQSS/dataverse-client-javascript/2.0.0-pr97.418bf5e/0d604232d6f567b41328143c2d5d27513d069380", - "integrity": "sha512-wKRR1ORFkFSoFoCrWsCQvcm1vExIIrVN3pJbIl9Nk8lopP7zkUEB1xcAU+UVShbGktIZIa20TQMFl0ONafDiZw==", + "version": "2.0.0-pr99.c36f1db", + "resolved": "https://npm.pkg.github.com/download/@IQSS/dataverse-client-javascript/2.0.0-pr99.c36f1db/3f49037b14e53295c39ce787cce53f20b2558ba6", + "integrity": "sha512-KzMVzB420eKKaOuwDEpvAB/k1RrW3Le/ZJcVtjxFk/Wvxov2Jl1npbwy4SXQWasEXaJWslohn2KRkBfBDoTHTQ==", "license": "MIT", "dependencies": { "@types/node": "^18.15.11", @@ -31965,6 +31966,25 @@ "integrity": "sha512-23g5BFj4zdQL/b6tor7Ji+QY4pEfNH784BMslY9Qb0UnJWRAt+lQGLYmRaM0KDBwIG23ffEBELhZDP2rhi9f/Q==", "dev": true }, + "node_modules/moment": { + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "engines": { + "node": "*" + } + }, + "node_modules/moment-timezone": { + "version": "0.5.43", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.43.tgz", + "integrity": "sha512-72j3aNyuIsDxdF1i7CEgV2FfxM1r6aaqJyLB2vwb33mXYyoyLly+F1zbWqhA3/bVIoJ4szlUoMbUnVdid32NUQ==", + "dependencies": { + "moment": "^2.29.4" + }, + "engines": { + "node": "*" + } + }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", diff --git a/package.json b/package.json index 0c61160a5..af7cf0bdb 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-pr97.418bf5e", + "@iqss/dataverse-client-javascript": "2.0.0-pr99.c36f1db", "@iqss/dataverse-design-system": "*", "@istanbuljs/nyc-config-typescript": "1.0.2", "@tanstack/react-table": "8.9.2", @@ -28,6 +28,7 @@ "i18next": "22.4.9", "i18next-browser-languagedetector": "7.0.1", "i18next-http-backend": "2.1.1", + "moment-timezone": "0.5.43", "react-bootstrap": "2.7.2", "react-bootstrap-icons": "1.10.3", "react-i18next": "12.1.5", diff --git a/public/locales/en/dataset.json b/public/locales/en/dataset.json index 916ab30b4..fbc62fefb 100644 --- a/public/locales/en/dataset.json +++ b/public/locales/en/dataset.json @@ -58,6 +58,30 @@ "uploadFiles": "Upload Files" }, "alerts": { + "publishInProgress": { + "heading": "Publish in Progress", + "alertText": "The dataset is locked while the persistent identifiers are being registered or updated, and/or the physical files are being validated." + }, + "filesUpdated": { + "heading": "Success!", + "alertText": "One or more files have been updated." + }, + "termsUpdated": { + "heading": "Success!", + "alertText": "The terms for this dataset have been updated." + }, + "thumbnailUpdated": { + "heading": "Success!", + "alertText": "Dataset thumbnail updated." + }, + "datasetDeleted": { + "heading": "Success!", + "alertText": "This dataset draft has been deleted." + }, + "metadataUpdated": { + "heading": "Success!", + "alertText": "The metadata for this dataset has been updated." + }, "draftVersion": { "heading": "This draft version needs to be published", "alertText": "When ready for sharing, please publish it so that others can see these changes" diff --git a/src/alert/domain/models/Alert.ts b/src/alert/domain/models/Alert.ts new file mode 100644 index 000000000..d48051847 --- /dev/null +++ b/src/alert/domain/models/Alert.ts @@ -0,0 +1,23 @@ +import { AlertVariant } from '@iqss/dataverse-design-system/dist/components/alert/AlertVariant' + +export enum AlertMessageKey { + DRAFT_VERSION = 'draftVersion', + REQUESTED_VERSION_NOT_FOUND = 'requestedVersionNotFound', + REQUESTED_VERSION_NOT_FOUND_SHOW_DRAFT = 'requestedVersionNotFoundShowDraft', + SHARE_UNPUBLISHED_DATASET = 'shareUnpublishedDataset', + UNPUBLISHED_DATASET = 'unpublishedDataset', + METADATA_UPDATED = 'metadataUpdated', + FILES_UPDATED = 'filesUpdated', + TERMS_UPDATED = 'termsUpdated', + THUMBNAIL_UPDATED = 'thumbnailUpdated', + DATASET_DELETED = 'datasetDeleted', + PUBLISH_IN_PROGRESS = 'publishInProgress' +} + +export class Alert { + constructor( + public readonly variant: AlertVariant, + public readonly messageKey: AlertMessageKey, + public dynamicFields?: object + ) {} +} diff --git a/src/dataset/domain/models/Dataset.ts b/src/dataset/domain/models/Dataset.ts index 26fe11ed2..0eb6fe534 100644 --- a/src/dataset/domain/models/Dataset.ts +++ b/src/dataset/domain/models/Dataset.ts @@ -1,4 +1,4 @@ -import { AlertVariant } from '@iqss/dataverse-design-system/dist/components/alert/AlertVariant' +import { Alert, AlertMessageKey } from '../../../alert/domain/models/Alert' export enum DatasetLabelSemanticMeaning { DATASET = 'dataset', @@ -24,22 +24,6 @@ export class DatasetLabel { ) {} } -export enum DatasetAlertMessageKey { - DRAFT_VERSION = 'draftVersion', - REQUESTED_VERSION_NOT_FOUND = 'requestedVersionNotFound', - REQUESTED_VERSION_NOT_FOUND_SHOW_DRAFT = 'requestedVersionNotFoundShowDraft', - SHARE_UNPUBLISHED_DATASET = 'shareUnpublishedDataset', - UNPUBLISHED_DATASET = 'unpublishedDataset' -} - -export class DatasetAlert { - constructor( - public readonly variant: AlertVariant, - public readonly message: DatasetAlertMessageKey, - public readonly dynamicFields?: object - ) {} -} - export enum MetadataBlockName { CITATION = 'citation', GEOSPATIAL = 'geospatial', @@ -259,20 +243,19 @@ export interface DatasetPermissions { } export interface DatasetLock { - id: number + userPersistentId: string reason: DatasetLockReason } export enum DatasetLockReason { - INGEST = 'ingest', - WORKFLOW = 'workflow', - IN_REVIEW = 'inReview', - DCM_UPLOAD = 'dcmUpload', - GLOBUS_UPLOAD = 'globusUpload', + INGEST = 'Ingest', + WORKFLOW = 'Workflow', + IN_REVIEW = 'InReview', + DCM_UPLOAD = 'DcmUpload', + GLOBUS_UPLOAD = 'GlobusUpload', FINALIZE_PUBLICATION = 'finalizePublication', - - EDIT_IN_PROGRESS = 'editInProgress', - FILE_VALIDATION_FAILED = 'fileValidationFailed' + EDIT_IN_PROGRESS = 'EditInProgress', + FILE_VALIDATION_FAILED = 'FileValidationFailed' } export interface PrivateUrl { @@ -286,7 +269,7 @@ export class Dataset { public readonly version: DatasetVersion, public readonly citation: string, public readonly labels: DatasetLabel[], - public readonly alerts: DatasetAlert[], + public readonly alerts: Alert[], public readonly summaryFields: DatasetMetadataBlock[], public readonly license: DatasetLicense, public readonly metadataBlocks: DatasetMetadataBlocks, @@ -303,8 +286,8 @@ export class Dataset { return this.metadataBlocks[0].fields.title } - public get isLockedFromPublishing(): boolean { - return this.isLockedFromEdits + public checkIsLockedFromPublishing(userPersistentId: string): boolean { + return this.checkIsLockedFromEdits(userPersistentId) } public get isLocked(): boolean { @@ -315,12 +298,19 @@ export class Dataset { return this.locks.some((lock) => lock.reason === DatasetLockReason.WORKFLOW) } - public get isLockedFromEdits(): boolean { + public checkIsLockedFromEdits(userPersistentId: string): boolean { const lockedReasonIsInReview = this.locks.some( (lock) => lock.reason === DatasetLockReason.IN_REVIEW ) - // If the lock reason is workflow and the workflow userId is the same as the current user, then the user can edit - // TODO - Ask how we want to manage pending workflows + + if ( + this.locks.some( + (lock) => + lock.reason === DatasetLockReason.WORKFLOW && lock.userPersistentId === userPersistentId + ) + ) { + return false + } return this.isLocked && !(lockedReasonIsInReview && this.permissions.canPublishDataset) } @@ -357,7 +347,7 @@ export class Dataset { static Builder = class { public readonly labels: DatasetLabel[] = [] - public readonly alerts: DatasetAlert[] = [] + public readonly alerts: Alert[] = [] constructor( public readonly persistentId: string, @@ -428,7 +418,7 @@ export class Dataset { this.version.publishingStatus === DatasetPublishingStatus.DRAFT && this.permissions.canPublishDataset ) { - this.alerts.push(new DatasetAlert('warning', DatasetAlertMessageKey.DRAFT_VERSION)) + this.alerts.push(new Alert('warning', AlertMessageKey.DRAFT_VERSION)) } if (this.version.requestedVersion) { if (this.version.latestVersionStatus == DatasetPublishingStatus.RELEASED) { @@ -437,20 +427,16 @@ export class Dataset { returnedVersion: `${this.version.toString()}` } this.alerts.push( - new DatasetAlert( - 'warning', - DatasetAlertMessageKey.REQUESTED_VERSION_NOT_FOUND, - dynamicFields - ) + new Alert('warning', AlertMessageKey.REQUESTED_VERSION_NOT_FOUND, dynamicFields) ) } else { const dynamicFields = { requestedVersion: this.version.requestedVersion } this.alerts.push( - new DatasetAlert( + new Alert( 'warning', - DatasetAlertMessageKey.REQUESTED_VERSION_NOT_FOUND_SHOW_DRAFT, + AlertMessageKey.REQUESTED_VERSION_NOT_FOUND_SHOW_DRAFT, dynamicFields ) ) @@ -460,14 +446,10 @@ export class Dataset { if (this.permissions.canPublishDataset) { const dynamicFields = { privateUrl: this.privateUrl.urlSnippet + this.privateUrl.token } this.alerts.push( - new DatasetAlert( - 'info', - DatasetAlertMessageKey.SHARE_UNPUBLISHED_DATASET, - dynamicFields - ) + new Alert('info', AlertMessageKey.SHARE_UNPUBLISHED_DATASET, dynamicFields) ) } else { - this.alerts.push(new DatasetAlert('warning', DatasetAlertMessageKey.UNPUBLISHED_DATASET)) + this.alerts.push(new Alert('warning', AlertMessageKey.UNPUBLISHED_DATASET)) } } } diff --git a/src/dataset/infrastructure/mappers/JSDatasetMapper.ts b/src/dataset/infrastructure/mappers/JSDatasetMapper.ts index 77a8c3031..be6f7ce1c 100644 --- a/src/dataset/infrastructure/mappers/JSDatasetMapper.ts +++ b/src/dataset/infrastructure/mappers/JSDatasetMapper.ts @@ -3,7 +3,9 @@ import { DatasetMetadataBlock as JSDatasetMetadataBlock, DatasetMetadataBlocks as JSDatasetMetadataBlocks, DatasetMetadataFields as JSDatasetMetadataFields, - DatasetVersionInfo as JSDatasetVersionInfo + DatasetVersionInfo as JSDatasetVersionInfo, + DatasetUserPermissions as JSDatasetPermissions, + DatasetLock as JSDatasetLock } from '@iqss/dataverse-client-javascript' import { DatasetVersionState as JSDatasetVersionState } from '@iqss/dataverse-client-javascript/dist/datasets/domain/models/Dataset' import { @@ -14,6 +16,9 @@ import { DatasetMetadataFields, DatasetVersion, MetadataBlockName, + DatasetPermissions, + DatasetLock, + DatasetLockReason, PrivateUrl } from '../../domain/models/Dataset' @@ -22,6 +27,8 @@ export class JSDatasetMapper { jsDataset: JSDataset, citation: string, summaryFieldsNames: string[], + jsDatasetPermissions: JSDatasetPermissions, + jsDatasetLocks: JSDatasetLock[], requestedVersion?: string, privateUrl?: PrivateUrl ): Dataset { @@ -37,19 +44,11 @@ export class JSDatasetMapper { jsDataset.publicationDate, jsDataset.citationDate ), - { - canDownloadFiles: true, - canUpdateDataset: true, - canPublishDataset: true, - canManageDatasetPermissions: true, - canManageFilesPermissions: true, - canDeleteDataset: true - }, // TODO Connect with dataset permissions - [], // TODO Connect with dataset locks + JSDatasetMapper.toDatasetPermissions(jsDatasetPermissions), + JSDatasetMapper.toLocks(jsDatasetLocks), true, // TODO Connect with dataset hasValidTermsOfAccess true, // TODO Connect with dataset isValid - jsDataset.versionInfo.releaseTime !== undefined && - !isNaN(jsDataset.versionInfo.releaseTime.getTime()), // TODO Connect with dataset isReleased, + JSDatasetMapper.toIsReleased(jsDataset.versionInfo), undefined, // TODO: get dataset thumbnail from Dataverse https://github.com/IQSS/dataverse-frontend/issues/203 privateUrl ).build() @@ -185,4 +184,30 @@ export class JSDatasetMapper { return extraFields } + + static toIsReleased(jsDatasetVersionInfo: JSDatasetVersionInfo): boolean { + return ( + jsDatasetVersionInfo.releaseTime !== undefined && + !isNaN(jsDatasetVersionInfo.releaseTime.getTime()) + ) + } + + static toDatasetPermissions(jsDatasetPermissions: JSDatasetPermissions): DatasetPermissions { + return { + canDownloadFiles: true, // TODO: connect with js-dataverse + canUpdateDataset: jsDatasetPermissions.canEditDataset, + canPublishDataset: jsDatasetPermissions.canPublishDataset, + canManageDatasetPermissions: jsDatasetPermissions.canManageDatasetPermissions, + canManageFilesPermissions: true, // TODO: connect with js-dataverse DatasetPermissions.canManageFilesPermissions + canDeleteDataset: jsDatasetPermissions.canManageDatasetPermissions + } + } + static toLocks(jsDatasetLocks: JSDatasetLock[]): DatasetLock[] { + return jsDatasetLocks.map((jsDatasetLock) => { + return { + userPersistentId: jsDatasetLock.userId, + reason: jsDatasetLock.lockType as unknown as DatasetLockReason + } + }) + } } diff --git a/src/dataset/infrastructure/repositories/DatasetJSDataverseRepository.ts b/src/dataset/infrastructure/repositories/DatasetJSDataverseRepository.ts index c3fc7fbf0..02f05124b 100644 --- a/src/dataset/infrastructure/repositories/DatasetJSDataverseRepository.ts +++ b/src/dataset/infrastructure/repositories/DatasetJSDataverseRepository.ts @@ -5,12 +5,18 @@ import { getDatasetCitation, getDatasetSummaryFieldNames, Dataset as JSDataset, + DatasetUserPermissions as JSDatasetPermissions, getPrivateUrlDataset, getPrivateUrlDatasetCitation, - ReadError + getDatasetUserPermissions, + ReadError, + getDatasetLocks, + DatasetLock as JSDatasetLock } from '@iqss/dataverse-client-javascript' import { JSDatasetMapper } from '../mappers/JSDatasetMapper' +const includeDeaccessioned = true + export class DatasetJSDataverseRepository implements DatasetRepository { getByPersistentId( persistentId: string, @@ -18,16 +24,32 @@ export class DatasetJSDataverseRepository implements DatasetRepository { requestedVersion?: string ): Promise { return getDataset - .execute(persistentId, this.versionToVersionId(version)) + .execute(persistentId, this.versionToVersionId(version), includeDeaccessioned) .then((jsDataset) => Promise.all([ jsDataset, getDatasetSummaryFieldNames.execute(), - getDatasetCitation.execute(jsDataset.id, this.versionToVersionId(version)) + getDatasetCitation.execute(jsDataset.id, this.versionToVersionId(version)), + getDatasetUserPermissions.execute(jsDataset.id), + getDatasetLocks.execute(jsDataset.id) ]) ) - .then(([jsDataset, summaryFieldsNames, citation]: [JSDataset, string[], string]) => - JSDatasetMapper.toDataset(jsDataset, citation, summaryFieldsNames, requestedVersion) + .then( + ([jsDataset, summaryFieldsNames, citation, jsDatasetPermissions, jsDatasetLocks]: [ + JSDataset, + string[], + string, + JSDatasetPermissions, + JSDatasetLock[] + ]) => + JSDatasetMapper.toDataset( + jsDataset, + citation, + summaryFieldsNames, + jsDatasetPermissions, + jsDatasetLocks, + requestedVersion + ) ) .catch((error: ReadError) => { if (!version) { @@ -44,8 +66,20 @@ export class DatasetJSDataverseRepository implements DatasetRepository { getPrivateUrlDatasetCitation.execute(privateUrlToken) ]) .then(([jsDataset, summaryFieldsNames, citation]: [JSDataset, string[], string]) => - JSDatasetMapper.toDataset(jsDataset, citation, summaryFieldsNames, undefined) - ) + JSDatasetMapper.toDataset( + jsDataset, + citation, + summaryFieldsNames, + { + canEditDataset: true, + canPublishDataset: true, + canManageDatasetPermissions: true, + canDeleteDatasetDraft: true, + canViewUnpublishedDataset: true + }, + [] + ) + ) // TODO Connect with JS dataset permissions and getDatasetLocks.execute(privateUrlToken) when it is available in js-dataverse .catch((error: ReadError) => { throw new Error(error.message) }) diff --git a/src/files/domain/models/File.ts b/src/files/domain/models/File.ts index 1be76bc8f..bb24317aa 100644 --- a/src/files/domain/models/File.ts +++ b/src/files/domain/models/File.ts @@ -108,7 +108,7 @@ export class FileEmbargo { export interface FileTabularData { variablesCount: number observationsCount: number - unf: string + unf?: string } export enum FileLabelType { @@ -160,7 +160,7 @@ export class File { readonly type: FileType, readonly size: FileSize, readonly date: FileDate, - public downloadCount: number, + readonly downloadCount: number, readonly labels: FileLabel[], public readonly isDeleted: boolean, public readonly ingest: FileIngest, diff --git a/src/files/domain/models/FilePaginationInfo.ts b/src/files/domain/models/FilePaginationInfo.ts index 5f0e1ddcc..fbef8d177 100644 --- a/src/files/domain/models/FilePaginationInfo.ts +++ b/src/files/domain/models/FilePaginationInfo.ts @@ -24,7 +24,7 @@ export class FilePaginationInfo { withPageSize(pageSize: number): FilePaginationInfo { const getNewPage = (oldPageSize: number, newPageSize: number) => { - const newPage = Math.ceil((this.page * oldPageSize) / newPageSize) + const newPage = Math.ceil(((this.page - 1) * oldPageSize + 1) / newPageSize) return newPage > 0 ? newPage : 1 } return new FilePaginationInfo(getNewPage(this.pageSize, pageSize), pageSize, this.totalFiles) diff --git a/src/files/infrastructure/FileJSDataverseRepository.ts b/src/files/infrastructure/FileJSDataverseRepository.ts index e16c3c004..e96c994bf 100644 --- a/src/files/infrastructure/FileJSDataverseRepository.ts +++ b/src/files/infrastructure/FileJSDataverseRepository.ts @@ -1,5 +1,5 @@ import { FileRepository } from '../domain/repositories/FileRepository' -import { File, FilePublishingStatus } from '../domain/models/File' +import { File } from '../domain/models/File' import { FilesCountInfo } from '../domain/models/FilesCountInfo' import { FilePaginationInfo } from '../domain/models/FilePaginationInfo' import { FileUserPermissions } from '../domain/models/FileUserPermissions' @@ -10,7 +10,10 @@ import { getDatasetFilesTotalDownloadSize, getFileDownloadCount, getFileUserPermissions, - ReadError + ReadError, + File as JSFile, + getFileDataTables, + FileDataTable as JSFileTabularData } from '@iqss/dataverse-client-javascript' import { FileCriteria } from '../domain/models/FileCriteria' import { DomainFileMapper } from './mappers/DomainFileMapper' @@ -30,7 +33,6 @@ export class FileJSDataverseRepository implements FileRepository { criteria: FileCriteria = new FileCriteria() ): Promise { const jsPagination = DomainFileMapper.toJSPagination(paginationInfo) - return getDatasetFiles .execute( datasetPersistentId, @@ -41,48 +43,54 @@ export class FileJSDataverseRepository implements FileRepository { DomainFileMapper.toJSFileSearchCriteria(criteria), DomainFileMapper.toJSFileOrderCriteria(criteria.sortBy) ) - .then((jsFiles) => jsFiles.map((jsFile) => JSFileMapper.toFile(jsFile, datasetVersion))) - .then((files) => FileJSDataverseRepository.getAllWithDownloadCount(files)) - .then((files) => FileJSDataverseRepository.getAllWithThumbnail(files)) + .then((jsFiles) => + Promise.all([ + jsFiles, + FileJSDataverseRepository.getAllDownloadCount(jsFiles), + FileJSDataverseRepository.getAllThumbnails(jsFiles), + FileJSDataverseRepository.getAllTabularData(jsFiles) + ]) + ) + .then(([jsFiles, downloadCounts, thumbnails, jsTabularData]) => + jsFiles.map((jsFile, index) => + JSFileMapper.toFile( + jsFile, + datasetVersion, + downloadCounts[index], + thumbnails[index], + jsTabularData[index] + ) + ) + ) .catch((error: ReadError) => { throw new Error(error.message) }) } - private static getAllWithDownloadCount(files: File[]): Promise { + private static getAllTabularData( + jsFiles: JSFile[] + ): Promise<(JSFileTabularData[] | undefined)[]> { return Promise.all( - files.map((file) => - FileJSDataverseRepository.getDownloadCountById(file.id, file.version.publishingStatus).then( - (downloadCount) => { - file.downloadCount = downloadCount - return file - } - ) + jsFiles.map((jsFile) => + jsFile.tabularData ? getFileDataTables.execute(jsFile.id) : undefined ) ) } - private static getDownloadCountById( - id: number, - publishingStatus: FilePublishingStatus - ): Promise { - if (publishingStatus === FilePublishingStatus.RELEASED) { - return getFileDownloadCount.execute(id).then((downloadCount) => Number(downloadCount)) - } - return Promise.resolve(0) - } - - private static getAllWithThumbnail(files: File[]): Promise { + private static getAllDownloadCount(jsFiles: JSFile[]): Promise { return Promise.all( - files.map((file) => - FileJSDataverseRepository.getThumbnailById(file.id).then((thumbnail) => { - file.thumbnail = thumbnail - return file - }) + jsFiles.map((jsFile) => + jsFile.publicationDate + ? getFileDownloadCount.execute(jsFile.id).then((downloadCount) => Number(downloadCount)) + : 0 ) ) } + private static getAllThumbnails(jsFiles: JSFile[]): Promise<(string | undefined)[]> { + return Promise.all(jsFiles.map((jsFile) => this.getThumbnailById(jsFile.id))) + } + private static getThumbnailById(id: number): Promise { return fetch(`${this.DATAVERSE_BACKEND_URL}/api/access/datafile/${id}?imageThumb=400`) .then((response) => { diff --git a/src/files/infrastructure/mappers/DomainFileMapper.ts b/src/files/infrastructure/mappers/DomainFileMapper.ts index cda15bf92..71082838d 100644 --- a/src/files/infrastructure/mappers/DomainFileMapper.ts +++ b/src/files/infrastructure/mappers/DomainFileMapper.ts @@ -13,7 +13,10 @@ import { import { FileType } from '../../domain/models/File' export class DomainFileMapper { - static toJSPagination(paginationInfo: FilePaginationInfo): { limit?: number; offset?: number } { + static toJSPagination(paginationInfo: FilePaginationInfo): { + limit?: number + offset?: number + } { return { limit: paginationInfo.pageSize, offset: (paginationInfo.page - 1) * paginationInfo.pageSize diff --git a/src/files/infrastructure/mappers/JSFileMapper.ts b/src/files/infrastructure/mappers/JSFileMapper.ts index cba999277..d16f872b9 100644 --- a/src/files/infrastructure/mappers/JSFileMapper.ts +++ b/src/files/infrastructure/mappers/JSFileMapper.ts @@ -12,6 +12,7 @@ import { FilePublishingStatus, FileSize, FileSizeUnit, + FileTabularData, FileType, FileVersion } from '../../domain/models/File' @@ -23,7 +24,8 @@ import { FileContentTypeCount as JSFileContentTypeCount, FileCategoryNameCount as JSFileCategoryNameCount, FileAccessStatusCount as JSFileAccessStatusCount, - FileAccessStatus as JSFileAccessStatus + FileAccessStatus as JSFileAccessStatus, + FileDataTable as JSFileTabularData } from '@iqss/dataverse-client-javascript' import { DatasetPublishingStatus, DatasetVersion } from '../../../dataset/domain/models/Dataset' import { FileUserPermissions } from '../../domain/models/FileUserPermissions' @@ -36,7 +38,13 @@ import { import { FileAccessOption, FileTag } from '../../domain/models/FileCriteria' export class JSFileMapper { - static toFile(jsFile: JSFile, datasetVersion: DatasetVersion): File { + static toFile( + jsFile: JSFile, + datasetVersion: DatasetVersion, + downloadsCount: number, + thumbnail?: string, + jsTabularData?: JSFileTabularData[] + ): File { return new File( this.toFileId(jsFile.id), this.toFileVersion(jsFile.version, datasetVersion, jsFile.publicationDate), @@ -45,15 +53,15 @@ export class JSFileMapper { this.toFileType(jsFile.contentType, jsFile.originalFormatLabel), this.toFileSize(jsFile.sizeBytes), this.toFileDate(jsFile.creationDate, jsFile.publicationDate, jsFile.embargo), - this.toFileDownloads(), + this.toFileDownloads(downloadsCount), this.toFileLabels(jsFile.categories, jsFile.tabularTags), - false, - { status: FileIngestStatus.NONE }, + this.toFileIsDeleted(jsFile.deleted), + { status: FileIngestStatus.NONE }, // TODO - Implement this when it is added to js-dataverse this.toFileOriginalFileDownloadUrl(jsFile.id), - this.toFileThumbnail(), + this.toFileThumbnail(thumbnail), this.toFileDirectory(jsFile.directoryLabel), this.toFileEmbargo(jsFile.embargo), - this.toFileTabularData(), + this.toFileTabularData(jsTabularData), this.toFileDescription(jsFile.description), this.toFileChecksum(jsFile.checksum) ) @@ -134,8 +142,8 @@ export class JSFileMapper { throw new Error('File date not found') } - static toFileDownloads(): number { - return 0 // This is always 0 because the downloads come from a different endpoint + static toFileDownloads(downloadsCount: number): number { + return downloadsCount } static toFileLabels(jsFileCategories?: string[], jsFileTabularTags?: string[]): FileLabel[] { @@ -169,8 +177,8 @@ export class JSFileMapper { } } - static toFileThumbnail(): undefined { - return undefined // This is always undefined because the thumbnails come from a different endpoint + static toFileThumbnail(thumbnail?: string): string | undefined { + return thumbnail } static toFileDirectory(jsFileDirectory: string | undefined): string | undefined { @@ -184,8 +192,15 @@ export class JSFileMapper { return undefined } - static toFileTabularData(): undefined { - return undefined // This is always undefined because the tabular data comes from a different endpoint + static toFileTabularData(jsTabularData?: JSFileTabularData[]): FileTabularData | undefined { + if (jsTabularData === undefined) { + return undefined + } + return { + variablesCount: jsTabularData[0].varQuantity ?? 0, + observationsCount: jsTabularData[0].caseQuantity ?? 0, + unf: jsTabularData[0].UNF + } } static toFileDescription(jsFileDescription?: string): string | undefined { @@ -240,4 +255,8 @@ export class JSFileMapper { return FileAccessOption.EMBARGOED_RESTRICTED } } + + static toFileIsDeleted(jsFileIsDeleted: boolean | undefined): boolean { + return jsFileIsDeleted ?? false + } } diff --git a/src/sections/alerts/AlertContext.ts b/src/sections/alerts/AlertContext.ts new file mode 100644 index 000000000..48bcc8682 --- /dev/null +++ b/src/sections/alerts/AlertContext.ts @@ -0,0 +1,16 @@ +import { createContext, useContext } from 'react' + +import { Alert, AlertMessageKey } from '../../alert/domain/models/Alert' + +interface DatasetAlertContextProps { + datasetAlerts: Alert[] + addDatasetAlert: (newAlert: Alert) => void + removeDatasetAlert: (alertId: AlertMessageKey) => void +} + +export const AlertContext = createContext({ + datasetAlerts: [], + addDatasetAlert: /* istanbul ignore next */ () => {}, + removeDatasetAlert: /* istanbul ignore next */ () => {} +}) +export const useAlertContext = () => useContext(AlertContext) diff --git a/src/sections/alerts/AlertProvider.tsx b/src/sections/alerts/AlertProvider.tsx new file mode 100644 index 000000000..6504ff8e7 --- /dev/null +++ b/src/sections/alerts/AlertProvider.tsx @@ -0,0 +1,31 @@ +import { PropsWithChildren, useState } from 'react' +import { AlertContext } from './AlertContext' + +import { Alert, AlertMessageKey } from '../../alert/domain/models/Alert' + +export const AlertProvider = ({ children }: PropsWithChildren) => { + const [datasetAlerts, setDatasetAlerts] = useState([]) + + const addDatasetAlert = (newAlert: Alert) => { + // Check if an alert with the same id already exists + const alertExists = datasetAlerts.some((alert) => alert.messageKey === newAlert.messageKey) + + // If it doesn't exist, add it to the array + if (!alertExists) datasetAlerts.push(newAlert) + } + + const removeDatasetAlert = (alertId: AlertMessageKey) => { + setDatasetAlerts(datasetAlerts.filter((alert) => alert.messageKey !== alertId)) + } + + return ( + + {children} + + ) +} diff --git a/src/sections/alerts/Alerts.module.scss b/src/sections/alerts/Alerts.module.scss new file mode 100644 index 000000000..3a63a82e6 --- /dev/null +++ b/src/sections/alerts/Alerts.module.scss @@ -0,0 +1,4 @@ +.container > * { + margin-top: 1em; + margin-right: 0.5em; +} \ No newline at end of file diff --git a/src/sections/alerts/Alerts.tsx b/src/sections/alerts/Alerts.tsx new file mode 100644 index 000000000..e576b838b --- /dev/null +++ b/src/sections/alerts/Alerts.tsx @@ -0,0 +1,32 @@ +import { Alert as AlertComponent } from '@iqss/dataverse-design-system' + +import { useTranslation } from 'react-i18next' +import styles from './Alerts.module.scss' +import parse from 'html-react-parser' +import { useAlertContext } from './AlertContext' +import { Alert } from '../../alert/domain/models/Alert' + +export function Alerts() { + const { t } = useTranslation('dataset') + const { datasetAlerts } = useAlertContext() + return ( +
+ {datasetAlerts.map((alert: Alert, index) => { + const translatedMsg = alert.dynamicFields + ? t(`alerts.${alert.messageKey}.alertText`, alert.dynamicFields) + : t(`alerts.${alert.messageKey}.alertText`) + const translatedHeading = t(`alerts.${alert.messageKey}.heading`) + const alertKey = `alert-${index}` + return ( + + {parse(translatedMsg)} + + ) + })} +
+ ) +} diff --git a/src/sections/dataset/Dataset.tsx b/src/sections/dataset/Dataset.tsx index c5444efb3..8095e0ae8 100644 --- a/src/sections/dataset/Dataset.tsx +++ b/src/sections/dataset/Dataset.tsx @@ -14,6 +14,8 @@ import { DatasetActionButtons } from './dataset-action-buttons/DatasetActionButt import { useDataset } from './DatasetContext' import { useEffect } from 'react' import { DatasetAlerts } from './dataset-alerts/DatasetAlerts' +import { useNotImplementedModal } from '../not-implemented/NotImplementedModalContext' +import { NotImplementedModal } from '../not-implemented/NotImplementedModal' interface DatasetProps { fileRepository: FileRepository @@ -23,6 +25,7 @@ export function Dataset({ fileRepository }: DatasetProps) { const { setIsLoading } = useLoading() const { dataset, isLoading } = useDataset() const { t } = useTranslation('dataset') + const { hideModal, isModalOpen } = useNotImplementedModal() useEffect(() => { setIsLoading(isLoading) @@ -34,6 +37,7 @@ export function Dataset({ fileRepository }: DatasetProps) { return ( <> + {!dataset ? ( ) : ( diff --git a/src/sections/dataset/DatasetFactory.tsx b/src/sections/dataset/DatasetFactory.tsx index 1ac3e1e15..7cbdecc0b 100644 --- a/src/sections/dataset/DatasetFactory.tsx +++ b/src/sections/dataset/DatasetFactory.tsx @@ -11,6 +11,8 @@ import { SettingJSDataverseRepository } from '../../settings/infrastructure/Sett import { FilePermissionsProvider } from '../file/file-permissions/FilePermissionsProvider' import { SettingsProvider } from '../settings/SettingsProvider' import { DatasetProvider } from './DatasetProvider' +import { NotImplementedModalProvider } from '../not-implemented/NotImplementedModalProvider' +import { AlertProvider } from '../alerts/AlertProvider' const datasetRepository = new DatasetJSDataverseRepository() const fileRepository = new FileJSDataverseRepository() @@ -22,11 +24,15 @@ export class DatasetFactory { return ( - - - - - + + + + + + + + + ) 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 326518a5e..03faf3cd1 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 @@ -4,24 +4,29 @@ import { EditDatasetPermissionsMenu } from './EditDatasetPermissionsMenu' import { DeleteDatasetButton } from './DeleteDatasetButton' import { DeaccessionDatasetButton } from './DeaccessionDatasetButton' import { useTranslation } from 'react-i18next' +import { useNotImplementedModal } from '../../../not-implemented/NotImplementedModalContext' +import { useSession } from '../../../session/SessionContext' interface EditDatasetMenuProps { dataset: Dataset } export function EditDatasetMenu({ dataset }: EditDatasetMenuProps) { - if (!dataset.permissions.canUpdateDataset) { + const { user } = useSession() + + if (!user || !dataset.permissions.canUpdateDataset) { return <> } - + const { showModal } = useNotImplementedModal() const { t } = useTranslation('dataset') return ( + disabled={dataset.checkIsLockedFromEdits(user.persistentId)}> {t('datasetActionButtons.editDataset.filesUpload')} diff --git a/src/sections/dataset/dataset-action-buttons/link-dataset-button/LinkDatasetButton.tsx b/src/sections/dataset/dataset-action-buttons/link-dataset-button/LinkDatasetButton.tsx index 0a51341cd..ed613dfbb 100644 --- a/src/sections/dataset/dataset-action-buttons/link-dataset-button/LinkDatasetButton.tsx +++ b/src/sections/dataset/dataset-action-buttons/link-dataset-button/LinkDatasetButton.tsx @@ -2,13 +2,21 @@ import { Button, ButtonGroup } from '@iqss/dataverse-design-system' import { Dataset, DatasetPublishingStatus } from '../../../../dataset/domain/models/Dataset' import { useTranslation } from 'react-i18next' import { useSession } from '../../../session/SessionContext' +import { useNotImplementedModal } from '../../../not-implemented/NotImplementedModalContext' interface LinkDatasetButtonProps { dataset: Dataset } + export function LinkDatasetButton({ dataset }: LinkDatasetButtonProps) { const { t } = useTranslation('dataset') const { user } = useSession() + const handleClick = () => { + // TODO - Implement upload files + showModal() + } + const { showModal } = useNotImplementedModal() + if ( !user || !dataset.isReleased || @@ -19,7 +27,9 @@ export function LinkDatasetButton({ dataset }: LinkDatasetButtonProps) { return ( - + ) } diff --git a/src/sections/dataset/dataset-action-buttons/publish-dataset-menu/PublishDatasetMenu.tsx b/src/sections/dataset/dataset-action-buttons/publish-dataset-menu/PublishDatasetMenu.tsx index 58ec23cd8..6ea678467 100644 --- a/src/sections/dataset/dataset-action-buttons/publish-dataset-menu/PublishDatasetMenu.tsx +++ b/src/sections/dataset/dataset-action-buttons/publish-dataset-menu/PublishDatasetMenu.tsx @@ -2,29 +2,42 @@ import { Dataset, DatasetPublishingStatus } from '../../../../dataset/domain/mod import { DropdownButton, DropdownButtonItem } from '@iqss/dataverse-design-system' import { ChangeCurationStatusMenu } from './ChangeCurationStatusMenu' import { useTranslation } from 'react-i18next' +import { useNotImplementedModal } from '../../../not-implemented/NotImplementedModalContext' +import { useSession } from '../../../session/SessionContext' interface PublishDatasetMenuProps { dataset: Dataset } export function PublishDatasetMenu({ dataset }: PublishDatasetMenuProps) { + const { user } = useSession() if ( !dataset.version.isLatest || dataset.version.publishingStatus !== DatasetPublishingStatus.DRAFT || + !user || !dataset.permissions.canPublishDataset ) { return <> } const { t } = useTranslation('dataset') + const handleSelect = () => { + // TODO - Implement upload files + showModal() + } + const { showModal } = useNotImplementedModal() + return ( {t('datasetActionButtons.publish.publish')} {dataset.version.isInReview && ( diff --git a/src/sections/dataset/dataset-action-buttons/submit-for-review-button/SubmitForReviewButton.tsx b/src/sections/dataset/dataset-action-buttons/submit-for-review-button/SubmitForReviewButton.tsx index 1f9c18a3a..b352b4065 100644 --- a/src/sections/dataset/dataset-action-buttons/submit-for-review-button/SubmitForReviewButton.tsx +++ b/src/sections/dataset/dataset-action-buttons/submit-for-review-button/SubmitForReviewButton.tsx @@ -1,17 +1,20 @@ import { Dataset, DatasetPublishingStatus } from '../../../../dataset/domain/models/Dataset' import { Button } from '@iqss/dataverse-design-system' import { useTranslation } from 'react-i18next' +import { useSession } from '../../../session/SessionContext' interface SubmitForReviewButtonProps { dataset: Dataset } export function SubmitForReviewButton({ dataset }: SubmitForReviewButtonProps) { + const { user } = useSession() if ( !dataset.version.isLatest || dataset.version.publishingStatus !== DatasetPublishingStatus.DRAFT || dataset.isLockedInWorkflow || dataset.permissions.canPublishDataset || + !user || !dataset.permissions.canUpdateDataset ) { return <> @@ -22,7 +25,9 @@ export function SubmitForReviewButton({ dataset }: SubmitForReviewButtonProps) { ) diff --git a/src/sections/dataset/dataset-files/file-criteria-form/FileCriteriaForm.tsx b/src/sections/dataset/dataset-files/file-criteria-form/FileCriteriaForm.tsx index 5096d4001..26194bc35 100644 --- a/src/sections/dataset/dataset-files/file-criteria-form/FileCriteriaForm.tsx +++ b/src/sections/dataset/dataset-files/file-criteria-form/FileCriteriaForm.tsx @@ -18,31 +18,36 @@ export function FileCriteriaForm({ onCriteriaChange, filesCountInfo }: FileCriteriaInputsProps) { - if (!filesCountInfo || filesCountInfo.total < MINIMUM_FILES_TO_SHOW_CRITERIA_INPUTS) { - return <> - } + const showFileCriteriaInputs = + filesCountInfo && filesCountInfo.total >= MINIMUM_FILES_TO_SHOW_CRITERIA_INPUTS return ( -
- - - - - - - - - - - - - - - - -
+
+
+ + {showFileCriteriaInputs && ( + + + + )} + + + + + {showFileCriteriaInputs && ( + + + + + + + + + )} +
+
) } diff --git a/src/sections/dataset/dataset-files/files-table/file-actions/download-files/DownloadFilesButton.tsx b/src/sections/dataset/dataset-files/files-table/file-actions/download-files/DownloadFilesButton.tsx index 4bf4bbbed..887b4c940 100644 --- a/src/sections/dataset/dataset-files/files-table/file-actions/download-files/DownloadFilesButton.tsx +++ b/src/sections/dataset/dataset-files/files-table/file-actions/download-files/DownloadFilesButton.tsx @@ -7,6 +7,7 @@ import { useTranslation } from 'react-i18next' import { FileSelection } from '../../row-selection/useFileSelection' import { NoSelectedFilesModal } from '../no-selected-files-modal/NoSelectedFilesModal' import { useState } from 'react' +import { useNotImplementedModal } from '../../../../../not-implemented/NotImplementedModalContext' interface DownloadFilesButtonProps { files: File[] @@ -15,11 +16,16 @@ interface DownloadFilesButtonProps { const MINIMUM_FILES_COUNT_TO_SHOW_DOWNLOAD_FILES_BUTTON = 1 const SELECTED_FILES_EMPTY = 0 + export function DownloadFilesButton({ files, fileSelection }: DownloadFilesButtonProps) { const { t } = useTranslation('files') const { dataset } = useDataset() const [showNoFilesSelectedModal, setShowNoFilesSelectedModal] = useState(false) - + const handleClick = () => { + // TODO - Implement upload files + showModal() + } + const { showModal } = useNotImplementedModal() if ( files.length < MINIMUM_FILES_COUNT_TO_SHOW_DOWNLOAD_FILES_BUTTON || !dataset?.permissions.canDownloadFiles @@ -30,6 +36,8 @@ export function DownloadFilesButton({ files, fileSelection }: DownloadFilesButto const onClick = () => { if (Object.keys(fileSelection).length === SELECTED_FILES_EMPTY) { setShowNoFilesSelectedModal(true) + } else { + handleClick() } } diff --git a/src/sections/dataset/dataset-files/files-table/file-actions/edit-files-menu/EditFilesMenu.tsx b/src/sections/dataset/dataset-files/files-table/file-actions/edit-files-menu/EditFilesMenu.tsx index 052d5957e..23f34bd02 100644 --- a/src/sections/dataset/dataset-files/files-table/file-actions/edit-files-menu/EditFilesMenu.tsx +++ b/src/sections/dataset/dataset-files/files-table/file-actions/edit-files-menu/EditFilesMenu.tsx @@ -30,7 +30,7 @@ export function EditFilesMenu({ files, fileSelection }: EditFilesMenuProps) { variant="secondary" id="edit-files-menu" title={t('actions.editFilesMenu.title')} - disabled={dataset.isLockedFromEdits || !dataset.hasValidTermsOfAccess} + disabled={dataset.checkIsLockedFromEdits(user.persistentId) || !dataset.hasValidTermsOfAccess} icon={}>
diff --git a/src/sections/dataset/dataset-files/files-table/file-actions/edit-files-menu/EditFilesOptions.tsx b/src/sections/dataset/dataset-files/files-table/file-actions/edit-files-menu/EditFilesOptions.tsx index 1377303f5..4f21ec3a2 100644 --- a/src/sections/dataset/dataset-files/files-table/file-actions/edit-files-menu/EditFilesOptions.tsx +++ b/src/sections/dataset/dataset-files/files-table/file-actions/edit-files-menu/EditFilesOptions.tsx @@ -4,21 +4,27 @@ import { useTranslation } from 'react-i18next' import { useState } from 'react' import { FileSelection } from '../../row-selection/useFileSelection' import { NoSelectedFilesModal } from '../no-selected-files-modal/NoSelectedFilesModal' +import { useNotImplementedModal } from '../../../../../not-implemented/NotImplementedModalContext' interface EditFileOptionsProps { files: File[] fileSelection: FileSelection } + const SELECTED_FILES_EMPTY = 0 + export function EditFilesOptions({ files, fileSelection }: EditFileOptionsProps) { const { t } = useTranslation('files') const [showNoFilesSelectedModal, setShowNoFilesSelectedModal] = useState(false) const settingsEmbargoAllowed = false // TODO - Ask Guillermo if this is included in the settings endpoint const provenanceEnabledByConfig = false // TODO - Ask Guillermo if this is included in the MVP and from which endpoint is coming from - + const { showModal } = useNotImplementedModal() const onClick = () => { if (Object.keys(fileSelection).length === SELECTED_FILES_EMPTY) { setShowNoFilesSelectedModal(true) + } else { + // TODO - Implement edit files + showModal() } } diff --git a/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/file-options-menu/FileOptionsMenu.tsx b/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/file-options-menu/FileOptionsMenu.tsx index c81d34c77..f336fe594 100644 --- a/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/file-options-menu/FileOptionsMenu.tsx +++ b/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/file-options-menu/FileOptionsMenu.tsx @@ -24,7 +24,7 @@ export function FileOptionsMenu({ file }: { file: File }) { {t('actions.optionsMenu.title')}}> + + + ) +} diff --git a/src/sections/not-implemented/NotImplementedModalContext.ts b/src/sections/not-implemented/NotImplementedModalContext.ts new file mode 100644 index 000000000..81165cc8a --- /dev/null +++ b/src/sections/not-implemented/NotImplementedModalContext.ts @@ -0,0 +1,15 @@ +import { createContext, useContext } from 'react' + +interface NotImplementedModalContextProps { + showModal: () => void + hideModal: () => void + isModalOpen: boolean +} + +export const NotImplementedModal = createContext({ + isModalOpen: false, + showModal: /* istanbul ignore next */ () => {}, + hideModal: /* istanbul ignore next */ () => {} +}) + +export const useNotImplementedModal = () => useContext(NotImplementedModal) diff --git a/src/sections/not-implemented/NotImplementedModalProvider.tsx b/src/sections/not-implemented/NotImplementedModalProvider.tsx new file mode 100644 index 000000000..3a18ce32e --- /dev/null +++ b/src/sections/not-implemented/NotImplementedModalProvider.tsx @@ -0,0 +1,14 @@ +import { useState, PropsWithChildren } from 'react' +import { NotImplementedModal } from './NotImplementedModalContext' + +export function NotImplementedModalProvider({ children }: PropsWithChildren) { + const [isModalOpen, setIsModalOpen] = useState(false) + const showModal = () => setIsModalOpen(true) + const hideModal = () => setIsModalOpen(false) + + return ( + + {children} + + ) +} diff --git a/src/settings/infrastructure/SettingJSDataverseRepository.ts b/src/settings/infrastructure/SettingJSDataverseRepository.ts index eb5801de7..e55e995fe 100644 --- a/src/settings/infrastructure/SettingJSDataverseRepository.ts +++ b/src/settings/infrastructure/SettingJSDataverseRepository.ts @@ -9,11 +9,33 @@ export class SettingJSDataverseRepository implements SettingRepository { // TODO - implement using js-dataverse return new Promise((resolve) => { setTimeout(() => { - resolve({ - name: SettingName.ZIP_DOWNLOAD_LIMIT, - value: new ZipDownloadLimit(1, FileSizeUnit.BYTES) - } as Setting) + resolve(mockedSettingResponse(name)) }, 1000) }) } } + +function mockedSettingResponse(name: SettingName): Setting { + switch (name) { + case SettingName.ZIP_DOWNLOAD_LIMIT: + return { + name: SettingName.ZIP_DOWNLOAD_LIMIT, + value: new ZipDownloadLimit(1, FileSizeUnit.BYTES) + } as Setting + case SettingName.ALLOWED_EXTERNAL_STATUSES: + return { + name: SettingName.ALLOWED_EXTERNAL_STATUSES, + value: [ + 'Author Contacted', + 'Privacy Review', + 'Awaiting Paper Publication', + 'Final Approval' + ] + } as Setting + case SettingName.HAS_PUBLIC_STORE: + return { + name: SettingName.HAS_PUBLIC_STORE, + value: false + } as Setting + } +} diff --git a/src/stories/WithAlerts.tsx b/src/stories/WithAlerts.tsx new file mode 100644 index 000000000..9225bd196 --- /dev/null +++ b/src/stories/WithAlerts.tsx @@ -0,0 +1,10 @@ +import { StoryFn } from '@storybook/react' +import { AlertProvider } from '../sections/alerts/AlertProvider' + +export const WithAlerts = (Story: StoryFn) => { + return ( + + + + ) +} diff --git a/src/stories/WithNotImplementedModal.tsx b/src/stories/WithNotImplementedModal.tsx new file mode 100644 index 000000000..2f91d7d7b --- /dev/null +++ b/src/stories/WithNotImplementedModal.tsx @@ -0,0 +1,10 @@ +import { StoryFn } from '@storybook/react' +import { NotImplementedModalProvider } from '../sections/not-implemented/NotImplementedModalProvider' + +export const WithNotImplementedModal = (Story: StoryFn) => { + return ( + + + + ) +} diff --git a/src/stories/dataset/Dataset.stories.tsx b/src/stories/dataset/Dataset.stories.tsx index ab3c7e26e..85fe96332 100644 --- a/src/stories/dataset/Dataset.stories.tsx +++ b/src/stories/dataset/Dataset.stories.tsx @@ -15,11 +15,13 @@ import { WithDatasetDraftAsOwner } from './WithDatasetDraftAsOwner' import { WithDatasetNotFound } from './WithDatasetNotFound' import { WithDatasetLoading } from './WithDatasetLoading' import { WithLoggedInUser } from '../WithLoggedInUser' +import { WithAlerts } from '../WithAlerts' +import { WithNotImplementedModal } from '../WithNotImplementedModal' const meta: Meta = { title: 'Pages/Dataset', component: Dataset, - decorators: [WithI18next, WithCitationMetadataBlockInfo, WithSettings], + decorators: [WithI18next, WithCitationMetadataBlockInfo, WithSettings, WithAlerts], parameters: { // Sets the delay for all stories. chromatic: { delay: 15000, pauseAnimationAtEnd: true } @@ -30,16 +32,28 @@ export default meta type Story = StoryObj export const Default: Story = { - decorators: [WithLayout, WithDataset, WithFilePermissionsDenied], + decorators: [WithLayout, WithDataset, WithFilePermissionsDenied, WithNotImplementedModal], render: () => } export const DraftWithAllDatasetPermissions: Story = { - decorators: [WithLayout, WithDatasetDraftAsOwner, WithLoggedInUser, WithFilePermissionsGranted], + decorators: [ + WithLayout, + WithDatasetDraftAsOwner, + WithLoggedInUser, + WithFilePermissionsGranted, + WithNotImplementedModal + ], render: () => } export const LoggedInAsOwner: Story = { - decorators: [WithDataset, WithLayout, WithLoggedInUser, WithFilePermissionsGranted], + decorators: [ + WithDataset, + WithLayout, + WithLoggedInUser, + WithFilePermissionsGranted, + WithNotImplementedModal + ], render: () => } diff --git a/src/stories/dataset/dataset-alerts/DatasetAlert.stories.tsx b/src/stories/dataset/dataset-alerts/DatasetAlert.stories.tsx index 072994ad5..31de08bf7 100644 --- a/src/stories/dataset/dataset-alerts/DatasetAlert.stories.tsx +++ b/src/stories/dataset/dataset-alerts/DatasetAlert.stories.tsx @@ -8,15 +8,51 @@ import { DatasetMother, DatasetPermissionsMother } from '../../../../tests/component/dataset/domain/models/DatasetMother' +import { useAlertContext } from '../../../sections/alerts/AlertContext' +import { WithAlerts } from '../../WithAlerts' +import { Alert, AlertMessageKey } from '../../../alert/domain/models/Alert' const meta: Meta = { title: 'Sections/Dataset Page/DatasetAlerts', component: DatasetAlerts, - decorators: [WithI18next] + decorators: [WithI18next, WithAlerts] } +const allUpdateAlerts: Alert[] = [ + new Alert('success', AlertMessageKey.METADATA_UPDATED), + new Alert('success', AlertMessageKey.THUMBNAIL_UPDATED), + new Alert('success', AlertMessageKey.TERMS_UPDATED), + new Alert('success', AlertMessageKey.FILES_UPDATED), + new Alert('success', AlertMessageKey.DATASET_DELETED) +] export default meta type Story = StoryObj +export const UpdateAlerts: Story = { + render: () => { + const dataset = DatasetMother.createRealistic() + const { addDatasetAlert } = useAlertContext() + allUpdateAlerts.forEach((alert) => addDatasetAlert(alert)) + return ( +
+ +
+ ) + } +} + +const publishAlert = new Alert('warning', AlertMessageKey.PUBLISH_IN_PROGRESS) +export const PublishInProgress: Story = { + render: () => { + const dataset = DatasetMother.createRealistic() + const { addDatasetAlert } = useAlertContext() + addDatasetAlert(publishAlert) + return ( +
+ +
+ ) + } +} export const DraftVersion: Story = { render: () => { diff --git a/src/stories/not-implemented/NotImplementedModal.stories.tsx b/src/stories/not-implemented/NotImplementedModal.stories.tsx new file mode 100644 index 000000000..8b8489011 --- /dev/null +++ b/src/stories/not-implemented/NotImplementedModal.stories.tsx @@ -0,0 +1,30 @@ +import { Meta, StoryObj } from '@storybook/react' +import { NotImplementedModal } from '../../sections/not-implemented/NotImplementedModal' +import { Button } from '@iqss/dataverse-design-system' +import { useNotImplementedModal } from '../../sections/not-implemented/NotImplementedModalContext' +import { WithNotImplementedModal } from '../WithNotImplementedModal' + +const meta: Meta = { + title: 'Sections/NotImplementedModal', + component: NotImplementedModal, + decorators: [WithNotImplementedModal] +} + +export default meta + +type Story = StoryObj + +function DefaultExample() { + const { showModal, hideModal, isModalOpen } = useNotImplementedModal() + return ( + <> + + + + + ) +} + +export const Default: Story = { + render: () => DefaultExample() +} diff --git a/src/users/domain/models/User.ts b/src/users/domain/models/User.ts index be3cb9315..1b7885f5d 100644 --- a/src/users/domain/models/User.ts +++ b/src/users/domain/models/User.ts @@ -1,3 +1,4 @@ export interface User { name: string + persistentId: string } diff --git a/src/users/infrastructure/repositories/UserJSDataverseRepository.ts b/src/users/infrastructure/repositories/UserJSDataverseRepository.ts index 69ea18d64..dd8c6607d 100644 --- a/src/users/infrastructure/repositories/UserJSDataverseRepository.ts +++ b/src/users/infrastructure/repositories/UserJSDataverseRepository.ts @@ -11,7 +11,10 @@ export class UserJSDataverseRepository implements UserRepository { return getCurrentAuthenticatedUser .execute() .then((authenticatedUser: AuthenticatedUser) => { - return { name: authenticatedUser.displayName } + return { + name: authenticatedUser.displayName, + persistentId: authenticatedUser.persistentUserId + } }) .catch((error: ReadError) => { throw new Error(error.message) diff --git a/tests/component/dataset/domain/models/DatasetMother.ts b/tests/component/dataset/domain/models/DatasetMother.ts index 8a255d4be..85bc9fee3 100644 --- a/tests/component/dataset/domain/models/DatasetMother.ts +++ b/tests/component/dataset/domain/models/DatasetMother.ts @@ -166,7 +166,7 @@ export class DatasetPermissionsMother { export class DatasetLockMother { static create(props?: Partial): DatasetLock { return { - id: faker.datatype.number(), + userPersistentId: faker.internet.userName(), reason: faker.helpers.arrayElement(Object.values(DatasetLockReason)), ...props } @@ -408,7 +408,7 @@ export class DatasetMother { datasetContact: [ { datasetContactName: 'Admin, Dataverse', - datasetContactEmail: '' + datasetContactEmail: 'admin@dataverse.org' } ], dsDescription: [ diff --git a/tests/component/dataset/infrastructure/mappers/JSDatasetMapper.spec.ts b/tests/component/dataset/infrastructure/mappers/JSDatasetMapper.spec.ts index 699c3d8a7..ed3a65b19 100644 --- a/tests/component/dataset/infrastructure/mappers/JSDatasetMapper.spec.ts +++ b/tests/component/dataset/infrastructure/mappers/JSDatasetMapper.spec.ts @@ -1,11 +1,16 @@ 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 { + DatasetLockType, + DatasetVersionState, + DatasetLock as JSDatasetLock +} from '@iqss/dataverse-client-javascript' import { CitationMetadataBlock, DatasetMetadataBlock } from '@iqss/dataverse-client-javascript/dist/datasets/domain/models/Dataset' +import { DatasetLockReason } from '../../../../../src/dataset/domain/models/Dataset' chai.use(chaiAsPromised) const expect = chai.expect @@ -51,6 +56,20 @@ const jsDataset = { 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 jsDatasetPermissions = { + canEditDataset: true, + canPublishDataset: true, + canManageDatasetPermissions: true, + canDeleteDatasetDraft: true, + canViewUnpublishedDataset: true +} +const jsDatasetLocks: JSDatasetLock[] = [ + { + lockType: DatasetLockType.IN_REVIEW, + userId: 'dataverseAdmin', + datasetPersistentId: 'doi:10.5072/FK2/B4B2MJ' + } +] const expectedDataset = { persistentId: 'doi:10.5072/FK2/B4B2MJ', version: { @@ -69,7 +88,7 @@ const expectedDataset = { { semanticMeaning: 'dataset', value: 'Draft' }, { semanticMeaning: 'warning', value: 'Unpublished' } ], - alerts: [{ variant: 'warning', message: 'draftVersion', dynamicFields: undefined }], + alerts: [{ variant: 'warning', messageKey: 'draftVersion', dynamicFields: undefined }], summaryFields: [ { name: 'citation', @@ -116,7 +135,12 @@ const expectedDataset = { canManageFilesPermissions: true, canDeleteDataset: true }, - locks: [], + locks: [ + { + userPersistentId: 'dataverseAdmin', + reason: DatasetLockReason.IN_REVIEW + } + ], hasValidTermsOfAccess: true, isValid: true, isReleased: false, @@ -148,11 +172,11 @@ const expectedDatasetAlternateVersion = { alerts: [ { variant: 'warning', - message: 'draftVersion', + messageKey: 'draftVersion', dynamicFields: undefined }, { - message: 'requestedVersionNotFoundShowDraft', + messageKey: 'requestedVersionNotFoundShowDraft', variant: 'warning', dynamicFields: { requestedVersion: '4.0' } } @@ -176,7 +200,12 @@ const expectedDatasetAlternateVersion = { uri: 'http://creativecommons.org/publicdomain/zero/1.0', iconUri: 'https://licensebuttons.net/p/zero/1.0/88x31.png' }, - locks: [], + locks: [ + { + userPersistentId: 'dataverseAdmin', + reason: DatasetLockReason.IN_REVIEW + } + ], metadataBlocks: [ { name: 'citation', @@ -208,7 +237,13 @@ const expectedDatasetAlternateVersion = { } describe('JS Dataset Mapper', () => { it('maps jsDataset model to the domain Dataset model', () => { - const mapped = JSDatasetMapper.toDataset(jsDataset, citation, datasetSummaryFields) + const mapped = JSDatasetMapper.toDataset( + jsDataset, + citation, + datasetSummaryFields, + jsDatasetPermissions, + jsDatasetLocks + ) expect(expectedDataset).to.deep.equal(mapped) }) it('maps jsDataset model to the domain Dataset model for alternate version', () => { @@ -216,6 +251,8 @@ describe('JS Dataset Mapper', () => { jsDataset, citation, datasetSummaryFields, + jsDatasetPermissions, + jsDatasetLocks, '4.0' ) @@ -255,7 +292,9 @@ describe('JS Dataset Mapper', () => { JSDatasetMapper.toDataset( jsDatasetWithAlternativePersistentId, citation, - datasetSummaryFields + datasetSummaryFields, + jsDatasetPermissions, + jsDatasetLocks ) ) }) @@ -290,7 +329,13 @@ describe('JS Dataset Mapper', () => { } expect(expectedDatasetWithCitationDate).to.deep.equal( - JSDatasetMapper.toDataset(jsDatasetWithCitationDate, citation, datasetSummaryFields) + JSDatasetMapper.toDataset( + jsDatasetWithCitationDate, + citation, + datasetSummaryFields, + jsDatasetPermissions, + jsDatasetLocks + ) ) }) @@ -323,7 +368,13 @@ describe('JS Dataset Mapper', () => { ] } expect(expectedDatasetWithPublicationDate).to.deep.equal( - JSDatasetMapper.toDataset(jsDatasetWithPublicationDate, citation, datasetSummaryFields) + JSDatasetMapper.toDataset( + jsDatasetWithPublicationDate, + citation, + datasetSummaryFields, + jsDatasetPermissions, + jsDatasetLocks + ) ) }) }) diff --git a/tests/component/sections/dataset/dataset-action-buttons/edit-dataset-menu/EditDatasetMenu.spec.tsx b/tests/component/sections/dataset/dataset-action-buttons/edit-dataset-menu/EditDatasetMenu.spec.tsx index 52ec1060f..aae0fa697 100644 --- a/tests/component/sections/dataset/dataset-action-buttons/edit-dataset-menu/EditDatasetMenu.spec.tsx +++ b/tests/component/sections/dataset/dataset-action-buttons/edit-dataset-menu/EditDatasetMenu.spec.tsx @@ -16,7 +16,7 @@ describe('EditDatasetMenu', () => { isReleased: true }) - cy.customMount() + cy.mountAuthenticated() cy.findByRole('button', { name: 'Edit Dataset' }).should('exist').should('be.enabled').click() @@ -30,13 +30,27 @@ describe('EditDatasetMenu', () => { cy.findByRole('button', { name: 'Deaccession Dataset' }).should('exist') }) + it('does not render if the user is not authenticated', () => { + const dataset = DatasetMother.create({ + permissions: DatasetPermissionsMother.createWithAllAllowed(), + locks: [], + hasValidTermsOfAccess: true, + version: DatasetVersionMother.createReleasedWithLatestVersionIsADraft(), + isReleased: true + }) + + cy.customMount() + + cy.findByRole('button', { name: 'Edit Dataset' }).should('not.exist') + }) + it('does not render the EditDatasetMenu if the user does not have update dataset permissions', () => { const dataset = DatasetMother.create({ permissions: DatasetPermissionsMother.createWithUpdateDatasetNotAllowed(), locks: [] }) - cy.customMount() + cy.mountAuthenticated() cy.findByRole('button', { name: 'Edit Dataset' }).should('not.exist') }) @@ -47,7 +61,7 @@ describe('EditDatasetMenu', () => { locks: [DatasetLockMother.createLockedInEditInProgress()] }) - cy.customMount() + cy.mountAuthenticated() cy.findByRole('button', { name: 'Edit Dataset' }).should('exist').should('be.disabled') }) @@ -59,7 +73,7 @@ describe('EditDatasetMenu', () => { hasValidTermsOfAccess: true }) - cy.customMount() + cy.mountAuthenticated() cy.findByRole('button', { name: 'Edit Dataset' }).click() cy.findByRole('button', { name: 'Files (Upload)' }) @@ -77,7 +91,7 @@ describe('EditDatasetMenu', () => { hasValidTermsOfAccess: false }) - cy.customMount() + cy.mountAuthenticated() cy.findByRole('button', { name: 'Edit Dataset' }).click() cy.findByRole('button', { name: 'Files (Upload)' }) diff --git a/tests/component/sections/dataset/dataset-action-buttons/publish-dataset-menu/PublishDatasetMenu.spec.tsx b/tests/component/sections/dataset/dataset-action-buttons/publish-dataset-menu/PublishDatasetMenu.spec.tsx index 9960f2655..c4904c7f6 100644 --- a/tests/component/sections/dataset/dataset-action-buttons/publish-dataset-menu/PublishDatasetMenu.spec.tsx +++ b/tests/component/sections/dataset/dataset-action-buttons/publish-dataset-menu/PublishDatasetMenu.spec.tsx @@ -5,7 +5,6 @@ import { DatasetPermissionsMother, DatasetVersionMother } from '../../../../dataset/domain/models/DatasetMother' -import { DatasetLockReason } from '../../../../../../src/dataset/domain/models/Dataset' import { SettingRepository } from '../../../../../../src/settings/domain/repositories/SettingRepository' import { SettingMother } from '../../../../settings/domain/models/SettingMother' import { SettingsProvider } from '../../../../../../src/sections/settings/SettingsProvider' @@ -20,7 +19,7 @@ describe('PublishDatasetMenu', () => { isValid: true }) - cy.customMount() + cy.mountAuthenticated() cy.findByRole('button', { name: 'Publish Dataset' }) .should('exist') @@ -44,7 +43,7 @@ describe('PublishDatasetMenu', () => { .stub() .resolves(SettingMother.createExternalStatusesAllowed(['Author Contacted', 'Privacy Review'])) - cy.customMount( + cy.mountAuthenticated( @@ -55,6 +54,20 @@ describe('PublishDatasetMenu', () => { cy.findByRole('button', { name: 'Change Curation Status' }).should('exist') }) + it('does not render if the user is not authenticated', () => { + const dataset = DatasetMother.create({ + version: DatasetVersionMother.createDraftAsLatestVersion(), + permissions: DatasetPermissionsMother.createWithPublishingDatasetAllowed(), + locks: [], + hasValidTermsOfAccess: true, + isValid: true + }) + + cy.customMount() + + cy.findByRole('button', { name: 'Publish Dataset' }).should('not.exist') + }) + it('does not render the PublishDatasetMenu if publishing is not allowed', () => { const dataset = DatasetMother.create({ version: DatasetVersionMother.createDraftAsLatestVersion(), @@ -62,7 +75,7 @@ describe('PublishDatasetMenu', () => { locks: [] }) - cy.customMount() + cy.mountAuthenticated() cy.findByRole('button', { name: 'Publish Dataset' }).should('not.exist') }) @@ -74,7 +87,7 @@ describe('PublishDatasetMenu', () => { locks: [] }) - cy.customMount() + cy.mountAuthenticated() cy.findByRole('button', { name: 'Publish Dataset' }).should('not.exist') }) @@ -86,7 +99,7 @@ describe('PublishDatasetMenu', () => { locks: [] }) - cy.customMount() + cy.mountAuthenticated() cy.findByRole('button', { name: 'Publish Dataset' }).should('not.exist') }) @@ -100,7 +113,7 @@ describe('PublishDatasetMenu', () => { isValid: true }) - cy.customMount() + cy.mountAuthenticated() cy.findByRole('button', { name: 'Publish Dataset' }).should('be.enabled') }) @@ -109,17 +122,12 @@ describe('PublishDatasetMenu', () => { const dataset = DatasetMother.create({ version: DatasetVersionMother.createDraftAsLatestVersion(), permissions: DatasetPermissionsMother.createWithPublishingDatasetAllowed(), - locks: [ - { - id: 1, - reason: DatasetLockReason.EDIT_IN_PROGRESS - } - ], + locks: [DatasetLockMother.createLockedInEditInProgress()], hasValidTermsOfAccess: true, isValid: true }) - cy.customMount() + cy.mountAuthenticated() cy.findByRole('button', { name: 'Publish Dataset' }).should('be.disabled') }) @@ -133,7 +141,7 @@ describe('PublishDatasetMenu', () => { isValid: true }) - cy.customMount() + cy.mountAuthenticated() cy.findByRole('button', { name: 'Publish Dataset' }).should('be.disabled') }) @@ -147,7 +155,7 @@ describe('PublishDatasetMenu', () => { isValid: false }) - cy.customMount() + cy.mountAuthenticated() cy.findByRole('button', { name: 'Publish Dataset' }).should('be.disabled') }) @@ -161,7 +169,7 @@ describe('PublishDatasetMenu', () => { isValid: true }) - cy.customMount() + cy.mountAuthenticated() cy.findByRole('button', { name: 'Publish Dataset' }) .should('exist') diff --git a/tests/component/sections/dataset/dataset-action-buttons/submit-for-review-button/SubmitForReviewButton.spec.tsx b/tests/component/sections/dataset/dataset-action-buttons/submit-for-review-button/SubmitForReviewButton.spec.tsx index a41edda9f..69e9c1b34 100644 --- a/tests/component/sections/dataset/dataset-action-buttons/submit-for-review-button/SubmitForReviewButton.spec.tsx +++ b/tests/component/sections/dataset/dataset-action-buttons/submit-for-review-button/SubmitForReviewButton.spec.tsx @@ -5,7 +5,6 @@ import { DatasetPermissionsMother, DatasetVersionMother } from '../../../../dataset/domain/models/DatasetMother' -import { DatasetLockReason } from '../../../../../../src/dataset/domain/models/Dataset' describe('SubmitForReviewButton', () => { it('renders the SubmitForReviewButton if is dataset latest version and it is a draft and the dataset is not locked in workflow and the user has dataset update permissions and the user do not have publish dataset permissions', () => { @@ -20,11 +19,28 @@ describe('SubmitForReviewButton', () => { isValid: true }) - cy.customMount() + cy.mountAuthenticated() cy.findByRole('button', { name: 'Submit for Review' }).should('exist').should('be.enabled') }) + it('does not render if the user is not authenticated', () => { + const dataset = DatasetMother.create({ + version: DatasetVersionMother.createDraftAsLatestVersion(), + permissions: DatasetPermissionsMother.create({ + canUpdateDataset: true, + canPublishDataset: false + }), + locks: [], + hasValidTermsOfAccess: true, + isValid: true + }) + + cy.customMount() + + cy.findByRole('button', { name: 'Submit for Review' }).should('not.exist') + }) + it('does not render the SubmitForReviewButton if is not dataset latest version', () => { const dataset = DatasetMother.create({ version: DatasetVersionMother.createDraft(), @@ -35,7 +51,7 @@ describe('SubmitForReviewButton', () => { locks: [DatasetLockMother.createLockedInWorkflow()] }) - cy.customMount() + cy.mountAuthenticated() cy.findByRole('button', { name: 'Submit for Review' }).should('not.exist') }) @@ -50,7 +66,7 @@ describe('SubmitForReviewButton', () => { locks: [DatasetLockMother.createLockedInWorkflow()] }) - cy.customMount() + cy.mountAuthenticated() cy.findByRole('button', { name: 'Submit for Review' }).should('not.exist') }) @@ -65,7 +81,7 @@ describe('SubmitForReviewButton', () => { locks: [DatasetLockMother.createLockedInWorkflow()] }) - cy.customMount() + cy.mountAuthenticated() cy.findByRole('button', { name: 'Submit for Review' }).should('not.exist') }) @@ -80,7 +96,7 @@ describe('SubmitForReviewButton', () => { locks: [] }) - cy.customMount() + cy.mountAuthenticated() cy.findByRole('button', { name: 'Submit for Review' }).should('not.exist') }) @@ -95,7 +111,7 @@ describe('SubmitForReviewButton', () => { locks: [] }) - cy.customMount() + cy.mountAuthenticated() cy.findByRole('button', { name: 'Submit for Review' }).should('not.exist') }) @@ -107,17 +123,12 @@ describe('SubmitForReviewButton', () => { canUpdateDataset: true, canPublishDataset: false }), - locks: [ - { - id: 1, - reason: DatasetLockReason.EDIT_IN_PROGRESS - } - ], + locks: [DatasetLockMother.createLockedInEditInProgress()], hasValidTermsOfAccess: true, isValid: true }) - cy.customMount() + cy.mountAuthenticated() cy.findByRole('button', { name: 'Submit for Review' }).should('be.disabled') }) @@ -134,7 +145,7 @@ describe('SubmitForReviewButton', () => { isValid: true }) - cy.customMount() + cy.mountAuthenticated() cy.findByRole('button', { name: 'Submit for Review' }).should('be.disabled') }) @@ -151,7 +162,7 @@ describe('SubmitForReviewButton', () => { isValid: false }) - cy.customMount() + cy.mountAuthenticated() cy.findByRole('button', { name: 'Submit for Review' }).should('be.disabled') }) @@ -166,7 +177,7 @@ describe('SubmitForReviewButton', () => { locks: [DatasetLockMother.createLockedInReview()] }) - cy.customMount() + cy.mountAuthenticated() cy.findByRole('button', { name: 'Submitted for Review' }).should('be.disabled') }) diff --git a/tests/component/sections/dataset/dataset-alerts/DatasetAlerts.spec.tsx b/tests/component/sections/dataset/dataset-alerts/DatasetAlerts.spec.tsx index d70fd4977..0ccf2c504 100644 --- a/tests/component/sections/dataset/dataset-alerts/DatasetAlerts.spec.tsx +++ b/tests/component/sections/dataset/dataset-alerts/DatasetAlerts.spec.tsx @@ -1,15 +1,13 @@ import { DatasetAlerts } from '../../../../../src/sections/dataset/dataset-alerts/DatasetAlerts' import { faker } from '@faker-js/faker' -import { - DatasetAlert, - DatasetAlertMessageKey -} from '../../../../../src/dataset/domain/models/Dataset' import { DatasetMother, DatasetPermissionsMother, DatasetVersionMother } from '../../../dataset/domain/models/DatasetMother' +import { Alert, AlertMessageKey } from '../../../../../src/alert/domain/models/Alert' +import { AlertProvider } from '../../../../../src/sections/alerts/AlertProvider' function removeMarkup(htmlString: string): string { // Use a regular expression to match HTML tags and replace them with an empty string @@ -23,27 +21,37 @@ interface AlertTranslation { interface DatasetTranslation { alerts: { - [DatasetAlertMessageKey.DRAFT_VERSION]: AlertTranslation - [DatasetAlertMessageKey.REQUESTED_VERSION_NOT_FOUND]: AlertTranslation - [DatasetAlertMessageKey.REQUESTED_VERSION_NOT_FOUND_SHOW_DRAFT]: AlertTranslation - [DatasetAlertMessageKey.UNPUBLISHED_DATASET]: AlertTranslation - [DatasetAlertMessageKey.SHARE_UNPUBLISHED_DATASET]: AlertTranslation + [AlertMessageKey.DRAFT_VERSION]: AlertTranslation + [AlertMessageKey.REQUESTED_VERSION_NOT_FOUND]: AlertTranslation + [AlertMessageKey.REQUESTED_VERSION_NOT_FOUND_SHOW_DRAFT]: AlertTranslation + [AlertMessageKey.UNPUBLISHED_DATASET]: AlertTranslation + [AlertMessageKey.SHARE_UNPUBLISHED_DATASET]: AlertTranslation + [AlertMessageKey.METADATA_UPDATED]: AlertTranslation + [AlertMessageKey.FILES_UPDATED]: AlertTranslation + [AlertMessageKey.PUBLISH_IN_PROGRESS]: AlertTranslation + [AlertMessageKey.TERMS_UPDATED]: AlertTranslation + [AlertMessageKey.DATASET_DELETED]: AlertTranslation + [AlertMessageKey.THUMBNAIL_UPDATED]: AlertTranslation } } it('renders the correct number of alerts', () => { const alerts = [ - new DatasetAlert('warning', DatasetAlertMessageKey.DRAFT_VERSION), - new DatasetAlert('warning', DatasetAlertMessageKey.REQUESTED_VERSION_NOT_FOUND, { + new Alert('warning', AlertMessageKey.DRAFT_VERSION), + new Alert('warning', AlertMessageKey.REQUESTED_VERSION_NOT_FOUND, { requestedVersion: 4.0, returnedVersion: 2.0 }), - new DatasetAlert('info', DatasetAlertMessageKey.SHARE_UNPUBLISHED_DATASET, { + new Alert('info', AlertMessageKey.SHARE_UNPUBLISHED_DATASET, { privateUrl: faker.internet.url() }) ] cy.fixture('../../../public/locales/en/dataset.json').then((dataset: DatasetTranslation) => { - cy.mount() + cy.mount( + + + + ) const headingProps = [ dataset.alerts.draftVersion.heading, dataset.alerts.requestedVersionNotFound.heading, @@ -57,14 +65,17 @@ it('renders the correct number of alerts', () => { }) it('renders alerts with correct text', () => { - const draftAlert = new DatasetAlert('info', DatasetAlertMessageKey.DRAFT_VERSION) + const draftAlert = new Alert('info', AlertMessageKey.DRAFT_VERSION) const alerts = [draftAlert] cy.fixture('../../../public/locales/en/dataset.json').then((dataset: DatasetTranslation) => { - cy.mount() - - const alertHeading = dataset.alerts[draftAlert.message].heading - const alertText = removeMarkup(dataset.alerts[draftAlert.message].alertText) + cy.mount( + + + + ) + const alertHeading = dataset.alerts[draftAlert.messageKey].heading + const alertText = removeMarkup(dataset.alerts[draftAlert.messageKey].alertText) cy.findByText(alertHeading).should('exist') cy.findByRole('alert').should(($element) => { // text() removes markup, so we can compare to the expected text @@ -78,12 +89,17 @@ it('renders dynamic text', () => { requestedVersion: '4.0', returnedVersion: '2.0' } - const notFoundAlert = new DatasetAlert( + const notFoundAlert = new Alert( 'warning', - DatasetAlertMessageKey.REQUESTED_VERSION_NOT_FOUND, + AlertMessageKey.REQUESTED_VERSION_NOT_FOUND, dynamicFields ) - cy.mount() + cy.mount( + + + + ) + cy.findByRole('alert').should('contain.text', dynamicFields.requestedVersion) cy.findByRole('alert').should('contain.text', dynamicFields.returnedVersion) }) @@ -93,7 +109,11 @@ it('shows draft alert if version is DRAFT', () => { permissions: DatasetPermissionsMother.createWithPublishingDatasetAllowed() }) - cy.customMount() + cy.mount( + + + + ) cy.findByRole('alert').should('contain.text', 'draft') }) @@ -103,7 +123,11 @@ it('does not show draft alert if version is RELEASED', () => { permissions: DatasetPermissionsMother.createWithPublishingDatasetAllowed() }) - cy.customMount() + cy.mount( + + + + ) cy.findByRole('alert').should('not.exist') }) @@ -117,10 +141,14 @@ it('shows draft & share private url message if privateUrl exists and user can ed token: 'cd943c75-1cc7-4c1d-9717-98141d65d5cb' } }) - cy.customMount() + cy.mount( + + + + ) const expectedMessageKeys = [ - DatasetAlertMessageKey.DRAFT_VERSION, - DatasetAlertMessageKey.SHARE_UNPUBLISHED_DATASET + AlertMessageKey.DRAFT_VERSION, + AlertMessageKey.SHARE_UNPUBLISHED_DATASET ] cy.findAllByRole('alert').should('have.length', 2) cy.findAllByRole('alert').each(($alert, index) => { @@ -140,8 +168,12 @@ it('shows private url message only if privateUrl exists and user cannot edit', token: 'cd943c75-1cc7-4c1d-9717-98141d65d5cb' } }) - cy.customMount() - const expectedMessageKey = DatasetAlertMessageKey.UNPUBLISHED_DATASET + cy.mount( + + + + ) + const expectedMessageKey = AlertMessageKey.UNPUBLISHED_DATASET cy.findAllByRole('alert').should('have.length', 1) cy.findByRole('alert').then(($alert) => { diff --git a/tests/component/sections/dataset/dataset-files/DatasetFiles.spec.tsx b/tests/component/sections/dataset/dataset-files/DatasetFiles.spec.tsx index 32cd4f0fe..77c287336 100644 --- a/tests/component/sections/dataset/dataset-files/DatasetFiles.spec.tsx +++ b/tests/component/sections/dataset/dataset-files/DatasetFiles.spec.tsx @@ -98,7 +98,7 @@ describe('DatasetFiles', () => { cy.findByLabelText('Files per page').select('25') - cy.findByRole('columnheader', { name: '26 to 50 of 200 Files' }).should('exist') + cy.findByRole('columnheader', { name: '1 to 25 of 200 Files' }).should('exist') }) it('renders the files table with the correct header with a different page size ', () => { @@ -116,6 +116,81 @@ describe('DatasetFiles', () => { cy.findByRole('columnheader', { name: '101 to 150 of 200 Files' }).should('exist') }) + it('renders the first page if there is only one page and the user changes to a lower page size', () => { + const testFilesCountInfo = FilesCountInfoMother.create({ + total: 32 + }) + fileRepository.getAllByDatasetPersistentId = cy.stub().resolves(testFiles) + fileRepository.getFilesCountInfoByDatasetPersistentId = cy.stub().resolves(testFilesCountInfo) + fileRepository.getFilesTotalDownloadSizeByDatasetPersistentId = cy.stub().resolves(19900) + + cy.customMount( + + ) + + cy.findByRole('button', { name: '1' }).should('not.exist') + cy.findByRole('button', { name: '2' }).should('exist') + cy.findByRole('button', { name: '3' }).should('exist') + cy.findByRole('button', { name: '4' }).should('exist') + + cy.findByLabelText('Files per page').select('50') + + cy.findByRole('button', { name: '1' }).should('not.exist') + cy.findByRole('button', { name: '2' }).should('not.exist') + cy.findByRole('button', { name: '3' }).should('not.exist') + cy.findByRole('columnheader', { name: '1 to 32 of 32 Files' }).should('exist') + + cy.findByLabelText('Files per page').select('10') + + cy.findByRole('button', { name: '1' }).should('not.exist') + cy.findByRole('button', { name: '2' }).should('exist') + cy.findByRole('button', { name: '3' }).should('exist') + cy.findByRole('button', { name: '4' }).should('exist') + cy.findByRole('columnheader', { name: '1 to 10 of 32 Files' }).should('exist') + }) + + it('renders the page that includes the first element of the current page when changing the page size', () => { + const testFilesCountInfo = FilesCountInfoMother.create({ + total: 32 + }) + fileRepository.getAllByDatasetPersistentId = cy.stub().resolves(testFiles) + fileRepository.getFilesCountInfoByDatasetPersistentId = cy.stub().resolves(testFilesCountInfo) + fileRepository.getFilesTotalDownloadSizeByDatasetPersistentId = cy.stub().resolves(19900) + + cy.customMount( + + ) + + cy.findByRole('button', { name: '1' }).should('not.exist') + cy.findByRole('button', { name: '2' }).should('exist') + cy.findByRole('button', { name: '3' }).should('exist') + cy.findByRole('button', { name: '4' }).should('exist') + + cy.findByLabelText('Files per page').select('25') + + cy.findByRole('button', { name: '1' }).should('not.exist') + cy.findByRole('button', { name: '2' }).should('exist') + cy.findByRole('button', { name: '3' }).should('not.exist') + cy.findByRole('columnheader', { name: '1 to 25 of 32 Files' }).should('exist') + + cy.findByRole('button', { name: '2' }).click() + cy.findByLabelText('Files per page').select('10') + + cy.findByRole('button', { name: '1' }).should('exist') + cy.findByRole('button', { name: '2' }).should('exist') + cy.findByRole('button', { name: '3' }).should('not.exist') + cy.findByRole('button', { name: '4' }).should('exist') + cy.findByRole('columnheader', { name: '21 to 30 of 32 Files' }).should('exist') + }) + it('maintains the selection when the page changes', () => { cy.customMount( { + it('renders the modal when show is true', () => { + // Mount the component with show set to true + mount( {}} />) + + // Check if the modal title is present + cy.findByText('Not Implemented').should('exist') + + // Check if the modal body has specific content + cy.findByText('This feature is not implemented yet in SPA.').should('exist') + }) + + it('closes the modal when the Close button is clicked', () => { + // A spy to check if handleClose is called + const handleClose = cy.spy().as('handleClose') + + // Mount the component with the spy as the handleClose prop + mount() + + // Click the Close button + cy.findByText('Close').click() + + // Check if handleClose was called + cy.get('@handleClose').should('have.been.calledOnce') + }) +}) diff --git a/tests/component/users/domain/models/UserMother.ts b/tests/component/users/domain/models/UserMother.ts index 9c35f94e8..3d2052242 100644 --- a/tests/component/users/domain/models/UserMother.ts +++ b/tests/component/users/domain/models/UserMother.ts @@ -3,7 +3,8 @@ import { User } from '../../../../../src/users/domain/models/User' export class UserMother { static create(): User { return { - name: 'James D. Potts' + name: 'James D. Potts', + persistentId: 'jamesPotts' } } } diff --git a/tests/e2e-integration/e2e/sections/dataset/Dataset.spec.tsx b/tests/e2e-integration/e2e/sections/dataset/Dataset.spec.tsx index 96ef1d87b..5bb4b06b6 100644 --- a/tests/e2e-integration/e2e/sections/dataset/Dataset.spec.tsx +++ b/tests/e2e-integration/e2e/sections/dataset/Dataset.spec.tsx @@ -1,7 +1,8 @@ import { DatasetLabelValue } from '../../../../../src/dataset/domain/models/Dataset' import { TestsUtils } from '../../../shared/TestsUtils' -import { DatasetHelper } from '../../../shared/datasets/DatasetHelper' +import { DatasetHelper, DatasetResponse } from '../../../shared/datasets/DatasetHelper' import { FileHelper } from '../../../shared/files/FileHelper' +import moment from 'moment-timezone' type Dataset = { datasetVersion: { metadataBlocks: { citation: { fields: { value: string }[] } } } @@ -28,10 +29,38 @@ describe('Dataset', () => { name: dataset.datasetVersion.metadataBlocks.citation.fields[0].value }).should('exist') cy.findByText(DatasetLabelValue.DRAFT).should('exist') - // cy.findByText(DatasetLabelValue.UNPUBLISHED).should('exist') TODO - Implemnent isReleased property in js-dataverse to get the Unpublished label + cy.findByText(DatasetLabelValue.UNPUBLISHED).should('exist') cy.findByText('Metadata').should('exist') cy.findByText('Files').should('exist') + + cy.findByRole('button', { name: 'Edit Dataset' }).should('exist').click() + cy.findByRole('button', { name: 'Permissions' }).should('exist').click() + cy.findByRole('button', { name: 'Dataset' }).should('exist') + cy.findByRole('button', { name: 'Delete Dataset' }).should('exist') + cy.findByRole('button', { name: 'Publish Dataset' }).should('exist') + }) + }) + }) + + it('successfully loads a published dataset when the user is not authenticated', () => { + cy.wrap(DatasetHelper.create().then((dataset) => DatasetHelper.publish(dataset.persistentId))) + .its('persistentId') + .then((persistentId: string) => { + cy.wrap(TestsUtils.logout()) + cy.wait(1500) // Wait for the dataset to be published + cy.visit(`/spa/datasets?persistentId=${persistentId}`) + + cy.fixture('dataset-finch1.json').then((dataset: Dataset) => { + cy.findByRole('heading', { + name: dataset.datasetVersion.metadataBlocks.citation.fields[0].value + }).should('exist') + + cy.findByRole('button', { name: 'Edit Dataset' }).should('not.exist') + cy.findByRole('button', { name: 'Publish Dataset' }).should('not.exist') + cy.findByRole('button', { name: 'Upload Files' }).should('not.exist') + cy.findByText('Metadata').should('exist') + cy.findByText('Files').should('exist') }) }) }) @@ -51,7 +80,7 @@ describe('Dataset', () => { cy.wrap(DatasetHelper.create().then((dataset) => DatasetHelper.publish(dataset.persistentId))) .its('persistentId') .then((persistentId: string) => { - cy.wait(1500) + cy.wait(1500) // Wait for the dataset to be published cy.visit(`/spa/datasets?persistentId=${persistentId}&version=1.0`) cy.fixture('dataset-finch1.json').then((dataset: Dataset) => { @@ -59,7 +88,7 @@ describe('Dataset', () => { name: dataset.datasetVersion.metadataBlocks.citation.fields[0].value }).should('exist') cy.findByText(DatasetLabelValue.DRAFT).should('not.exist') - // cy.findByText(DatasetLabelValue.UNPUBLISHED).should('not.exist') TODO - Implemnent isReleased property in js-dataverse to get the Unpublished label + cy.findByText(DatasetLabelValue.UNPUBLISHED).should('not.exist') cy.findByText('Version 1.0').should('exist') }) }) @@ -99,7 +128,7 @@ describe('Dataset', () => { name: dataset.datasetVersion.metadataBlocks.citation.fields[0].value }).should('exist') cy.findByText(DatasetLabelValue.DRAFT).should('exist') - // cy.findByText(DatasetLabelValue.UNPUBLISHED).should('exist') TODO - Implemnent isReleased property in js-dataverse to get the Unpublished label + cy.findByText(DatasetLabelValue.UNPUBLISHED).should('exist') }) }) }) @@ -119,7 +148,7 @@ describe('Dataset', () => { name: dataset.datasetVersion.metadataBlocks.citation.fields[0].value }).should('exist') cy.findByText(DatasetLabelValue.DRAFT).should('exist') - // cy.findByText(DatasetLabelValue.UNPUBLISHED).should('exist') TODO - Implemnent isReleased property in js-dataverse to get the Unpublished label + cy.findByText(DatasetLabelValue.UNPUBLISHED).should('exist') cy.findAllByText('withheld').should('exist') }) @@ -127,7 +156,20 @@ describe('Dataset', () => { }) it.skip('successfully loads a dataset deaccessioned', () => { - // TODO - Add test when deaccessioned endpoint works + // TODO - Implement once the getDatasetCitation includes deaccessioned datasets + cy.wrap(DatasetHelper.create()) + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + .then((dataset) => Promise.all([dataset, DatasetHelper.publish(dataset.persistentId)])) + .then(([dataset]: [DatasetResponse]) => { + return cy + .wait(2500) + .then(() => Promise.all([dataset, DatasetHelper.deaccession(dataset.id)])) + }) + .then(([dataset]: [DatasetResponse]) => { + cy.visit(`/spa/datasets?persistentId=${dataset.persistentId}`) + + cy.findByText(DatasetLabelValue.DEACCESSIONED).should('exist') + }) }) }) @@ -265,39 +307,64 @@ describe('Dataset', () => { cy.findByText('Restricted with access Icon').should('not.exist') cy.findByText('Restricted File Icon').should('exist') - cy.findByRole('button', { name: 'Access File' }).should('exist').click() + // use alias below to avoid a timing error + cy.findByRole('button', { name: 'Access File' }).as('accessButton') + cy.get('@accessButton').should('exist') + cy.get('@accessButton').click() cy.findByText('Restricted').should('exist') }) }) it('loads the embargoed files', () => { - cy.wrap( - DatasetHelper.createWithFiles(FileHelper.createMany(1)).then((dataset) => - DatasetHelper.embargoFiles( - dataset.persistentId, - [dataset.files ? dataset.files[0].id : 0], - '2100-10-20' + cy.window().then((win) => { + // Get the browser's locale from the window object + const browserLocale = win.navigator.language + + // Create a moment object in UTC and set the time to 12 AM (midnight) + const utcDate = moment.utc().startOf('day') + + // Add 100 years to the UTC date + utcDate.add(100, 'years') + const dateString = utcDate.format('YYYY-MM-DD') + + // Use the browser's locale to format the date using Intl.DateTimeFormat + const options: Intl.DateTimeFormatOptions = { + year: 'numeric', + month: 'short', + day: 'numeric' + } + const expectedDate = new Intl.DateTimeFormat(browserLocale, options).format( + utcDate.toDate() + ) + + cy.wrap( + DatasetHelper.createWithFiles(FileHelper.createMany(1)).then((dataset) => + DatasetHelper.embargoFiles( + dataset.persistentId, + [dataset.files ? dataset.files[0].id : 0], + dateString + ) ) ) - ) - .its('persistentId') - .then((persistentId: string) => { - cy.wait(1500) // Wait for the files to be embargoed + .its('persistentId') + .then((persistentId: string) => { + cy.wait(1500) // Wait for the files to be embargoed - cy.visit(`/spa/datasets?persistentId=${persistentId}`) + cy.visit(`/spa/datasets?persistentId=${persistentId}`) - cy.wait(1500) // Wait for the files to be loaded + cy.wait(1500) // Wait for the files to be loaded - cy.findByText('Files').should('exist') + cy.findByText('Files').should('exist') - cy.findByText(/Deposited/).should('exist') - cy.findByText('Draft: will be embargoed until Oct 20, 2100').should('exist') + cy.findByText(/Deposited/).should('exist') + cy.findByText(`Draft: will be embargoed until ${expectedDate}`).should('exist') - cy.findByText('Edit Files').should('exist') + cy.findByText('Edit Files').should('exist') - cy.findByRole('button', { name: 'Access File' }).should('exist').click() - cy.findByText('Embargoed').should('exist') - }) + cy.findByRole('button', { name: 'Access File' }).should('exist').click() + cy.findByText('Embargoed').should('exist') + }) + }) }) it('applies filters to the Files Table in the correct order', () => { diff --git a/tests/e2e-integration/integration/datasets/DatasetJSDataverseRepository.spec.ts b/tests/e2e-integration/integration/datasets/DatasetJSDataverseRepository.spec.ts index 65f01dbfc..1aa52c69a 100644 --- a/tests/e2e-integration/integration/datasets/DatasetJSDataverseRepository.spec.ts +++ b/tests/e2e-integration/integration/datasets/DatasetJSDataverseRepository.spec.ts @@ -3,6 +3,7 @@ import chaiAsPromised from 'chai-as-promised' import { DatasetJSDataverseRepository } from '../../../../src/dataset/infrastructure/repositories/DatasetJSDataverseRepository' import { TestsUtils } from '../../shared/TestsUtils' import { + DatasetLockReason, DatasetPublishingStatus, DatasetVersion } from '../../../../src/dataset/domain/models/Dataset' @@ -80,7 +81,16 @@ const datasetData = (persistentId: string, versionId: number) => { latestVersionStatus: 'draft', isLatest: true, isInReview: false - } + }, + permissions: { + canDownloadFiles: true, + canUpdateDataset: true, + canPublishDataset: true, + canManageDatasetPermissions: true, + canManageFilesPermissions: true, + canDeleteDataset: true + }, + locks: [] } } @@ -100,22 +110,26 @@ describe('Dataset JSDataverse Repository', () => { expect(dataset.getTitle()).to.deep.equal(datasetExpected.title) expect(dataset.citation).to.deep.equal(datasetExpected.citation) - // expect(dataset.labels).to.deep.equal(datasetExpected.labels) TODO - Implemnent isReleased property in js-dataverse to get the Unpublished label + expect(dataset.labels).to.deep.equal(datasetExpected.labels) expect(dataset.license).to.deep.equal(datasetExpected.license) 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 + expect(dataset.permissions).to.deep.equal(datasetExpected.permissions) + expect(dataset.locks).to.deep.equal(datasetExpected.locks) }) }) - it('gets the dataset by persistentId and version number', async () => { + it('gets a published dataset by persistentId without user authentication', async () => { const datasetResponse = await DatasetHelper.create() await DatasetHelper.publish(datasetResponse.persistentId) await TestsUtils.wait(1500) + await TestsUtils.logout() + await datasetRepository .getByPersistentId(datasetResponse.persistentId, '1.0') .then((dataset) => { @@ -139,6 +153,45 @@ describe('Dataset JSDataverse Repository', () => { expectedPublicationDate ) expect(dataset.metadataBlocks[0].fields.citationDate).not.to.exist + expect(dataset.permissions).to.deep.equal({ + canDownloadFiles: true, + canUpdateDataset: false, + canPublishDataset: false, + canManageDatasetPermissions: false, + canManageFilesPermissions: true, + canDeleteDataset: false + }) + }) + }) + + it('gets the dataset by persistentId and version number', async () => { + const datasetResponse = await DatasetHelper.create() + await DatasetHelper.publish(datasetResponse.persistentId) + await TestsUtils.waitForNoLocks(datasetResponse.persistentId) + await datasetRepository + .getByPersistentId(datasetResponse.persistentId, '1.0') + .then((dataset) => { + if (!dataset) { + throw new Error('Dataset not found') + } + const datasetExpected = datasetData(dataset.persistentId, dataset.version.id) + const newVersion = new DatasetVersion( + dataset.version.id, + DatasetPublishingStatus.RELEASED, + true, + false, + DatasetPublishingStatus.RELEASED, + 1, + 0 + ) + 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 + expect(dataset.permissions).to.deep.equal(datasetExpected.permissions) }) }) @@ -170,6 +223,7 @@ describe('Dataset JSDataverse Repository', () => { expect(dataset.getTitle()).to.deep.equal(datasetExpected.title) expect(dataset.version).to.deep.equal(datasetExpected.version) + expect(dataset.permissions).to.deep.equal(datasetExpected.permissions) }) }) @@ -177,7 +231,7 @@ describe('Dataset JSDataverse Repository', () => { const datasetResponse = await DatasetHelper.create() await DatasetHelper.publish(datasetResponse.persistentId) - await TestsUtils.wait(1500) + await TestsUtils.waitForNoLocks(datasetResponse.persistentId) await DatasetHelper.setCitationDateFieldType(datasetResponse.persistentId, 'dateOfDeposit') @@ -194,4 +248,41 @@ describe('Dataset JSDataverse Repository', () => { expect(dataset.metadataBlocks[0].fields.citationDate).not.to.exist }) }) + + it.skip('gets the dataset by persistentId when the dataset is deaccessioned', async () => { + // TODO - Implement once the getDatasetCitation includes deaccessioned datasets + const datasetResponse = await DatasetHelper.create() + + await DatasetHelper.publish(datasetResponse.persistentId) + await TestsUtils.wait(1500) + + await DatasetHelper.deaccession(datasetResponse.id) + await datasetRepository.getByPersistentId(datasetResponse.persistentId).then((dataset) => { + if (!dataset) { + throw new Error('Dataset not found') + } + const datasetExpected = datasetData(dataset.persistentId, dataset.version.id) + + expect(dataset.getTitle()).to.deep.equal(datasetExpected.title) + }) + }) + it('gets the dataset by persistentId when is locked', async () => { + const datasetResponse = await DatasetHelper.create() + await DatasetHelper.lock(datasetResponse.id, DatasetLockReason.FINALIZE_PUBLICATION) + + await datasetRepository.getByPersistentId(datasetResponse.persistentId).then((dataset) => { + if (!dataset) { + throw new Error('Dataset not found') + } + const datasetExpected = datasetData(dataset.persistentId, dataset.version.id) + + expect(dataset.getTitle()).to.deep.equal(datasetExpected.title) + expect(dataset.locks).to.deep.equal([ + { + userPersistentId: 'dataverseAdmin', + reason: DatasetLockReason.FINALIZE_PUBLICATION + } + ]) + }) + }) }) diff --git a/tests/e2e-integration/integration/files/FileJSDataverseRepository.spec.ts b/tests/e2e-integration/integration/files/FileJSDataverseRepository.spec.ts index e2513adf0..df8d92afe 100644 --- a/tests/e2e-integration/integration/files/FileJSDataverseRepository.spec.ts +++ b/tests/e2e-integration/integration/files/FileJSDataverseRepository.spec.ts @@ -109,6 +109,7 @@ describe('File JSDataverse Repository', () => { expect(file.tabularData).to.deep.equal(expectedFile.tabularData) expect(file.description).to.deep.equal(expectedFile.description) expect(file.downloadUrls).to.deep.equal(expectedFile.downloadUrls) + expect(file.isDeleted).to.deep.equal(expectedFile.isDeleted) }) }) }) @@ -138,7 +139,7 @@ describe('File JSDataverse Repository', () => { if (!dataset) throw new Error('Dataset not found') await DatasetHelper.publish(dataset.persistentId) - await TestsUtils.wait(1500) // Wait for the dataset to be published + await TestsUtils.waitForNoLocks(dataset.persistentId) // Wait for the dataset to be published await fileRepository .getAllByDatasetPersistentId( @@ -166,18 +167,15 @@ describe('File JSDataverse Repository', () => { }) it('gets all the files by dataset persistentId after dataset deaccession', async () => { - const dataset = await DatasetHelper.createWithFiles(FileHelper.createMany(3)).then( - (datasetResponse) => datasetRepository.getByPersistentId(datasetResponse.persistentId) - ) - if (!dataset) throw new Error('Dataset not found') + const datasetResponse = await DatasetHelper.createWithFiles(FileHelper.createMany(3)) - await DatasetHelper.publish(dataset.persistentId) - await TestsUtils.wait(1500) // Wait for the dataset to be published + await DatasetHelper.publish(datasetResponse.persistentId) + await TestsUtils.waitForNoLocks(datasetResponse.persistentId) // Wait for the dataset to be published + + const dataset = await datasetRepository.getByPersistentId(datasetResponse.persistentId) + if (!dataset) throw new Error('Dataset not found') - DatasetHelper.deaccession(dataset.persistentId) - await TestsUtils.wait(1500) // Wait for the dataset to be deaccessioned - await TestsUtils.wait(1500) // Wait for the dataset to be deaccessioned - await TestsUtils.wait(1500) // Wait for the dataset to be deaccessioned + await DatasetHelper.deaccession(datasetResponse.id) await fileRepository .getAllByDatasetPersistentId( @@ -207,7 +205,7 @@ describe('File JSDataverse Repository', () => { if (!datasetResponse.files) throw new Error('Files not found') await DatasetHelper.publish(datasetResponse.persistentId) - await TestsUtils.wait(1500) // Wait for the dataset to be published + await TestsUtils.waitForNoLocks(datasetResponse.persistentId) // Wait for the dataset to be published const dataset = await datasetRepository.getByPersistentId(datasetResponse.persistentId) if (!dataset) throw new Error('Dataset not found') @@ -246,8 +244,7 @@ describe('File JSDataverse Repository', () => { it('gets all the files by dataset persistentId after adding tag labels to the files', async () => { const datasetResponse = await DatasetHelper.createWithFiles(FileHelper.createMany(1, 'csv')) if (!datasetResponse.files) throw new Error('Files not found') - await TestsUtils.wait(1500) // Wait for the tabular data to be ingested - + await TestsUtils.waitForNoLocks(datasetResponse.persistentId) // Wait for the tabular data to be ingested const dataset = await datasetRepository.getByPersistentId(datasetResponse.persistentId) if (!dataset) throw new Error('Dataset not found') @@ -290,7 +287,7 @@ describe('File JSDataverse Repository', () => { [datasetResponse.files[0].id, datasetResponse.files[1].id, datasetResponse.files[2].id], embargoDate ) - await TestsUtils.wait(1500) // Wait for the files to be embargoed + await TestsUtils.waitForNoLocks(datasetResponse.persistentId) // Wait for the files to be embargoed await fileRepository .getAllByDatasetPersistentId(dataset.persistentId, dataset.version) @@ -300,8 +297,7 @@ describe('File JSDataverse Repository', () => { }) }) - it.skip('gets all the files by dataset persistentId when files are tabular data', async () => { - // TODO - Implement this when isTabularData flag is added to js-dataverse response + it('gets all the files by dataset persistentId when files are tabular data', async () => { const datasetResponse = await DatasetHelper.createWithFiles(FileHelper.createMany(1, 'csv')) if (!datasetResponse.files) throw new Error('Files not found') @@ -312,12 +308,17 @@ describe('File JSDataverse Repository', () => { .getAllByDatasetPersistentId(dataset.persistentId, dataset.version) .then((files) => { const expectedTabularData = { - variablesCount: 1, - observationsCount: 0, - unf: 'some' + variablesCount: 7, + observationsCount: 10 } files.forEach((file) => { - expect(file.tabularData).to.deep.equal(expectedTabularData) + expect(file.tabularData?.variablesCount).to.deep.equal( + expectedTabularData.variablesCount + ) + expect(file.tabularData?.observationsCount).to.deep.equal( + expectedTabularData.observationsCount + ) + expect(file.tabularData?.unf).to.not.be.undefined }) }) }) @@ -445,6 +446,29 @@ describe('File JSDataverse Repository', () => { expect(files.length).to.equal(1) }) }) + + it.skip('gets all the files when they are deleted', async () => { + // This test is failing because js-dataverse deleted property always returns undefined + // TODO: Remove the skip once the issue is fixed + const datasetResponse = await DatasetHelper.createWithFiles(FileHelper.createMany(1)) + + await DatasetHelper.publish(datasetResponse.persistentId) + await TestsUtils.wait(2000) // Wait for the dataset to be published + + const dataset = await datasetRepository.getByPersistentId(datasetResponse.persistentId) + if (!dataset) throw new Error('Dataset not found') + + if (!datasetResponse.files) throw new Error('Files not found') + datasetResponse.files.map((file) => FileHelper.delete(file.id)) + + await fileRepository + .getAllByDatasetPersistentId(dataset.persistentId, dataset.version) + .then((files) => { + files.forEach((file) => { + expect(file.isDeleted).to.equal(true) + }) + }) + }) }) describe('Get file user permissions by id', () => { @@ -515,11 +539,11 @@ describe('File JSDataverse Repository', () => { total: 6, perAccess: [ { - access: FileAccessOption.RESTRICTED, + access: FileAccessOption.PUBLIC, count: 3 }, { - access: FileAccessOption.PUBLIC, + access: FileAccessOption.RESTRICTED, count: 3 } ], @@ -681,7 +705,7 @@ describe('File JSDataverse Repository', () => { ) if (!dataset) throw new Error('Dataset not found') - await TestsUtils.wait(2500) // wait for the files to be ingested + await TestsUtils.waitForNoLocks(dataset.persistentId) // wait for the files to be ingested const expectedTotalDownloadSize = await fileRepository .getAllByDatasetPersistentId(dataset.persistentId, dataset.version) @@ -719,7 +743,7 @@ describe('File JSDataverse Repository', () => { ) if (!dataset) throw new Error('Dataset not found') - await TestsUtils.wait(2500) // wait for the files to be ingested + await TestsUtils.waitForNoLocks(dataset.persistentId) // wait for the files to be ingested const expectedTotalDownloadSize = await fileRepository .getAllByDatasetPersistentId( diff --git a/tests/e2e-integration/integration/users/infrastructure/repositories/UserJSDataverseRepository.spec.ts b/tests/e2e-integration/integration/users/infrastructure/repositories/UserJSDataverseRepository.spec.ts index ac4100e3c..efd036c88 100644 --- a/tests/e2e-integration/integration/users/infrastructure/repositories/UserJSDataverseRepository.spec.ts +++ b/tests/e2e-integration/integration/users/infrastructure/repositories/UserJSDataverseRepository.spec.ts @@ -12,7 +12,7 @@ describe('User JSDataverse Repository', () => { beforeEach(() => TestsUtils.login()) it('gets the authenticated user', async () => { - const expectedUser = { name: 'Dataverse Admin' } + const expectedUser = { name: 'Dataverse Admin', persistentId: 'dataverseAdmin' } const user = await userRepository.getAuthenticated() expect(user).to.deep.equal(expectedUser) diff --git a/tests/e2e-integration/shared/TestsUtils.ts b/tests/e2e-integration/shared/TestsUtils.ts index 1ad0c0bdb..f6508dcfd 100644 --- a/tests/e2e-integration/shared/TestsUtils.ts +++ b/tests/e2e-integration/shared/TestsUtils.ts @@ -2,6 +2,7 @@ import { ApiConfig } from '@iqss/dataverse-client-javascript/dist/core' import { DataverseApiHelper } from './DataverseApiHelper' import { DataverseApiAuthMechanism } from '@iqss/dataverse-client-javascript/dist/core/infra/repositories/ApiConfig' import { UserJSDataverseRepository } from '../../../src/users/infrastructure/repositories/UserJSDataverseRepository' +import { DatasetHelper } from './datasets/DatasetHelper' export class TestsUtils { static readonly DATAVERSE_BACKEND_URL = @@ -25,4 +26,33 @@ export class TestsUtils { static logout() { return new UserJSDataverseRepository().removeAuthenticated() } + + static async waitForNoLocks(persistentId: string, maxRetries = 20, delay = 1000): Promise { + await this.checkForLocks(persistentId, maxRetries, delay) + } + + private static async checkForLocks( + persistentId: string, + maxRetries: number, + delay: number + ): Promise { + let retry = 0 + + while (retry < maxRetries) { + const response = await DatasetHelper.getLocks(persistentId) + console.log('Checking locks: ' + JSON.stringify(response)) + + // The response will have a single key if there are no locks + if (Object.keys(response).length === 1) { + console.log('No locks found.') + return + } + + retry++ + await this.wait(delay) + } + + console.log('Max retries reached.') + throw new Error('Max retries reached.') + } } diff --git a/tests/e2e-integration/shared/datasets/DatasetHelper.ts b/tests/e2e-integration/shared/datasets/DatasetHelper.ts index 32143ec12..e46519395 100644 --- a/tests/e2e-integration/shared/datasets/DatasetHelper.ts +++ b/tests/e2e-integration/shared/datasets/DatasetHelper.ts @@ -1,6 +1,7 @@ import newDatasetData from '../../fixtures/dataset-finch1.json' import { DataverseApiHelper } from '../DataverseApiHelper' import { FileData } from '../files/FileHelper' +import { DatasetLockReason } from '../../../../src/dataset/domain/models/Dataset' export interface DatasetResponse { persistentId: string @@ -17,45 +18,54 @@ export class DatasetHelper extends DataverseApiHelper { return this.request(`/dataverses/root/datasets`, 'POST', newDatasetData) } - static async publish(persistentId: string): Promise<{ status: string; persistentId: string }> { - const response = await this.request<{ status: string }>( - `/datasets/:persistentId/actions/:publish?persistentId=${persistentId}&type=major`, - 'POST' - ) + static async publish(persistentId: string): Promise<{ + status: string + persistentId: string + }> { + const response = await this.request<{ + status: string + }>(`/datasets/:persistentId/actions/:publish?persistentId=${persistentId}&type=major`, 'POST') return { ...response, persistentId } } - static deaccession(persistentId: string) { - return cy - .visit(`/dataset.xhtml?persistentId=${persistentId}`) - .get('#editDataSet') - .click() - .get('#datasetForm\\:deaccessionDatasetLink') - .click() - .get('#datasetForm\\:reasonOptions_label') - .click() - .get('#datasetForm\\:reasonOptions_2') - .click() - .get('#datasetForm\\:reasonForDeaccession') - .type('Test deaccession') - .get('#datasetForm\\:j_idt2181') - .click() - .get('#datasetForm\\:deaccessionConfirmation_content > div > input') - .click() - } + static async getLocks(persistentId: string): Promise<{ + status: string + persistentId: string + }> { + const response = await this.request<{ + status: string + }>(`/datasets/:persistentId/locks?persistentId=${persistentId}`, 'GET') - static async createPrivateUrl(id: string): Promise<{ token: string }> { - return this.request<{ token: string }>(`/datasets/${id}/privateUrl`, 'POST') + return { ...response, persistentId } } - static async createPrivateUrlAnonymized(id: string): Promise<{ token: string }> { - return this.request<{ token: string }>( - `/datasets/${id}/privateUrl?anonymizedAccess=true`, - 'POST' + static deaccession(id: string) { + return this.request<{ status: string }>( + `/datasets/${id}/versions/:latest-published/deaccession`, + 'POST', + { + deaccessionReason: 'Description of the deaccession reason.' + } ) } + static async createPrivateUrl(id: string): Promise<{ + token: string + }> { + return this.request<{ + token: string + }>(`/datasets/${id}/privateUrl`, 'POST') + } + + static async createPrivateUrlAnonymized(id: string): Promise<{ + token: string + }> { + return this.request<{ + token: string + }>(`/datasets/${id}/privateUrl?anonymizedAccess=true`, 'POST') + } + static async createWithFiles(filesData: FileData[]): Promise { const datasetResponse = await this.create() const files = await this.uploadFiles(datasetResponse.persistentId, filesData) @@ -92,7 +102,15 @@ export class DatasetHelper extends DataverseApiHelper { datasetPersistentId: string, fileData: FileData ): Promise { - const { files } = await this.request<{ files: [{ dataFile: { id: number } }] }>( + const { files } = await this.request<{ + files: [ + { + dataFile: { + id: number + } + } + ] + }>( `/datasets/:persistentId/add?persistentId=${datasetPersistentId}`, 'POST', fileData, @@ -108,12 +126,27 @@ export class DatasetHelper extends DataverseApiHelper { static async setCitationDateFieldType( persistentId: string, fieldType: string - ): Promise<{ status: string }> { - return this.request<{ status: string }>( + ): Promise<{ + status: string + }> { + return this.request<{ + status: string + }>( `/datasets/:persistentId/citationdate?persistentId=${persistentId}`, 'PUT', fieldType, 'text/plain' ) } + + static async lock( + id: string, + reason: DatasetLockReason + ): Promise<{ + status: string + }> { + return this.request<{ + status: string + }>(`/datasets/${id}/lock/${reason}`, 'POST') + } } diff --git a/tests/e2e-integration/shared/files/FileHelper.ts b/tests/e2e-integration/shared/files/FileHelper.ts index 8c29b7fb7..7ae2c1b33 100644 --- a/tests/e2e-integration/shared/files/FileHelper.ts +++ b/tests/e2e-integration/shared/files/FileHelper.ts @@ -152,4 +152,8 @@ export class FileHelper extends DataverseApiHelper { 'multipart/form-data' ) } + + static async delete(id: number) { + return this.request(`/files/${id}`, 'DELETE') + } }