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/package-lock.json b/package-lock.json index 2abed2bfe..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-pr98.f3ad648", + "@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-pr98.f3ad648", - "resolved": "https://npm.pkg.github.com/download/@IQSS/dataverse-client-javascript/2.0.0-pr98.f3ad648/7d7f41216e9f534a2382f4ed27534c4b33819af1", - "integrity": "sha512-Okb8Q9aEG+byT2XT1DrRCQsg9f0Si/zWkWLE6FWqQw2pUc/H7gQAfAuoyoYEAHKVTylJKHBFUH5HHP2BhXAlpA==", + "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 ceba7e213..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-pr98.f3ad648", + "@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/public/locales/en/files.json b/public/locales/en/files.json index 3e8938625..98fc2b0bb 100644 --- a/public/locales/en/files.json +++ b/public/locales/en/files.json @@ -12,6 +12,7 @@ "pageSize": "Files per page" }, "tabularData": { + "name": "Tabular Data", "variables": "Variables", "observations": "Observations" }, @@ -112,6 +113,14 @@ "title": "Access File", "headers": { "fileAccess": "File Access" + }, + "downloadOptions": { + "title": "Download Options", + "options": { + "original": "Original File Format", + "RData": "R Data", + "tabular": "Tab-Delimited" + } } }, "editFilesMenu": { 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 409a7ab5e..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,19 +298,56 @@ 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) } + public get isLockedFromFileDownload(): boolean { + if (!this.isLocked) { + return false + } + + if ( + this.locks.some((lock) => + [ + DatasetLockReason.FINALIZE_PUBLICATION, + DatasetLockReason.DCM_UPLOAD, + DatasetLockReason.INGEST + ].includes(lock.reason) + ) + ) { + return true + } + + if ( + this.locks.some((lock) => lock.reason === DatasetLockReason.IN_REVIEW) && + !this.permissions.canUpdateDataset + ) { + return true + } + + // If the lock reason is workflow and the workflow userId is different than the current user, then is locked + // TODO - Ask how we want to manage pending workflows + + return false + } + static Builder = class { public readonly labels: DatasetLabel[] = [] - public readonly alerts: DatasetAlert[] = [] + public readonly alerts: Alert[] = [] constructor( public readonly persistentId: string, @@ -398,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) { @@ -407,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 ) ) @@ -430,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 4b4e49fb8..be6f7ce1c 100644 --- a/src/dataset/infrastructure/mappers/JSDatasetMapper.ts +++ b/src/dataset/infrastructure/mappers/JSDatasetMapper.ts @@ -4,7 +4,8 @@ import { DatasetMetadataBlocks as JSDatasetMetadataBlocks, DatasetMetadataFields as JSDatasetMetadataFields, DatasetVersionInfo as JSDatasetVersionInfo, - DatasetUserPermissions as JSDatasetPermissions + 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 { @@ -16,6 +17,8 @@ import { DatasetVersion, MetadataBlockName, DatasetPermissions, + DatasetLock, + DatasetLockReason, PrivateUrl } from '../../domain/models/Dataset' @@ -25,6 +28,7 @@ export class JSDatasetMapper { citation: string, summaryFieldsNames: string[], jsDatasetPermissions: JSDatasetPermissions, + jsDatasetLocks: JSDatasetLock[], requestedVersion?: string, privateUrl?: PrivateUrl ): Dataset { @@ -41,7 +45,7 @@ export class JSDatasetMapper { jsDataset.citationDate ), JSDatasetMapper.toDatasetPermissions(jsDatasetPermissions), - [], // TODO Connect with dataset locks + JSDatasetMapper.toLocks(jsDatasetLocks), true, // TODO Connect with dataset hasValidTermsOfAccess true, // TODO Connect with dataset isValid JSDatasetMapper.toIsReleased(jsDataset.versionInfo), @@ -198,4 +202,12 @@ export class JSDatasetMapper { 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 537e753f0..2b624ca35 100644 --- a/src/dataset/infrastructure/repositories/DatasetJSDataverseRepository.ts +++ b/src/dataset/infrastructure/repositories/DatasetJSDataverseRepository.ts @@ -9,7 +9,9 @@ import { getPrivateUrlDataset, getPrivateUrlDatasetCitation, getDatasetUserPermissions, - ReadError + ReadError, + getDatasetLocks, + DatasetLock as JSDatasetLock } from '@iqss/dataverse-client-javascript' import { JSDatasetMapper } from '../mappers/JSDatasetMapper' @@ -26,21 +28,24 @@ export class DatasetJSDataverseRepository implements DatasetRepository { jsDataset, getDatasetSummaryFieldNames.execute(), getDatasetCitation.execute(jsDataset.id, this.versionToVersionId(version)), - getDatasetUserPermissions.execute(jsDataset.id) + getDatasetUserPermissions.execute(jsDataset.id), + getDatasetLocks.execute(jsDataset.id) ]) ) .then( - ([jsDataset, summaryFieldsNames, citation, jsDatasetPermissions]: [ + ([jsDataset, summaryFieldsNames, citation, jsDatasetPermissions, jsDatasetLocks]: [ JSDataset, string[], string, - JSDatasetPermissions + JSDatasetPermissions, + JSDatasetLock[] ]) => JSDatasetMapper.toDataset( jsDataset, citation, summaryFieldsNames, jsDatasetPermissions, + jsDatasetLocks, requestedVersion ) ) @@ -58,16 +63,21 @@ export class DatasetJSDataverseRepository implements DatasetRepository { getDatasetSummaryFieldNames.execute(), getPrivateUrlDatasetCitation.execute(privateUrlToken) ]) - .then( - ([jsDataset, summaryFieldsNames, citation]: [JSDataset, string[], string]) => - JSDatasetMapper.toDataset(jsDataset, citation, summaryFieldsNames, { + .then(([jsDataset, summaryFieldsNames, citation]: [JSDataset, string[], string]) => + JSDatasetMapper.toDataset( + jsDataset, + citation, + summaryFieldsNames, + { canEditDataset: true, canPublishDataset: true, canManageDatasetPermissions: true, canDeleteDatasetDraft: true, canViewUnpublishedDataset: true - }) // TODO Connect with JS dataset permissions - ) + }, + [] + ) + ) // 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 0be9f000c..b6f685c75 100644 --- a/src/files/domain/models/File.ts +++ b/src/files/domain/models/File.ts @@ -121,10 +121,10 @@ export interface FileLabel { } export class FileType { - constructor(readonly value: string) {} + constructor(readonly value: string, readonly original?: string) {} toDisplayFormat(): string { - return FileTypeToFriendlyTypeMap[this.value] || this.value + return FileTypeToFriendlyTypeMap[this.value] || FileTypeToFriendlyTypeMap.unknown } } diff --git a/src/files/domain/models/FileTypeToFriendlyTypeMap.ts b/src/files/domain/models/FileTypeToFriendlyTypeMap.ts index ab4d9b30e..6f40517aa 100644 --- a/src/files/domain/models/FileTypeToFriendlyTypeMap.ts +++ b/src/files/domain/models/FileTypeToFriendlyTypeMap.ts @@ -221,7 +221,8 @@ const MimeTypeDisplay: Record = { 'application/x-docker-file': 'Docker Image File', 'application/x-vagrant-file': 'Vagrant Image File', // Dataverse-specific - 'application/vnd.dataverse.file-package': 'Dataverse Package' + 'application/vnd.dataverse.file-package': 'Dataverse Package', + unknown: 'Unknown' } export default MimeTypeDisplay 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 316c5019c..3956b6d25 100644 --- a/src/files/infrastructure/mappers/JSFileMapper.ts +++ b/src/files/infrastructure/mappers/JSFileMapper.ts @@ -41,7 +41,7 @@ export class JSFileMapper { this.toFileVersion(jsFile.version, datasetVersion, jsFile.publicationDate), this.toFileName(jsFile.name), this.toFileAccess(jsFile.restricted), - this.toFileType(jsFile.contentType), + this.toFileType(jsFile.contentType, jsFile.originalFormatLabel), this.toFileSize(jsFile.sizeBytes), this.toFileDate(jsFile.creationDate, jsFile.publicationDate, jsFile.embargo), this.toFileDownloads(), @@ -107,8 +107,8 @@ export class JSFileMapper { } } - static toFileType(jsFileContentType: string): FileType { - return new FileType(jsFileContentType) + static toFileType(jsFileContentType: string, jsOriginalFormatLabel?: string): FileType { + return new FileType(jsFileContentType, jsOriginalFormatLabel) } static toFileSize(jsFileSize: number): FileSize { 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/DatasetFactory.tsx b/src/sections/dataset/DatasetFactory.tsx index 1ac3e1e15..a256f892b 100644 --- a/src/sections/dataset/DatasetFactory.tsx +++ b/src/sections/dataset/DatasetFactory.tsx @@ -11,6 +11,7 @@ import { SettingJSDataverseRepository } from '../../settings/infrastructure/Sett import { FilePermissionsProvider } from '../file/file-permissions/FilePermissionsProvider' import { SettingsProvider } from '../settings/SettingsProvider' import { DatasetProvider } from './DatasetProvider' +import { AlertProvider } from '../alerts/AlertProvider' const datasetRepository = new DatasetJSDataverseRepository() const fileRepository = new FileJSDataverseRepository() @@ -24,7 +25,9 @@ export class DatasetFactory { - + + + 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..798e2d451 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,13 +4,16 @@ import { EditDatasetPermissionsMenu } from './EditDatasetPermissionsMenu' import { DeleteDatasetButton } from './DeleteDatasetButton' import { DeaccessionDatasetButton } from './DeaccessionDatasetButton' import { useTranslation } from 'react-i18next' +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 <> } @@ -21,7 +24,7 @@ export function EditDatasetMenu({ dataset }: EditDatasetMenuProps) { title={t('datasetActionButtons.editDataset.title')} asButtonGroup variant="secondary" - disabled={dataset.isLockedFromEdits}> + disabled={dataset.checkIsLockedFromEdits(user.persistentId)}> {t('datasetActionButtons.editDataset.filesUpload')} 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..ac7f9cc0d 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,15 +2,18 @@ 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 { 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 <> @@ -24,7 +27,9 @@ export function PublishDatasetMenu({ dataset }: PublishDatasetMenuProps) { asButtonGroup variant="secondary" disabled={ - dataset.isLockedFromPublishing || !dataset.hasValidTermsOfAccess || !dataset.isValid + dataset.checkIsLockedFromPublishing(user.persistentId) || + !dataset.hasValidTermsOfAccess || + !dataset.isValid }> {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/FileCriteriaFilterByType.tsx b/src/sections/dataset/dataset-files/file-criteria-form/FileCriteriaFilterByType.tsx index cf18e51da..ff1bd0d61 100644 --- a/src/sections/dataset/dataset-files/file-criteria-form/FileCriteriaFilterByType.tsx +++ b/src/sections/dataset/dataset-files/file-criteria-form/FileCriteriaFilterByType.tsx @@ -41,7 +41,9 @@ export function FileCriteriaFilterByType({ return ( 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/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/file-actions-cell/file-action-buttons/access-file-menu/AccessFileMenu.tsx b/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/access-file-menu/AccessFileMenu.tsx index 72a53597a..98f157ef9 100644 --- a/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/access-file-menu/AccessFileMenu.tsx +++ b/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/access-file-menu/AccessFileMenu.tsx @@ -4,6 +4,7 @@ import { AccessStatus } from './AccessStatus' import { RequestAccessOption } from './RequestAccessOption' import { DropdownButton, DropdownHeader, Tooltip } from '@iqss/dataverse-design-system' import { useTranslation } from 'react-i18next' +import { FileDownloadOptions } from './FileDownloadOptions' interface FileActionButtonAccessFileProps { file: File @@ -23,6 +24,7 @@ export function AccessFileMenu({ file }: FileActionButtonAccessFileProps) { + ) diff --git a/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/access-file-menu/FileDownloadOptions.tsx b/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/access-file-menu/FileDownloadOptions.tsx new file mode 100644 index 000000000..61f0e8400 --- /dev/null +++ b/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/access-file-menu/FileDownloadOptions.tsx @@ -0,0 +1,30 @@ +import { DropdownHeader } from '@iqss/dataverse-design-system' +import { Download } from 'react-bootstrap-icons' +import { File } from '../../../../../../../../files/domain/models/File' +import { FileTabularDownloadOptions } from './FileTabularDownloadOptions' +import { FileNonTabularDownloadOptions } from './FileNonTabularDownloadOptions' +import { useTranslation } from 'react-i18next' + +interface FileDownloadOptionsProps { + file: File +} + +export function FileDownloadOptions({ file }: FileDownloadOptionsProps) { + const { t } = useTranslation('files') + return ( + <> + + {t('actions.accessFileMenu.downloadOptions.title')} + + + {file.tabularData ? ( + + ) : ( + + )} + + ) +} + +// TODO: Add guestbook support +// TODO: Add file package support diff --git a/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/access-file-menu/FileNonTabularDownloadOptions.tsx b/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/access-file-menu/FileNonTabularDownloadOptions.tsx new file mode 100644 index 000000000..e7306abb0 --- /dev/null +++ b/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/access-file-menu/FileNonTabularDownloadOptions.tsx @@ -0,0 +1,32 @@ +import { File, FileIngestStatus } from '../../../../../../../../files/domain/models/File' +import FileTypeToFriendlyTypeMap from '../../../../../../../../files/domain/models/FileTypeToFriendlyTypeMap' +import { DropdownButtonItem } from '@iqss/dataverse-design-system' +import { useDataset } from '../../../../../../DatasetContext' +import { useTranslation } from 'react-i18next' + +interface FileNonTabularDownloadOptionsProps { + file: File +} + +export function FileNonTabularDownloadOptions({ file }: FileNonTabularDownloadOptionsProps) { + const { t } = useTranslation('files') + const { dataset } = useDataset() + const originalFileFormatIsKnown = + file.type.toDisplayFormat() !== FileTypeToFriendlyTypeMap.unknown + + if (file.tabularData) { + return <> + } + + return ( + + {originalFileFormatIsKnown + ? file.type.toDisplayFormat() + : t('actions.accessFileMenu.downloadOptions.options.original')} + + ) +} diff --git a/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/access-file-menu/FileTabularDownloadOptions.tsx b/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/access-file-menu/FileTabularDownloadOptions.tsx new file mode 100644 index 000000000..c4e76fbd1 --- /dev/null +++ b/src/sections/dataset/dataset-files/files-table/file-actions/file-actions-cell/file-action-buttons/access-file-menu/FileTabularDownloadOptions.tsx @@ -0,0 +1,39 @@ +import { File, FileIngestStatus } from '../../../../../../../../files/domain/models/File' +import { DropdownButtonItem } from '@iqss/dataverse-design-system' +import { useDataset } from '../../../../../../DatasetContext' +import { useTranslation } from 'react-i18next' + +interface FileTabularDownloadOptionsProps { + file: File +} + +export function FileTabularDownloadOptions({ file }: FileTabularDownloadOptionsProps) { + const { t } = useTranslation('files') + const { dataset } = useDataset() + const originalFileFormatIsKnown = file.type.original && file.type.original !== 'Unknown' + const downloadDisabled = + file.ingest.status === FileIngestStatus.IN_PROGRESS || + (dataset && dataset.isLockedFromFileDownload) + + if (!file.tabularData) { + return <> + } + + return ( + <> + {originalFileFormatIsKnown && ( + {`${file.type.original} (${t( + 'actions.accessFileMenu.downloadOptions.options.original' + )})`} + )} + + {t('actions.accessFileMenu.downloadOptions.options.tabular')} + + {file.type.original !== 'R Data' && ( + + {t('actions.accessFileMenu.downloadOptions.options.RData')} + + )} + + ) +} 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')}}>