diff --git a/package-lock.json b/package-lock.json index 543455944..9651fc310 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.0.0-alpha.67", + "@iqss/dataverse-client-javascript": "2.0.0-alpha.75", "@iqss/dataverse-design-system": "*", "@istanbuljs/nyc-config-typescript": "1.0.2", "@tanstack/react-table": "8.9.2", @@ -81,7 +81,7 @@ "babel-plugin-named-exports-order": "0.0.2", "chai": "4.3.7", "chai-as-promised": "7.1.1", - "chromatic": "^13.3.0", + "chromatic": "13.3.0", "concurrently": "8.0.1", "cypress": "15.2.0", "cypress-vite": "1.4.0", @@ -1954,14 +1954,14 @@ }, "node_modules/@iqss/dataverse-client-javascript": { "name": "@IQSS/dataverse-client-javascript", - "version": "2.0.0-alpha.67", - "resolved": "https://npm.pkg.github.com/download/@IQSS/dataverse-client-javascript/2.0.0-alpha.67/0ee4f1c8e03eb3ef688d6b034e84003c9e155d3b", - "integrity": "sha512-uEAGtwXz7LYkBfWCBRktgb5d8oba6yPH9YWnVFhI40UqgdB1sQ/WWCDhZTn5LFsaQY+1XBzOoTjwrQ2HGz94og==", + "version": "2.0.0-alpha.75", + "resolved": "https://npm.pkg.github.com/download/@IQSS/dataverse-client-javascript/2.0.0-alpha.75/7c205be675dd5b0bd2def5ede62f69c7b031c38f", + "integrity": "sha512-V/NZXUMCJF9K9p31EXd7EU5WSXmS4X7El2TNs0IdiVvh/oeQKXQkdmqQg8BJNC/LS/oVq6ytUT3PPl4Trjfqlw==", "license": "MIT", "dependencies": { "@types/node": "^18.15.11", "@types/turndown": "^5.0.1", - "axios": "^1.7.2", + "axios": "^1.12.2", "turndown": "^7.1.2", "typescript": "^4.9.5" } @@ -30401,7 +30401,7 @@ "@testing-library/cypress": "10.1.0", "@vitejs/plugin-react": "4.3.1", "axe-playwright": "1.2.3", - "chromatic": "^13.3.0", + "chromatic": "13.3.0", "cypress": "15.2.0", "react": "18.2.0", "vite": "5.4.20", diff --git a/package.json b/package.json index 34008bc0b..9fdf1297a 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.0.0-alpha.67", + "@iqss/dataverse-client-javascript": "2.0.0-alpha.75", "@iqss/dataverse-design-system": "*", "@istanbuljs/nyc-config-typescript": "1.0.2", "@tanstack/react-table": "8.9.2", diff --git a/public/locales/en/header.json b/public/locales/en/header.json index eef91ae01..dd4e57783 100644 --- a/public/locales/en/header.json +++ b/public/locales/en/header.json @@ -8,6 +8,7 @@ "addData": "Add Data", "newCollection": "New Collection", "newDataset": "New Dataset", + "newReview": "New Review", "accountInfo": "Account Information", "apiToken": "API Token", "myData": "My Data" 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 30e10ea5f..b5e8f9e8d 100644 --- a/src/dataset/domain/models/Dataset.ts +++ b/src/dataset/domain/models/Dataset.ts @@ -430,7 +430,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 { @@ -525,7 +526,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() } @@ -597,7 +599,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..56a5ed436 --- /dev/null +++ b/src/dataset/domain/models/DatasetType.ts @@ -0,0 +1,6 @@ +export interface DatasetType { + id?: number + name: string + linkedMetadataBlocks?: string[] + availableLicenses?: string[] +} diff --git a/src/dataset/domain/repositories/DatasetRepository.ts b/src/dataset/domain/repositories/DatasetRepository.ts index b87314506..27942a941 100644 --- a/src/dataset/domain/repositories/DatasetRepository.ts +++ b/src/dataset/domain/repositories/DatasetRepository.ts @@ -9,6 +9,7 @@ import { DatasetDeaccessionDTO } from '../useCases/DTOs/DatasetDTO' import { DatasetDownloadCount } from '../models/DatasetDownloadCount' import { FormattedCitation, CitationFormat } from '../models/DatasetCitation' import { DatasetTemplate } from '../models/DatasetTemplate' +import { DatasetType } from '../models/DatasetType' export interface DatasetRepository { getByPersistentId: ( @@ -26,7 +27,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, @@ -55,5 +60,6 @@ export interface DatasetRepository { version: string, format: CitationFormat ) => Promise + getAvailableDatasetTypes: () => Promise getTemplates: (collectionIdOrAlias: number | string) => 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 edb9d5a70..5f020b410 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, @@ -99,7 +100,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 8d7d73d51..08badfaa2 100644 --- a/src/dataset/infrastructure/repositories/DatasetJSDataverseRepository.ts +++ b/src/dataset/infrastructure/repositories/DatasetJSDataverseRepository.ts @@ -34,7 +34,9 @@ import { deleteDatasetDraft, getDatasetCitationInOtherFormats, getDatasetAvailableCategories, - getDatasetTemplates + getDatasetTemplates, + getDatasetAvailableDatasetTypes, + DatasetType } from '@iqss/dataverse-client-javascript' import { JSDatasetMapper } from '../mappers/JSDatasetMapper' import { DatasetPaginationInfo } from '../../domain/models/DatasetPaginationInfo' @@ -254,7 +256,8 @@ export class DatasetJSDataverseRepository implements DatasetRepository { datasetDetails.latestPublishedVersionMajorNumber, datasetDetails.latestPublishedVersionMinorNumber, datasetDetails.datasetVersionDiff, - datasetDetails.fileStore + datasetDetails.fileStore, + datasetDetails.jsDataset.datasetType ) }) .catch((error: ReadError) => { @@ -312,9 +315,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 })) @@ -397,6 +404,9 @@ export class DatasetJSDataverseRepository implements DatasetRepository { getAvailableCategories(datasetId: string | number): Promise { return getDatasetAvailableCategories.execute(datasetId) } + getAvailableDatasetTypes(): Promise { + return getDatasetAvailableDatasetTypes.execute() + } getTemplates(collectionIdOrAlias: number | string): Promise { return getDatasetTemplates.execute(collectionIdOrAlias) 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 d7d64fb7a..7bda291c6 100644 --- a/src/sections/Route.enum.ts +++ b/src/sections/Route.enum.ts @@ -79,6 +79,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/collection/Collection.tsx b/src/sections/collection/Collection.tsx index 7dc0f5c27..1ed9aaedc 100644 --- a/src/sections/collection/Collection.tsx +++ b/src/sections/collection/Collection.tsx @@ -22,6 +22,7 @@ import { CollectionHelper } from './CollectionHelper' import { ContactRepository } from '@/contact/domain/repositories/ContactRepository' import { NotFoundPage } from '../not-found-page/NotFoundPage' import styles from './Collection.module.scss' +import { DatasetRepository } from '@/dataset/domain/repositories/DatasetRepository' interface CollectionProps { collectionRepository: CollectionRepository @@ -31,6 +32,7 @@ interface CollectionProps { accountCreated: boolean infiniteScrollEnabled?: boolean contactRepository: ContactRepository + datasetRepository: DatasetRepository } export function Collection({ @@ -39,7 +41,8 @@ export function Collection({ created, collectionQueryParams, contactRepository, - accountCreated + accountCreated, + datasetRepository }: CollectionProps) { useScrollTop() const { previousPath } = useHistoryTracker() @@ -142,6 +145,7 @@ export function Collection({ collectionId={collection.id} canAddCollection={canUserAddCollection} canAddDataset={canUserAddDataset} + datasetRepository={datasetRepository} /> ) : null } diff --git a/src/sections/collection/CollectionFactory.tsx b/src/sections/collection/CollectionFactory.tsx index 8f4d97a9c..f34e9a479 100644 --- a/src/sections/collection/CollectionFactory.tsx +++ b/src/sections/collection/CollectionFactory.tsx @@ -6,9 +6,11 @@ import { Collection } from './Collection' import { INFINITE_SCROLL_ENABLED } from './config' import { useGetCollectionQueryParams } from './useGetCollectionQueryParams' import { ACCOUNT_CREATED_SESSION_STORAGE_KEY } from './AccountCreatedAlert' +import { DatasetJSDataverseRepository } from '@/dataset/infrastructure/repositories/DatasetJSDataverseRepository' const collectionRepository = new CollectionJSDataverseRepository() const contactRepository = new ContactJSDataverseRepository() +const datasetRepository = new DatasetJSDataverseRepository() export class CollectionFactory { static create(): ReactElement { @@ -39,6 +41,7 @@ function CollectionWithSearchParams() { accountCreated={accountCreated} infiniteScrollEnabled={INFINITE_SCROLL_ENABLED} contactRepository={contactRepository} + datasetRepository={datasetRepository} /> ) } 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 18a304b02..7ff550df7 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 @@ -41,6 +41,9 @@ export function EditDatasetMenu({ dataset, datasetRepository }: EditDatasetMenuP const handleOnSelect = (eventKey: EditDatasetMenuItems | string | null) => { const searchParams = new URLSearchParams() searchParams.set(QueryParamKey.PERSISTENT_ID, dataset.persistentId) + if (dataset.datasetType) { + searchParams.set(QueryParamKey.DATASET_TYPE, dataset.datasetType) + } if (dataset.version.publishingStatus === DatasetPublishingStatus.DRAFT) { searchParams.set(QueryParamKey.VERSION, DatasetNonNumericVersionSearchParam.DRAFT) diff --git a/src/sections/layout/header/Header.tsx b/src/sections/layout/header/Header.tsx index 95edfde45..f46d5bae3 100644 --- a/src/sections/layout/header/Header.tsx +++ b/src/sections/layout/header/Header.tsx @@ -10,11 +10,13 @@ import { LoggedInHeaderActions } from './LoggedInHeaderActions' import { CollectionRepository } from '@/collection/domain/repositories/CollectionRepository' import { encodeReturnToPathInStateQueryParam } from '@/sections/auth-callback/AuthCallback' import styles from './Header.module.scss' +import { DatasetRepository } from '@/dataset/domain/repositories/DatasetRepository' interface HeaderProps { collectionRepository: CollectionRepository + datasetRepository: DatasetRepository } -export function Header({ collectionRepository }: HeaderProps) { +export function Header({ collectionRepository, datasetRepository }: HeaderProps) { const { t } = useTranslation('header') const { user } = useSession() const { pathname, search } = useLocation() @@ -36,7 +38,11 @@ export function Header({ collectionRepository }: HeaderProps) { }} className={styles.navbar}> {user ? ( - + ) : (