diff --git a/CHANGELOG.md b/CHANGELOG.md index ab6954c57..46e13f875 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ This changelog follows the principles of [Keep a Changelog](https://keepachangel ### Added - Dataset Templates UI integration, including create/edit flows, previews, and skeleton states. +- Dataset Page: added a sidebar to show dataset reviews ### Changed diff --git a/cypress.config.ts b/cypress.config.ts index aefab8a21..61e8dd7f6 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -1,6 +1,20 @@ import { defineConfig } from 'cypress' import vitePreprocessor from 'cypress-vite' import path from 'path' +import fs from 'node:fs' +import os from 'node:os' +import { execFile } from 'node:child_process' +import { promisify } from 'node:util' + +const execFileAsync = promisify(execFile) +const solrCollectionUrl = 'http://localhost:8983/solr/collection1' +const solrCoreAdminUrl = 'http://localhost:8983/solr/admin/cores' +const solrSchemaPath = '/var/solr/data/collection1/conf/schema.xml' + +type ExecFileError = Error & { + stderr?: string + stdout?: string +} export default defineConfig({ video: false, @@ -13,8 +27,47 @@ export default defineConfig({ viewportWidth: 1920, viewportHeight: 1080, supportFile: 'tests/support/e2e.ts', - setupNodeEvents(on) { + setupNodeEvents(on, config) { on('file:preprocessor', vitePreprocessor(path.resolve(__dirname, './vite.config.ts'))) + + on('task', { + async solrSchemaFieldExists(fieldName: string): Promise { + const statusCode = await runDockerCommand(config, [ + 'exec', + getSolrContainerName(config), + 'curl', + '-sS', + '-o', + '/tmp/solr-schema-field-response.json', + '-w', + '%{http_code}', + `${solrCollectionUrl}/schema/fields/${encodeURIComponent(fieldName)}` + ]) + + if (statusCode.trim() === '200') { + return true + } + + if (statusCode.trim() === '404') { + return false + } + + throw new Error(`Unexpected Solr schema field check status for ${fieldName}: ${statusCode}`) + }, + async replaceSolrSchemaWithDataverseGeneratedSchema(): Promise { + const generatedSchemaFragment = await getDataverseGeneratedSolrSchemaFragment(config) + const currentSchema = await runDockerCommand(config, [ + 'exec', + getSolrContainerName(config), + 'cat', + solrSchemaPath + ]) + const mergedSchema = mergeGeneratedSchemaFragment(currentSchema, generatedSchemaFragment) + await copySchemaToSolrContainer(config, mergedSchema) + await reloadSolrCore(config) + return null + } + }) }, defaultCommandTimeout: 10_000 // https://docs.cypress.io/guides/references/configuration#Timeouts }, @@ -62,3 +115,108 @@ export default defineConfig({ } } }) + +function getSolrContainerName(config: Cypress.PluginConfigOptions): string { + return (config.env.solrContainerName as string | undefined) ?? 'dev_solr' +} + +async function getDataverseGeneratedSolrSchemaFragment( + config: Cypress.PluginConfigOptions +): Promise { + const backendUrl = config.env.backendUrl as string + const response = await fetch(`${backendUrl}/api/v1/admin/index/solr/schema`) + + if (!response.ok) { + throw new Error(`Error while getting Dataverse-generated Solr schema: ${response.status}`) + } + + return response.text() +} + +async function copySchemaToSolrContainer( + config: Cypress.PluginConfigOptions, + schemaXml: string +): Promise { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dataverse-solr-schema-')) + const tempSchemaPath = path.join(tempDir, 'schema.xml') + + try { + fs.writeFileSync(tempSchemaPath, schemaXml) + await runDockerCommand(config, [ + 'cp', + tempSchemaPath, + `${getSolrContainerName(config)}:${solrSchemaPath}` + ]) + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }) + } +} + +async function reloadSolrCore(config: Cypress.PluginConfigOptions): Promise { + await runDockerCommand(config, [ + 'exec', + getSolrContainerName(config), + 'curl', + '-sS', + `${solrCoreAdminUrl}?action=RELOAD&core=collection1&wt=json` + ]) +} + +function mergeGeneratedSchemaFragment( + currentSchema: string, + generatedSchemaFragment: string +): string { + const generatedSchemaLines = generatedSchemaFragment.split('\n') + const fieldLines = generatedSchemaLines.filter((line) => line.includes(' line.includes('', + '', + fieldLines + ), + '', + '', + copyFieldLines + ) +} + +function replaceSchemaSection( + schema: string, + beginMarker: string, + endMarker: string, + replacementLines: string[] +): string { + const beginIndex = schema.indexOf(beginMarker) + const endIndex = schema.indexOf(endMarker) + + if (beginIndex === -1 || endIndex === -1 || beginIndex > endIndex) { + throw new Error(`Could not find Solr schema section ${beginMarker}.`) + } + + return [ + schema.slice(0, beginIndex + beginMarker.length), + '', + replacementLines.join('\n'), + schema.slice(endIndex) + ].join('\n') +} + +async function runDockerCommand( + _config: Cypress.PluginConfigOptions, + args: string[] +): Promise { + try { + const { stdout } = await execFileAsync('docker', args, { maxBuffer: 10 * 1024 * 1024 }) + return stdout + } catch (error) { + const execError = error as ExecFileError + throw new Error( + `Docker command failed: docker ${args.join(' ')}. Reason was: ${ + execError.stderr ?? execError.stdout ?? execError.message + }` + ) + } +} diff --git a/package-lock.json b/package-lock.json index c5b242488..fbf90d053 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@dnd-kit/sortable": "8.0.0", "@dnd-kit/utilities": "3.2.2", "@faker-js/faker": "7.6.0", - "@iqss/dataverse-client-javascript": "2.1.0-alpha.4", + "@iqss/dataverse-client-javascript": "2.2.0-pr457.c6b21a4", "@iqss/dataverse-design-system": "*", "@istanbuljs/nyc-config-typescript": "1.0.2", "@tanstack/react-table": "8.9.2", @@ -3285,9 +3285,9 @@ } }, "node_modules/@iqss/dataverse-client-javascript": { - "version": "2.1.0-alpha.4", - "resolved": "https://npm.pkg.github.com/download/@IQSS/dataverse-client-javascript/2.1.0-alpha.4/0a0dc68d4d99581d7ec017e58dbce3407f99f5d9", - "integrity": "sha512-UwHnFSYuvhxpc/JG2cFr5+bwERZXqGfNBTMSUQwtJaj6vfHO7anJAAxAx8g/AB8b4JtR9rljnYjAbsGJXicfBw==", + "version": "2.2.0-pr457.c6b21a4", + "resolved": "https://npm.pkg.github.com/download/@IQSS/dataverse-client-javascript/2.2.0-pr457.c6b21a4/c29e777fe839acb125fc0c33bbaf97729a34fbca", + "integrity": "sha512-mFcUYGQXemCK3N3BnHnOME7D3LL6qcd547GeH6ThVbxq5/YnE7iVxXUzSv4uHWXXw715T+Y8x3axan8eLygzLA==", "license": "MIT", "dependencies": { "@types/node": "^18.15.11", diff --git a/package.json b/package.json index 6d54dca10..7c6785438 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "@dnd-kit/sortable": "8.0.0", "@dnd-kit/utilities": "3.2.2", "@faker-js/faker": "7.6.0", - "@iqss/dataverse-client-javascript": "2.1.0-alpha.4", + "@iqss/dataverse-client-javascript": "2.2.0-pr457.c6b21a4", "@iqss/dataverse-design-system": "*", "@istanbuljs/nyc-config-typescript": "1.0.2", "@tanstack/react-table": "8.9.2", diff --git a/public/locales/en/createDataset.json b/public/locales/en/createDataset.json index d9900dd7c..a7ee61f29 100644 --- a/public/locales/en/createDataset.json +++ b/public/locales/en/createDataset.json @@ -11,5 +11,12 @@ "label": "Dataset Template", "description": "The dataset template which prepopulates info into the form automatically.", "helpText": "Changing the template will clear any fields you may have entered data into." + }, + "datasetType": { + "label": "Dataset Type", + "description": "The type of dataset you are creating.", + "helpText": "Changing the dataset type will clear any fields you may have entered data into.", + "placeholder": "Select a dataset type", + "toggleMenu": "Toggle dataset types options menu" } } diff --git a/public/locales/en/dataset.json b/public/locales/en/dataset.json index 4e73950a9..9e583d6ec 100644 --- a/public/locales/en/dataset.json +++ b/public/locales/en/dataset.json @@ -330,6 +330,9 @@ }, "defaultGetDownloadCountError": "Something went wrong while getting the dataset download count. Try again later." }, + "reviews": { + "title": "Dataset Reviews" + }, "persistentId": { "name": "Persistent Identifier", "description": "The Dataset's unique persistent identifier, either a DOI or Handle" diff --git a/public/locales/en/editDatasetMetadata.json b/public/locales/en/editDatasetMetadata.json index ee406b0da..473dacf61 100644 --- a/public/locales/en/editDatasetMetadata.json +++ b/public/locales/en/editDatasetMetadata.json @@ -8,5 +8,8 @@ "label": "Host Collection", "description": "The collection which contains this data." }, - "metadata": "Metadata" + "metadata": "Metadata", + "datasetType": { + "label": "Dataset Type" + } } diff --git a/public/locales/es/createDataset.json b/public/locales/es/createDataset.json index 519328bf2..26e0cc1ea 100644 --- a/public/locales/es/createDataset.json +++ b/public/locales/es/createDataset.json @@ -11,5 +11,12 @@ "label": "Plantilla de dataset", "description": "La plantilla de dataset que completa información automáticamente en el formulario.", "helpText": "Cambiar la plantilla borrará cualquier campo en el que hayas ingresado datos." + }, + "datasetType": { + "label": "Tipo de dataset", + "description": "El tipo de dataset que estás creando.", + "helpText": "Cambiar el tipo de dataset borrará cualquier campo en el que hayas ingresado datos.", + "placeholder": "Selecciona un tipo de dataset", + "toggleMenu": "Alternar menú de opciones de tipos de dataset" } } diff --git a/public/locales/es/dataset.json b/public/locales/es/dataset.json index 8fff5866d..fb377fdba 100644 --- a/public/locales/es/dataset.json +++ b/public/locales/es/dataset.json @@ -332,6 +332,9 @@ }, "defaultGetDownloadCountError": "Algo salió mal al obtener el conteo de descargas del dataset. Intenta nuevamente más tarde." }, + "reviews": { + "title": "Reseñas del dataset" + }, "persistentId": { "name": "Identificador persistente", "description": "El identificador persistente único del Dataset, ya sea un DOI o Handle" diff --git a/public/locales/es/editDatasetMetadata.json b/public/locales/es/editDatasetMetadata.json index 5b8de975e..173e00bb3 100644 --- a/public/locales/es/editDatasetMetadata.json +++ b/public/locales/es/editDatasetMetadata.json @@ -8,5 +8,8 @@ "label": "Colección anfitriona", "description": "La colección que contiene estos datos." }, - "metadata": "Metadatos" + "metadata": "Metadatos", + "datasetType": { + "label": "Tipo de dataset" + } } diff --git a/src/collection/domain/models/Collection.ts b/src/collection/domain/models/Collection.ts index a929e632e..cd193d279 100644 --- a/src/collection/domain/models/Collection.ts +++ b/src/collection/domain/models/Collection.ts @@ -2,6 +2,7 @@ import { UpwardHierarchyNode } from '../../../shared/hierarchy/domain/models/Upw import { CollectionContact } from './CollectionContact' import { CollectionType } from './CollectionType' import { CollectionInputLevel } from './CollectionInputLevel' +import { DatasetType } from '@/dataset/domain/models/DatasetType' export interface Collection { id: string @@ -16,4 +17,5 @@ export interface Collection { isMetadataBlockRoot: boolean isFacetRoot: boolean childCount: number + allowedDatasetTypes?: DatasetType[] } diff --git a/src/collection/infrastructure/mappers/JSCollectionMapper.ts b/src/collection/infrastructure/mappers/JSCollectionMapper.ts index b409a281c..ec65d3910 100644 --- a/src/collection/infrastructure/mappers/JSCollectionMapper.ts +++ b/src/collection/infrastructure/mappers/JSCollectionMapper.ts @@ -8,6 +8,7 @@ import { UpwardHierarchyNode } from '../../../shared/hierarchy/domain/models/UpwardHierarchyNode' import { JSUpwardHierarchyNodeMapper } from '../../../shared/hierarchy/infrastructure/mappers/JSUpwardHierarchyNodeMapper' +import { DatasetType } from '@/dataset/domain/models/DatasetType' export class JSCollectionMapper { static toCollection(jsCollection: JSCollection): Collection { @@ -27,7 +28,9 @@ export class JSCollectionMapper { contacts: jsCollection.contacts ?? [], isMetadataBlockRoot: jsCollection.isMetadataBlockRoot, isFacetRoot: jsCollection.isFacetRoot, - childCount: jsCollection.childCount + childCount: jsCollection.childCount, + allowedDatasetTypes: (jsCollection as unknown as Record) + .allowedDatasetTypes as DatasetType[] | undefined } } diff --git a/src/dataset/domain/hooks/useGetAvailableDatasetTypes.ts b/src/dataset/domain/hooks/useGetAvailableDatasetTypes.ts new file mode 100644 index 000000000..c2fd56e5a --- /dev/null +++ b/src/dataset/domain/hooks/useGetAvailableDatasetTypes.ts @@ -0,0 +1,66 @@ +import { useCallback, useEffect, useState } from 'react' +import { ReadError } from '@iqss/dataverse-client-javascript' +import { JSDataverseReadErrorHandler } from '@/shared/helpers/JSDataverseReadErrorHandler' +import { DatasetRepository } from '../repositories/DatasetRepository' +import { getAvailableDatasetTypes } from '../useCases/getAvailableDatasetTypes' +import { DatasetType } from '../models/DatasetType' +import { CollectionRepository } from '@/collection/domain/repositories/CollectionRepository' + +interface useGetAvailableDatasetTypesProps { + datasetRepository: DatasetRepository + collectionRepository?: CollectionRepository + collectionId?: string + autoFetch?: boolean +} + +export const useGetAvailableDatasetTypes = ({ + datasetRepository, + collectionRepository, + collectionId, + autoFetch = true +}: useGetAvailableDatasetTypesProps) => { + const [datasetTypes, setDatasetTypes] = useState([]) + const [isLoadingDatasetTypes, setIsLoadingDatasetTypes] = useState(autoFetch) + const [errorGetDatasetTypes, setErrorGetDatasetTypes] = useState(null) + + const fetchDatasetTypes = useCallback(async () => { + setIsLoadingDatasetTypes(true) + setErrorGetDatasetTypes(null) + + try { + const response = + collectionRepository && collectionId + ? (await collectionRepository.getById(collectionId)).allowedDatasetTypes ?? [] + : await getAvailableDatasetTypes(datasetRepository) + + setDatasetTypes(response) + + setDatasetTypes(response) + } catch (err) { + if (err instanceof ReadError) { + const error = new JSDataverseReadErrorHandler(err) + const formattedError = + error.getReasonWithoutStatusCode() ?? /* istanbul ignore next */ error.getErrorMessage() + + setErrorGetDatasetTypes(formattedError) + } else { + setErrorGetDatasetTypes('Something went wrong getting the dataset types. Try again later.') + } + } finally { + setIsLoadingDatasetTypes(false) + } + }, [datasetRepository, collectionRepository, collectionId]) + + useEffect(() => { + if (autoFetch) { + void fetchDatasetTypes() + } + }, [autoFetch, fetchDatasetTypes]) + + return { + datasetTypes, + isLoadingDatasetTypes, + errorGetDatasetTypes, + fetchDatasetTypes + } +} diff --git a/src/dataset/domain/models/Dataset.ts b/src/dataset/domain/models/Dataset.ts index 4fb18403a..68ff11621 100644 --- a/src/dataset/domain/models/Dataset.ts +++ b/src/dataset/domain/models/Dataset.ts @@ -439,7 +439,8 @@ export class Dataset { public readonly nextMinorVersion?: string, public readonly requiresMajorVersionUpdate?: boolean, public readonly fileStore?: string, - public readonly guestbookId?: number + public readonly guestbookId?: number, + public readonly datasetType?: string ) {} public checkIsLockedFromPublishing(userPersistentId: string): boolean { @@ -535,7 +536,8 @@ export class Dataset { public readonly nextMinorVersionNumber?: string, public readonly requiresMajorVersionUpdate?: boolean, public readonly fileStore?: string, - public readonly guestbookId?: number + public readonly guestbookId?: number, + public readonly datasetType?: string ) { this.withAlerts() } @@ -608,7 +610,8 @@ export class Dataset { this.nextMinorVersionNumber, this.requiresMajorVersionUpdate, this.fileStore, - this.guestbookId + this.guestbookId, + this.datasetType ) } } diff --git a/src/dataset/domain/models/DatasetReview.ts b/src/dataset/domain/models/DatasetReview.ts new file mode 100644 index 000000000..2ffbf7e54 --- /dev/null +++ b/src/dataset/domain/models/DatasetReview.ts @@ -0,0 +1,23 @@ +export interface DatasetReview { + title: string + authors: string[] + persistentId: string + persistentIdUrl: string + id: number + citation: string + citationHtml: string + datePublished: string + description: string + rubricMetadataBlocks: DatasetReviewRubricMetadataBlock[] +} + +export interface DatasetReviewRubricMetadataBlock { + name: string + displayName: string + fields: DatasetReviewRubricMetadataField[] +} + +export interface DatasetReviewRubricMetadataField { + typeName: string + value: string +} diff --git a/src/dataset/domain/models/DatasetType.ts b/src/dataset/domain/models/DatasetType.ts new file mode 100644 index 000000000..70bd9d412 --- /dev/null +++ b/src/dataset/domain/models/DatasetType.ts @@ -0,0 +1,8 @@ +export interface DatasetType { + id: number + name: string + displayName: string + linkedMetadataBlocks?: string[] + availableLicenses?: string[] + description?: string +} diff --git a/src/dataset/domain/repositories/DatasetRepository.ts b/src/dataset/domain/repositories/DatasetRepository.ts index 1bd783499..efde1b9f7 100644 --- a/src/dataset/domain/repositories/DatasetRepository.ts +++ b/src/dataset/domain/repositories/DatasetRepository.ts @@ -12,6 +12,8 @@ import { DatasetLicenseUpdateRequest } from '../models/DatasetLicenseUpdateReque import { CollectionSummary } from '@/collection/domain/models/CollectionSummary' import { DatasetVersionPaginationInfo } from '../models/DatasetVersionPaginationInfo' import { DatasetUploadLimits } from '../models/DatasetUploadLimits' +import { DatasetType } from '../models/DatasetType' +import { DatasetReview } from '../models/DatasetReview' export interface DatasetRepository { getByPersistentId: ( @@ -29,7 +31,11 @@ export interface DatasetRepository { includeDeaccessioned: boolean ) => Promise - create: (dataset: DatasetDTO, collectionId: string) => Promise<{ persistentId: string }> + create: ( + dataset: DatasetDTO, + collectionId: string, + datasetType?: DatasetType['name'] + ) => Promise<{ persistentId: string }> updateMetadata: ( datasetId: string | number, datasetDTO: DatasetDTO, @@ -70,4 +76,6 @@ export interface DatasetRepository { unlink(datasetId: string | number, collectionIdOrAlias: string | number): Promise getDatasetLinkedCollections: (datasetId: string | number) => Promise getDatasetUploadLimits: (datasetId: string | number) => Promise + getDatasetReviews: (datasetId: string | number) => Promise + getAvailableDatasetTypes: () => Promise } diff --git a/src/dataset/domain/useCases/createDataset.ts b/src/dataset/domain/useCases/createDataset.ts index abe0a943f..4bd3e9f26 100644 --- a/src/dataset/domain/useCases/createDataset.ts +++ b/src/dataset/domain/useCases/createDataset.ts @@ -1,12 +1,14 @@ import { DatasetRepository } from '../repositories/DatasetRepository' import { DatasetDTO } from './DTOs/DatasetDTO' +import { DatasetType } from '../models/DatasetType' export function createDataset( datasetRepository: DatasetRepository, dataset: DatasetDTO, - collectionId: string + collectionId: string, + datasetType?: DatasetType['name'] ): Promise<{ persistentId: string }> { - return datasetRepository.create(dataset, collectionId).catch((error: Error) => { + return datasetRepository.create(dataset, collectionId, datasetType).catch((error: Error) => { throw new Error(error.message) }) } diff --git a/src/dataset/domain/useCases/getAvailableDatasetTypes.ts b/src/dataset/domain/useCases/getAvailableDatasetTypes.ts new file mode 100644 index 000000000..926a42f58 --- /dev/null +++ b/src/dataset/domain/useCases/getAvailableDatasetTypes.ts @@ -0,0 +1,8 @@ +import { DatasetRepository } from '../repositories/DatasetRepository' +import { DatasetType } from '@iqss/dataverse-client-javascript' + +export function getAvailableDatasetTypes( + datasetRepository: DatasetRepository +): Promise { + return datasetRepository.getAvailableDatasetTypes() +} diff --git a/src/dataset/domain/useCases/getDatasetReviews.ts b/src/dataset/domain/useCases/getDatasetReviews.ts new file mode 100644 index 000000000..b5af0ce19 --- /dev/null +++ b/src/dataset/domain/useCases/getDatasetReviews.ts @@ -0,0 +1,9 @@ +import { DatasetReview } from '../models/DatasetReview' +import { DatasetRepository } from '../repositories/DatasetRepository' + +export async function getDatasetReviews( + datasetRepository: DatasetRepository, + datasetId: string | number +): Promise { + return datasetRepository.getDatasetReviews(datasetId) +} diff --git a/src/dataset/infrastructure/mappers/JSDatasetMapper.ts b/src/dataset/infrastructure/mappers/JSDatasetMapper.ts index 3358c7d3d..ad7de37b7 100644 --- a/src/dataset/infrastructure/mappers/JSDatasetMapper.ts +++ b/src/dataset/infrastructure/mappers/JSDatasetMapper.ts @@ -49,7 +49,8 @@ export class JSDatasetMapper { latestPublishedVersionMajorNumber?: number, latestPublishedVersionMinorNumber?: number, datasetVersionDiff?: JSDatasetVersionDiff, - fileStore?: string + fileStore?: string, + datasetType?: string ): Dataset { const version = JSDatasetVersionMapper.toVersion( jsDataset.versionId, @@ -101,7 +102,8 @@ export class JSDatasetMapper { ), JSDatasetMapper.toRequiresMajorVersionUpdate(datasetVersionDiff), fileStore, - jsDataset.guestbookId as number + jsDataset.guestbookId as number, + datasetType ).build() } diff --git a/src/dataset/infrastructure/repositories/DatasetJSDataverseRepository.ts b/src/dataset/infrastructure/repositories/DatasetJSDataverseRepository.ts index a26660169..22ffa1e68 100644 --- a/src/dataset/infrastructure/repositories/DatasetJSDataverseRepository.ts +++ b/src/dataset/infrastructure/repositories/DatasetJSDataverseRepository.ts @@ -44,7 +44,10 @@ import { getDatasetLinkedCollections, updateTermsOfAccess, updateDatasetLicense, - getDatasetUploadLimits + getDatasetUploadLimits, + getDatasetReviews, + DatasetType, + getDatasetAvailableDatasetTypes } from '@iqss/dataverse-client-javascript' import { JSDatasetMapper } from '../mappers/JSDatasetMapper' import { DatasetPaginationInfo } from '../../domain/models/DatasetPaginationInfo' @@ -64,6 +67,7 @@ import { AxiosResponse } from 'axios' import { JSDataverseReadErrorHandler } from '@/shared/helpers/JSDataverseReadErrorHandler' import { CollectionSummary } from '@/collection/domain/models/CollectionSummary' import { DatasetUploadLimits } from '@/dataset/domain/models/DatasetUploadLimits' +import { DatasetReview } from '@/dataset/domain/models/DatasetReview' const includeDeaccessioned = true @@ -170,6 +174,7 @@ export class DatasetJSDataverseRepository implements DatasetRepository { ]) => { return { jsDataset, + datasetType: (jsDataset as { datasetType?: string }).datasetType, summaryFieldsNames, citation, jsDatasetPermissions, @@ -327,9 +332,13 @@ export class DatasetJSDataverseRepository implements DatasetRepository { }) } - create(dataset: DatasetDTO, collectionId: string): Promise<{ persistentId: string }> { + create( + dataset: DatasetDTO, + collectionId: string, + datasetType?: string + ): Promise<{ persistentId: string }> { return createDataset - .execute(DatasetDTOMapper.toJSDatasetDTO(dataset), collectionId) + .execute(DatasetDTOMapper.toJSDatasetDTO(dataset), collectionId, datasetType) .then((jsDatasetIdentifiers: JSDatasetIdentifiers) => ({ persistentId: jsDatasetIdentifiers.persistentId })) @@ -431,6 +440,10 @@ export class DatasetJSDataverseRepository implements DatasetRepository { return getDatasetLinkedCollections.execute(datasetId) } + getAvailableDatasetTypes: () => Promise = () => { + return getDatasetAvailableDatasetTypes.execute() + } + /* TODO: This is a temporary solution as this use case doesn't exist in js-dataverse yet and the API should also return the file store type rather than name only. After https://github.com/IQSS/dataverse/issues/11695 is implemented, create a js-dataverse use case. @@ -474,4 +487,8 @@ export class DatasetJSDataverseRepository implements DatasetRepository { getDatasetUploadLimits(datasetId: string | number): Promise { return getDatasetUploadLimits.execute(datasetId) } + + getDatasetReviews(datasetId: string | number): Promise { + return getDatasetReviews.execute(datasetId) + } } diff --git a/src/files/infrastructure/FileJSDataverseRepository.ts b/src/files/infrastructure/FileJSDataverseRepository.ts index 4ff853562..d37d012db 100644 --- a/src/files/infrastructure/FileJSDataverseRepository.ts +++ b/src/files/infrastructure/FileJSDataverseRepository.ts @@ -55,6 +55,12 @@ export class FileJSDataverseRepository implements FileRepository { return requireAppConfig().backendUrl } + private static readonly guestFilePermissions: FilePermissions = { + canDownloadFile: false, + canManageFilePermissions: false, + canEditOwnerDataset: false + } + getAllByDatasetPersistentId( datasetPersistentId: string, datasetVersion: DatasetVersion, @@ -177,7 +183,11 @@ export class FileJSDataverseRepository implements FileRepository { ) } private static getAllWithPermissions(files: JSFile[]): Promise { - return Promise.all(files.map((jsFile) => this.getPermissionsById(jsFile.id))) + return Promise.all( + files.map((jsFile) => + this.getPermissionsByIdOrGuest(jsFile.id, jsFile).then(({ permissions }) => permissions) + ) + ) } private static getPermissionsById(id: number): Promise { @@ -260,13 +270,17 @@ export class FileJSDataverseRepository implements FileRepository { } getById(id: number, datasetVersionNumber?: string): Promise { - return FileJSDataverseRepository.getPermissionsById(id) - .then((permissions) => { + return FileJSDataverseRepository.getPermissionsByIdOrGuest(id) + .then(({ permissions, isGuestFallback }) => { const includeDeaccessioned = permissions?.canEditOwnerDataset return getFileAndDataset .execute(id, datasetVersionNumber, includeDeaccessioned) .then(([jsFile, jsDataset]) => { + const resolvedPermissions = isGuestFallback + ? FileJSDataverseRepository.getGuestPermissionsForFile(jsFile) + : permissions + return Promise.all([ jsFile, jsDataset, @@ -277,7 +291,7 @@ export class FileJSDataverseRepository implements FileRepository { includeDeaccessioned ), FileJSDataverseRepository.getDownloadCountById(jsFile.id, jsFile.publicationDate), - Promise.resolve(permissions), + Promise.resolve(resolvedPermissions), FileJSDataverseRepository.getThumbnailById(jsFile.id), FileJSDataverseRepository.getTabularDataById(jsFile.id, jsFile.tabularData) ]) @@ -310,6 +324,41 @@ export class FileJSDataverseRepository implements FileRepository { }) } + private static getPermissionsByIdOrGuest( + id: number, + jsFile?: JSFile + ): Promise<{ + permissions: FilePermissions + isGuestFallback: boolean + }> { + return FileJSDataverseRepository.getPermissionsById(id) + .then((permissions) => ({ permissions, isGuestFallback: false })) + .catch((error: ReadError) => { + if (error instanceof ReadError) { + const errorHandler = new JSDataverseReadErrorHandler(error) + + if (errorHandler.getStatusCode() === 401) { + return { + permissions: + jsFile !== undefined + ? FileJSDataverseRepository.getGuestPermissionsForFile(jsFile) + : FileJSDataverseRepository.guestFilePermissions, + isGuestFallback: true + } + } + } + + throw error + }) + } + + private static getGuestPermissionsForFile(jsFile: JSFile): FilePermissions { + return { + ...FileJSDataverseRepository.guestFilePermissions, + canDownloadFile: !jsFile.restricted && jsFile.embargo === undefined + } + } + private static getCitationById( id: number, datasetVersionNumber?: string, diff --git a/src/metadata-block-info/domain/repositories/MetadataBlockInfoRepository.ts b/src/metadata-block-info/domain/repositories/MetadataBlockInfoRepository.ts index 4b5044051..95553cf0e 100644 --- a/src/metadata-block-info/domain/repositories/MetadataBlockInfoRepository.ts +++ b/src/metadata-block-info/domain/repositories/MetadataBlockInfoRepository.ts @@ -8,8 +8,13 @@ export interface MetadataBlockInfoRepository { getByName: (name: string) => Promise getAll: () => Promise getDisplayedOnCreateByCollectionId: ( - collectionId: number | string + collectionId: number | string, + datasetType?: string + ) => Promise + getByCollectionId: ( + collectionId: number | string, + onlyDisplayedOnCreate?: boolean, + datasetType?: string ) => Promise - getByCollectionId: (collectionId: number | string) => Promise getAllFacetableMetadataFields: () => Promise } diff --git a/src/metadata-block-info/domain/useCases/getDisplayedOnCreateMetadataBlockInfoByCollectionId.ts b/src/metadata-block-info/domain/useCases/getDisplayedOnCreateMetadataBlockInfoByCollectionId.ts index 3fede920f..da9c676c8 100644 --- a/src/metadata-block-info/domain/useCases/getDisplayedOnCreateMetadataBlockInfoByCollectionId.ts +++ b/src/metadata-block-info/domain/useCases/getDisplayedOnCreateMetadataBlockInfoByCollectionId.ts @@ -3,10 +3,11 @@ import { MetadataBlockInfoRepository } from '../repositories/MetadataBlockInfoRe export async function getDisplayedOnCreateMetadataBlockInfoByCollectionId( metadataBlockInfoRepository: MetadataBlockInfoRepository, - collectionId: number | string + collectionId: number | string, + datasetType?: string ): Promise { return metadataBlockInfoRepository - .getDisplayedOnCreateByCollectionId(collectionId) + .getDisplayedOnCreateByCollectionId(collectionId, datasetType) .catch((error: Error) => { throw new Error(error.message) }) diff --git a/src/metadata-block-info/domain/useCases/getMetadataBlockInfoByCollectionId.ts b/src/metadata-block-info/domain/useCases/getMetadataBlockInfoByCollectionId.ts index ab3fba345..5fe7becdf 100644 --- a/src/metadata-block-info/domain/useCases/getMetadataBlockInfoByCollectionId.ts +++ b/src/metadata-block-info/domain/useCases/getMetadataBlockInfoByCollectionId.ts @@ -3,9 +3,13 @@ import { MetadataBlockInfoRepository } from '../repositories/MetadataBlockInfoRe export async function getMetadataBlockInfoByCollectionId( metadataBlockInfoRepository: MetadataBlockInfoRepository, - collectionId: number | string + collectionId: number | string, + onlyDisplayedOnCreate?: boolean, + datasetType?: string ): Promise { - return metadataBlockInfoRepository.getByCollectionId(collectionId).catch((error: Error) => { - throw new Error(error.message) - }) + return metadataBlockInfoRepository + .getByCollectionId(collectionId, onlyDisplayedOnCreate, datasetType) + .catch((error: Error) => { + throw new Error(error.message) + }) } diff --git a/src/metadata-block-info/infrastructure/repositories/MetadataBlockInfoJSDataverseRepository.ts b/src/metadata-block-info/infrastructure/repositories/MetadataBlockInfoJSDataverseRepository.ts index baa46e3d3..fb49676da 100644 --- a/src/metadata-block-info/infrastructure/repositories/MetadataBlockInfoJSDataverseRepository.ts +++ b/src/metadata-block-info/infrastructure/repositories/MetadataBlockInfoJSDataverseRepository.ts @@ -37,9 +37,13 @@ export class MetadataBlockInfoJSDataverseRepository implements MetadataBlockInfo }) } - getByCollectionId(collectionIdOrAlias: number | string): Promise { + getByCollectionId( + collectionIdOrAlias: number | string, + onlyDisplayedOnCreate?: boolean, + datasetType?: string + ): Promise { return getCollectionMetadataBlocks - .execute(collectionIdOrAlias) + .execute(collectionIdOrAlias, onlyDisplayedOnCreate, datasetType) .then((metadataBlocks: MetadataBlockInfo[]) => { return metadataBlocks }) @@ -49,12 +53,23 @@ export class MetadataBlockInfoJSDataverseRepository implements MetadataBlockInfo } getDisplayedOnCreateByCollectionId( - collectionIdOrAlias: number | string + collectionIdOrAlias: number | string, + datasetType?: string ): Promise { return getCollectionMetadataBlocks - .execute(collectionIdOrAlias, true) + .execute(collectionIdOrAlias, true, datasetType) .then((metadataBlocks: MetadataBlockInfo[]) => { - return metadataBlocks + const metadataBlocksWithFields: MetadataBlockInfo[] = [] + metadataBlocks.forEach((block) => { + const numFields = Object.keys(block.metadataFields).length + // numFields can be zero if you pass a datasetType that's linked to + // a metadata block that doesn't have any fields set to displayOnCreate. + // See https://github.com/IQSS/dataverse/blob/v6.7.1/src/test/java/edu/harvard/iq/dataverse/api/DatasetTypesIT.java#L512 + if (numFields > 0) { + metadataBlocksWithFields.push(block) + } + }) + return metadataBlocksWithFields }) .catch((error: ReadError) => { throw new Error(error.message) diff --git a/src/sections/Route.enum.ts b/src/sections/Route.enum.ts index d9853c57a..5ad1118d4 100644 --- a/src/sections/Route.enum.ts +++ b/src/sections/Route.enum.ts @@ -90,6 +90,7 @@ export enum QueryParamKey { TAB = 'tab', FILE_ID = 'id', DATASET_VERSION = 'datasetVersion', + DATASET_TYPE = 'datasetType', REFERRER = 'referrer', AUTH_STATE = 'state', VALID_TOKEN_BUT_NOT_LINKED_ACCOUNT = 'validTokenButNotLinkedAccount', diff --git a/src/sections/create-dataset/CreateDataset.tsx b/src/sections/create-dataset/CreateDataset.tsx index 8afded02e..f2f158e35 100644 --- a/src/sections/create-dataset/CreateDataset.tsx +++ b/src/sections/create-dataset/CreateDataset.tsx @@ -18,6 +18,8 @@ import { type Template } from '@/templates/domain/models/Template' import { DatasetTemplateSelect } from './dataset-template-select/DatasetTemplateSelect' import { TemplateRepository } from '@/templates/domain/repositories/TemplateRepository' import { useCollectionRepositories } from '@/shared/contexts/repositories/RepositoriesProvider' +import { DatasetType } from '@/dataset/domain/models/DatasetType' +import { DatasetTypeSelect } from './dataset-type-select/DatasetTypeSelect' interface CreateDatasetProps { templateRepository: TemplateRepository @@ -35,12 +37,15 @@ export function CreateDataset({ const { isModalOpen, hideModal } = useNotImplementedModal() const { setIsLoading } = useLoading() const [selectedTemplate, setSelectedTemplate] = useState