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/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/src/dataset/domain/hooks/useGetAvailableDatasetTypes.ts b/src/dataset/domain/hooks/useGetAvailableDatasetTypes.ts new file mode 100644 index 000000000..16f6e34bd --- /dev/null +++ b/src/dataset/domain/hooks/useGetAvailableDatasetTypes.ts @@ -0,0 +1,56 @@ +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' + +interface useGetAvailableDatasetTypesProps { + datasetRepository: DatasetRepository + autoFetch?: boolean +} + +export const useGetAvailableDatasetTypes = ({ + datasetRepository, + 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: DatasetType[] = await getAvailableDatasetTypes(datasetRepository) + + 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]) + + 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 19c9935a2..8289208ab 100644 --- a/src/dataset/domain/models/Dataset.ts +++ b/src/dataset/domain/models/Dataset.ts @@ -438,7 +438,8 @@ export class Dataset { public readonly nextMajorVersion?: string, public readonly nextMinorVersion?: string, public readonly requiresMajorVersionUpdate?: boolean, - public readonly fileStore?: string + public readonly fileStore?: string, + public readonly datasetType?: string ) {} public checkIsLockedFromPublishing(userPersistentId: string): boolean { @@ -533,7 +534,8 @@ export class Dataset { public readonly nextMajorVersionNumber?: string, public readonly nextMinorVersionNumber?: string, public readonly requiresMajorVersionUpdate?: boolean, - public readonly fileStore?: string + public readonly fileStore?: string, + public readonly datasetType?: string ) { this.withAlerts() } @@ -605,7 +607,8 @@ export class Dataset { this.nextMajorVersionNumber, this.nextMinorVersionNumber, this.requiresMajorVersionUpdate, - this.fileStore + this.fileStore, + this.datasetType ) } } diff --git a/src/dataset/domain/models/DatasetType.ts b/src/dataset/domain/models/DatasetType.ts new file mode 100644 index 000000000..d8e02e180 --- /dev/null +++ b/src/dataset/domain/models/DatasetType.ts @@ -0,0 +1,7 @@ +export interface DatasetType { + id: number + name: string + description: string + linkedMetadataBlocks?: string[] + availableLicenses?: string[] +} diff --git a/src/dataset/domain/repositories/DatasetRepository.ts b/src/dataset/domain/repositories/DatasetRepository.ts index 4a3e332dd..e74add1f0 100644 --- a/src/dataset/domain/repositories/DatasetRepository.ts +++ b/src/dataset/domain/repositories/DatasetRepository.ts @@ -11,6 +11,7 @@ import { FormattedCitation, CitationFormat } from '../models/DatasetCitation' import { DatasetLicenseUpdateRequest } from '../models/DatasetLicenseUpdateRequest' import { CollectionSummary } from '@/collection/domain/models/CollectionSummary' import { DatasetVersionPaginationInfo } from '../models/DatasetVersionPaginationInfo' +import { DatasetType } from '../models/DatasetType' export interface DatasetRepository { getByPersistentId: ( @@ -28,7 +29,11 @@ export interface DatasetRepository { includeDeaccessioned: boolean ) => Promise - create: (dataset: DatasetDTO, collectionId: string) => Promise<{ persistentId: string }> + create: ( + dataset: DatasetDTO, + collectionId: string, + datasetType?: string + ) => Promise<{ persistentId: string }> updateMetadata: ( datasetId: string | number, datasetDTO: DatasetDTO, @@ -68,4 +73,5 @@ export interface DatasetRepository { link(datasetId: string | number, collectionIdOrAlias: string | number): Promise unlink(datasetId: string | number, collectionIdOrAlias: string | number): Promise getDatasetLinkedCollections: (datasetId: string | number) => Promise + getAvailableDatasetTypes: () => Promise } diff --git a/src/dataset/domain/useCases/createDataset.ts b/src/dataset/domain/useCases/createDataset.ts index abe0a943f..bc231855c 100644 --- a/src/dataset/domain/useCases/createDataset.ts +++ b/src/dataset/domain/useCases/createDataset.ts @@ -4,9 +4,10 @@ import { DatasetDTO } from './DTOs/DatasetDTO' export function createDataset( datasetRepository: DatasetRepository, dataset: DatasetDTO, - collectionId: string + collectionId: string, + datasetType?: string ): 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/infrastructure/mappers/JSDatasetMapper.ts b/src/dataset/infrastructure/mappers/JSDatasetMapper.ts index a147285e5..99e615d13 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, @@ -100,7 +101,8 @@ export class JSDatasetMapper { latestPublishedVersionMinorNumber ), JSDatasetMapper.toRequiresMajorVersionUpdate(datasetVersionDiff), - fileStore + fileStore, + datasetType ).build() } diff --git a/src/dataset/infrastructure/repositories/DatasetJSDataverseRepository.ts b/src/dataset/infrastructure/repositories/DatasetJSDataverseRepository.ts index 7dd2f3354..52546243b 100644 --- a/src/dataset/infrastructure/repositories/DatasetJSDataverseRepository.ts +++ b/src/dataset/infrastructure/repositories/DatasetJSDataverseRepository.ts @@ -43,7 +43,9 @@ import { unlinkDataset, getDatasetLinkedCollections, updateTermsOfAccess, - updateDatasetLicense + updateDatasetLicense, + DatasetType, + getDatasetAvailableDatasetTypes } from '@iqss/dataverse-client-javascript' import { JSDatasetMapper } from '../mappers/JSDatasetMapper' import { DatasetPaginationInfo } from '../../domain/models/DatasetPaginationInfo' @@ -267,7 +269,8 @@ export class DatasetJSDataverseRepository implements DatasetRepository { datasetDetails.latestPublishedVersionMajorNumber, datasetDetails.latestPublishedVersionMinorNumber, datasetDetails.datasetVersionDiff, - datasetDetails.fileStore + datasetDetails.fileStore, + datasetDetails.jsDataset.datasetType ) }) .catch((error: ReadError) => { @@ -325,9 +328,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 })) @@ -429,6 +436,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. 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 133e043af..96e094ab0 100644 --- a/src/sections/Route.enum.ts +++ b/src/sections/Route.enum.ts @@ -80,6 +80,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 0460ee96e..ba565097d 100644 --- a/src/sections/create-dataset/CreateDataset.tsx +++ b/src/sections/create-dataset/CreateDataset.tsx @@ -19,6 +19,9 @@ import { useGetTemplatesByCollectionId } from '@/dataset/domain/hooks/useGetTemp import { type Template } from '@/templates/domain/models/Template' import { DatasetTemplateSelect } from './dataset-template-select/DatasetTemplateSelect' import { TemplateRepository } from '@/templates/domain/repositories/TemplateRepository' +import { useGetAvailableDatasetTypes } from '@/dataset/domain/hooks/useGetAvailableDatasetTypes' +import { DatasetType } from '@/dataset/domain/models/DatasetType' +import { DatasetTypeSelect } from './dataset-type-select/DatasetTypeSelect' interface CreateDatasetProps { datasetRepository: DatasetRepository @@ -39,6 +42,7 @@ export function CreateDataset({ const { isModalOpen, hideModal } = useNotImplementedModal() const { setIsLoading } = useLoading() const [selectedTemplate, setSelectedTemplate] = useState