diff --git a/.github/workflows/copy_labels.yml b/.github/workflows/copy_labels.yml new file mode 100644 index 000000000..f9b39c8a6 --- /dev/null +++ b/.github/workflows/copy_labels.yml @@ -0,0 +1,30 @@ +name: Copy labels from issue to pull request + +on: + pull_request: + types: [opened] + +jobs: + copy-labels: + runs-on: ubuntu-latest + name: Copy labels from linked issues + steps: + - name: copy-labels + uses: michalvankodev/copy-issue-labels@v1.3.0 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + labels-to-exclude: | + Size: 3 + Size: 10 + Size: 20 + Size: 30 + Size: 50 + Size: 80 + Size: 130 + Original size: 3 + Original size: 10 + Original size: 20 + Original size: 30 + Original size: 50 + Original size: 80 + Original size: 130 diff --git a/dev-env/.env.example b/dev-env/.env.example index 972aa9026..0064a6676 100644 --- a/dev-env/.env.example +++ b/dev-env/.env.example @@ -1,6 +1,6 @@ POSTGRES_VERSION=13 DATAVERSE_DB_USER=dataverse -SOLR_VERSION=9.3.0 +SOLR_VERSION=9.8.0 REGISTRY=docker.io S3_ACCESS_KEY= S3_SECRET_KEY= diff --git a/public/locales/en/collection.json b/public/locales/en/collection.json index 71b171f5e..47705c97f 100644 --- a/public/locales/en/collection.json +++ b/public/locales/en/collection.json @@ -50,7 +50,8 @@ "editedAlert": "You have successfully updated your collection!", "editCollection": { "edit": "Edit", - "generalInfo": "General Information" + "generalInfo": "General Information", + "deleteCollection": "Delete Collection" }, "featuredItems": { "title": "Featured Items", @@ -59,5 +60,12 @@ "nextLabel": "Go to next slide", "dotLabel": "Go to slide" } - } + }, + "deleteCollectionModal": { + "title": "Delete Collection", + "message": "Are you sure you want to delete your collection? You cannot undelete this collection.", + "delete": "Delete" + }, + "collectionDeletedSuccess": "Your collection has been deleted.", + "defaultCollectionDeleteError": "Something went wrong deleting the collection. Try again later." } diff --git a/src/collection/domain/models/Collection.ts b/src/collection/domain/models/Collection.ts index dd7159ea2..a929e632e 100644 --- a/src/collection/domain/models/Collection.ts +++ b/src/collection/domain/models/Collection.ts @@ -15,4 +15,5 @@ export interface Collection { contacts: CollectionContact[] isMetadataBlockRoot: boolean isFacetRoot: boolean + childCount: number } diff --git a/src/collection/domain/repositories/CollectionRepository.ts b/src/collection/domain/repositories/CollectionRepository.ts index 34e1862d2..fd7eb0586 100644 --- a/src/collection/domain/repositories/CollectionRepository.ts +++ b/src/collection/domain/repositories/CollectionRepository.ts @@ -11,6 +11,7 @@ import { CollectionFeaturedItemsDTO } from '../useCases/DTOs/CollectionFeaturedI export interface CollectionRepository { getById: (id?: string) => Promise create(collection: CollectionDTO, hostCollection?: string): Promise + delete(collectionIdOrAlias: number | string): Promise getFacets(collectionIdOrAlias?: number | string): Promise getUserPermissions(collectionIdOrAlias?: number | string): Promise publish(collectionIdOrAlias: number | string): Promise diff --git a/src/collection/domain/useCases/deleteCollection.ts b/src/collection/domain/useCases/deleteCollection.ts new file mode 100644 index 000000000..cac0450b3 --- /dev/null +++ b/src/collection/domain/useCases/deleteCollection.ts @@ -0,0 +1,11 @@ +import { WriteError } from '@iqss/dataverse-client-javascript' +import { CollectionRepository } from '../repositories/CollectionRepository' + +export function deleteCollection( + collectionRepository: CollectionRepository, + collectionIdOrAlias: number | string +): Promise { + return collectionRepository.delete(collectionIdOrAlias).catch((error: WriteError | unknown) => { + throw error + }) +} diff --git a/src/collection/infrastructure/mappers/JSCollectionMapper.ts b/src/collection/infrastructure/mappers/JSCollectionMapper.ts index 1d7312c0c..b409a281c 100644 --- a/src/collection/infrastructure/mappers/JSCollectionMapper.ts +++ b/src/collection/infrastructure/mappers/JSCollectionMapper.ts @@ -26,7 +26,8 @@ export class JSCollectionMapper { type: jsCollection.type, contacts: jsCollection.contacts ?? [], isMetadataBlockRoot: jsCollection.isMetadataBlockRoot, - isFacetRoot: jsCollection.isFacetRoot + isFacetRoot: jsCollection.isFacetRoot, + childCount: jsCollection.childCount } } diff --git a/src/collection/infrastructure/repositories/CollectionJSDataverseRepository.ts b/src/collection/infrastructure/repositories/CollectionJSDataverseRepository.ts index cb892cff0..abdf1f77d 100644 --- a/src/collection/infrastructure/repositories/CollectionJSDataverseRepository.ts +++ b/src/collection/infrastructure/repositories/CollectionJSDataverseRepository.ts @@ -10,7 +10,8 @@ import { updateCollection, getCollectionFeaturedItems, updateCollectionFeaturedItems, - deleteCollectionFeaturedItems + deleteCollectionFeaturedItems, + deleteCollection } from '@iqss/dataverse-client-javascript' import { JSCollectionMapper } from '../mappers/JSCollectionMapper' import { CollectionDTO } from '../../domain/useCases/DTOs/CollectionDTO' @@ -36,6 +37,10 @@ export class CollectionJSDataverseRepository implements CollectionRepository { .then((newCollectionIdentifier) => newCollectionIdentifier) } + delete(collectionIdOrAlias: number | string): Promise { + return deleteCollection.execute(collectionIdOrAlias) + } + getFacets(collectionIdOrAlias?: number | string): Promise { return getCollectionFacets.execute(collectionIdOrAlias).then((facets) => facets) } diff --git a/src/sections/collection/Collection.tsx b/src/sections/collection/Collection.tsx index e1992098c..21b662238 100644 --- a/src/sections/collection/Collection.tsx +++ b/src/sections/collection/Collection.tsx @@ -52,6 +52,7 @@ export function Collection({ const canUserEditCollection = Boolean(collectionUserPermissions?.canEditCollection) const canUserAddDataset = Boolean(collectionUserPermissions?.canAddDataset) const canUserPublishCollection = Boolean(collectionUserPermissions?.canPublishCollection) + const canUserDeleteCollection = Boolean(collectionUserPermissions?.canDeleteCollection) const showAddDataActions = canUserAddCollection || canUserAddDataset const showPublishButton = !collection?.isReleased && canUserPublishCollection @@ -107,7 +108,13 @@ export function Collection({ collectionId={collection.id} /> )} - {showEditButton && } + {showEditButton && ( + + )} )} diff --git a/src/sections/collection/collection-items-panel/items-list/collection-card/CollectionCardInfo.tsx b/src/sections/collection/collection-items-panel/items-list/collection-card/CollectionCardInfo.tsx index 7fcaf07d6..f1368c6f7 100644 --- a/src/sections/collection/collection-items-panel/items-list/collection-card/CollectionCardInfo.tsx +++ b/src/sections/collection/collection-items-panel/items-list/collection-card/CollectionCardInfo.tsx @@ -31,7 +31,7 @@ export function CollectionCardInfo({ + searchParams={{ id: collectionPreview.parentCollectionAlias?.toString() }}> {collectionPreview.parentCollectionName} )} diff --git a/src/sections/collection/edit-collection-dropdown/EditCollectionDropdown.tsx b/src/sections/collection/edit-collection-dropdown/EditCollectionDropdown.tsx index 605502640..99cf26111 100644 --- a/src/sections/collection/edit-collection-dropdown/EditCollectionDropdown.tsx +++ b/src/sections/collection/edit-collection-dropdown/EditCollectionDropdown.tsx @@ -11,13 +11,22 @@ import { import { PencilFill } from 'react-bootstrap-icons' import { Collection } from '@/collection/domain/models/Collection' import { RouteWithParams } from '@/sections/Route.enum' +import { CollectionRepository } from '@/collection/domain/repositories/CollectionRepository' +import { CollectionHelper } from '../CollectionHelper' +import { DeleteCollectionButton } from './delete-collection-button/DeleteCollectionButton' import styles from './EditCollectionDropdown.module.scss' interface EditCollectionDropdownProps { collection: Collection + canUserDeleteCollection: boolean + collectionRepository: CollectionRepository } -export const EditCollectionDropdown = ({ collection }: EditCollectionDropdownProps) => { +export const EditCollectionDropdown = ({ + collection, + collectionRepository, + canUserDeleteCollection +}: EditCollectionDropdownProps) => { const { t } = useTranslation('collection') const navigate = useNavigate() @@ -29,6 +38,11 @@ export const EditCollectionDropdown = ({ collection }: EditCollectionDropdownPro navigate(RouteWithParams.EDIT_COLLECTION_FEATURED_ITEMS(collection.id)) } + const canCollectionBeDeleted = + canUserDeleteCollection && + !CollectionHelper.isRootCollection(collection.hierarchy) && + collection.childCount === 0 + return ( {t('featuredItems.title')} + + {canCollectionBeDeleted && ( + <> + + + + )} ) } diff --git a/src/sections/collection/edit-collection-dropdown/delete-collection-button/DeleteCollectionButton.tsx b/src/sections/collection/edit-collection-dropdown/delete-collection-button/DeleteCollectionButton.tsx new file mode 100644 index 000000000..7e905041a --- /dev/null +++ b/src/sections/collection/edit-collection-dropdown/delete-collection-button/DeleteCollectionButton.tsx @@ -0,0 +1,57 @@ +import { useState } from 'react' +import { toast } from 'react-toastify' +import { useTranslation } from 'react-i18next' +import { useNavigate } from 'react-router-dom' +import { DropdownButtonItem } from '@iqss/dataverse-design-system' +import { CollectionRepository } from '@/collection/domain/repositories/CollectionRepository' +import { ConfirmDeleteCollectionModal } from './confirm-delete-collection-modal/ConfirmDeleteCollectionModal' +import { useDeleteCollection } from './useDeleteCollection' +import { UpwardHierarchyNode } from '@/shared/hierarchy/domain/models/UpwardHierarchyNode' +import { RouteWithParams } from '@/sections/Route.enum' + +interface DeleteCollectionButtonProps { + collectionId: string + parentCollection: UpwardHierarchyNode | undefined + collectionRepository: CollectionRepository +} + +export const DeleteCollectionButton = ({ + collectionId, + parentCollection, + collectionRepository +}: DeleteCollectionButtonProps) => { + const [showConfirmationModal, setShowConfirmationModal] = useState(false) + const navigate = useNavigate() + const { t } = useTranslation('collection') + + const { handleDeleteCollection, isDeletingCollection, errorDeletingCollection } = + useDeleteCollection({ + collectionRepository, + onSuccessfulDelete: closeModalAndNavigateToParentCollection + }) + + const handleOpenModal = () => setShowConfirmationModal(true) + const handleCloseModal = () => setShowConfirmationModal(false) + + function closeModalAndNavigateToParentCollection() { + handleCloseModal() + + toast.success(t('collectionDeletedSuccess')) + navigate(RouteWithParams.COLLECTIONS(parentCollection?.id)) + } + + return ( + <> + + {t('editCollection.deleteCollection')} + + handleDeleteCollection(collectionId)} + isDeletingCollection={isDeletingCollection} + errorDeletingCollection={errorDeletingCollection} + /> + + ) +} diff --git a/src/sections/collection/edit-collection-dropdown/delete-collection-button/confirm-delete-collection-modal/ConfirmDeleteCollectionModal.module.scss b/src/sections/collection/edit-collection-dropdown/delete-collection-button/confirm-delete-collection-modal/ConfirmDeleteCollectionModal.module.scss new file mode 100644 index 000000000..ca7905490 --- /dev/null +++ b/src/sections/collection/edit-collection-dropdown/delete-collection-button/confirm-delete-collection-modal/ConfirmDeleteCollectionModal.module.scss @@ -0,0 +1,13 @@ +@import 'node_modules/@iqss/dataverse-design-system/src/lib/assets/styles/design-tokens/colors.module'; + +.message { + color: $dv-warning-color; + + &.error { + color: $dv-danger-color; + } + + svg { + min-width: fit-content; + } +} diff --git a/src/sections/collection/edit-collection-dropdown/delete-collection-button/confirm-delete-collection-modal/ConfirmDeleteCollectionModal.tsx b/src/sections/collection/edit-collection-dropdown/delete-collection-button/confirm-delete-collection-modal/ConfirmDeleteCollectionModal.tsx new file mode 100644 index 000000000..36bf6e154 --- /dev/null +++ b/src/sections/collection/edit-collection-dropdown/delete-collection-button/confirm-delete-collection-modal/ConfirmDeleteCollectionModal.tsx @@ -0,0 +1,62 @@ +import { useTranslation } from 'react-i18next' +import { Button, Modal, Spinner, Stack } from '@iqss/dataverse-design-system' +import { ExclamationCircleFill, ExclamationTriangle } from 'react-bootstrap-icons' +import styles from './ConfirmDeleteCollectionModal.module.scss' + +interface ConfirmDeleteCollectionModalProps { + show: boolean + handleClose: () => void + handleDelete: () => void + isDeletingCollection: boolean + errorDeletingCollection: string | null +} + +export const ConfirmDeleteCollectionModal = ({ + show, + handleClose, + handleDelete, + isDeletingCollection, + errorDeletingCollection +}: ConfirmDeleteCollectionModalProps) => { + const { t: tShared } = useTranslation('shared') + const { t } = useTranslation('collection') + + return ( + {} : handleClose} centered size="lg"> + + {t('deleteCollectionModal.title')} + + + + + {t('deleteCollectionModal.message')} + + {errorDeletingCollection && ( + + {errorDeletingCollection} + + )} + + + + + + + + ) +} diff --git a/src/sections/collection/edit-collection-dropdown/delete-collection-button/useDeleteCollection.ts b/src/sections/collection/edit-collection-dropdown/delete-collection-button/useDeleteCollection.ts new file mode 100644 index 000000000..3721174c9 --- /dev/null +++ b/src/sections/collection/edit-collection-dropdown/delete-collection-button/useDeleteCollection.ts @@ -0,0 +1,58 @@ +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { WriteError } from '@iqss/dataverse-client-javascript' +import { JSDataverseWriteErrorHandler } from '@/shared/helpers/JSDataverseWriteErrorHandler' +import { CollectionRepository } from '@/collection/domain/repositories/CollectionRepository' +import { deleteCollection } from '@/collection/domain/useCases/deleteCollection' +import { Utils } from '@/shared/helpers/Utils' + +interface UseDeleteCollection { + collectionRepository: CollectionRepository + onSuccessfulDelete?: () => void +} + +interface UseDeleteCollectionReturn { + isDeletingCollection: boolean + errorDeletingCollection: string | null + handleDeleteCollection: (collectionIdOrAlias: number | string) => Promise +} + +export const useDeleteCollection = ({ + collectionRepository, + onSuccessfulDelete +}: UseDeleteCollection): UseDeleteCollectionReturn => { + const { t } = useTranslation('collection') + const [isDeletingCollection, setIsDeletingCollection] = useState(false) + const [errorDeletingCollection, setErrorDeletingCollection] = useState(null) + + const handleDeleteCollection = async (collectionIdOrAlias: number | string) => { + setIsDeletingCollection(true) + + try { + await deleteCollection(collectionRepository, collectionIdOrAlias) + + // Wait before calling onSuccessfulDelete (which is redirecting to parent collection) + // Otherwise Search API will still return the deleted collection item + await Utils.sleep(3000) + + onSuccessfulDelete?.() + } catch (err: WriteError | unknown) { + if (err instanceof WriteError) { + const error = new JSDataverseWriteErrorHandler(err) + const formattedError = + error.getReasonWithoutStatusCode() ?? /* istanbul ignore next */ error.getErrorMessage() + setErrorDeletingCollection(formattedError) + } else { + setErrorDeletingCollection(t('defaultCollectionDeleteError')) + } + } finally { + setIsDeletingCollection(false) + } + } + + return { + isDeletingCollection, + errorDeletingCollection, + handleDeleteCollection + } +} diff --git a/src/sections/edit-collection-featured-items/featured-items-form/FeaturedItemsForm.tsx b/src/sections/edit-collection-featured-items/featured-items-form/FeaturedItemsForm.tsx index 191758b79..4e8970931 100644 --- a/src/sections/edit-collection-featured-items/featured-items-form/FeaturedItemsForm.tsx +++ b/src/sections/edit-collection-featured-items/featured-items-form/FeaturedItemsForm.tsx @@ -86,7 +86,7 @@ export const FeaturedItemsForm = ({ const handleOnRemoveField = (index: number) => remove(index) - const handleDragEnd = (event: DragEndEvent) => { + const handleDragEnd = /* istanbul ignore next */ (event: DragEndEvent) => { const { active, over } = event if (over && active.id !== over?.id) { diff --git a/src/shared/helpers/Utils.ts b/src/shared/helpers/Utils.ts index ba75bdaae..ea1a90206 100644 --- a/src/shared/helpers/Utils.ts +++ b/src/shared/helpers/Utils.ts @@ -12,4 +12,6 @@ export class Utils { timeoutId = setTimeout(() => fn(...args), delay) } } + + static sleep = (ms: number): Promise => new Promise((resolve) => setTimeout(resolve, ms)) } diff --git a/src/stories/collection/CollectionErrorMockRepository.ts b/src/stories/collection/CollectionErrorMockRepository.ts index 7882e9e80..c56a9a7a3 100644 --- a/src/stories/collection/CollectionErrorMockRepository.ts +++ b/src/stories/collection/CollectionErrorMockRepository.ts @@ -17,6 +17,14 @@ export class CollectionErrorMockRepository extends CollectionMockRepository { }) } + delete(_collectionIdOrAlias: number | string): Promise { + return new Promise((_resolve, reject) => { + setTimeout(() => { + reject('Something went wrong') + }, FakerHelper.loadingTimout()) + }) + } + getFacets(_collectionIdOrAlias: number | string): Promise { return new Promise((_resolve, reject) => { setTimeout(() => { diff --git a/src/stories/collection/CollectionLoadingMockRepository.ts b/src/stories/collection/CollectionLoadingMockRepository.ts index ac259661d..0d9a75ed6 100644 --- a/src/stories/collection/CollectionLoadingMockRepository.ts +++ b/src/stories/collection/CollectionLoadingMockRepository.ts @@ -13,15 +13,23 @@ export class CollectionLoadingMockRepository extends CollectionMockRepository { getById(_id?: string): Promise { return new Promise(() => {}) } + create(_collection: CollectionDTO, _hostCollection?: string): Promise { return new Promise(() => {}) } + + delete(_collectionIdOrAlias: number | string): Promise { + return new Promise(() => {}) + } + getFacets(_collectionIdOrAlias: number | string): Promise { return new Promise(() => {}) } + getUserPermissions(_collectionIdOrAlias: number | string): Promise { return new Promise(() => {}) } + getItems( _collectionId: string, _paginationInfo: CollectionItemsPaginationInfo, diff --git a/src/stories/collection/CollectionMockRepository.ts b/src/stories/collection/CollectionMockRepository.ts index f73a10fed..5e8d7bee9 100644 --- a/src/stories/collection/CollectionMockRepository.ts +++ b/src/stories/collection/CollectionMockRepository.ts @@ -23,6 +23,7 @@ export class CollectionMockRepository implements CollectionRepository { }, FakerHelper.loadingTimout()) }) } + create(_collection: CollectionDTO, _hostCollection?: string): Promise { return new Promise((resolve) => { setTimeout(() => { @@ -31,6 +32,14 @@ export class CollectionMockRepository implements CollectionRepository { }) } + delete(_collectionIdOrAlias: number | string): Promise { + return new Promise((resolve) => { + setTimeout(() => { + resolve() + }, FakerHelper.loadingTimout()) + }) + } + getFacets(_collectionIdOrAlias: number | string): Promise { return new Promise((resolve) => { setTimeout(() => { diff --git a/src/stories/collection/edit-collection-dropdown/ConfirmDeleteCollectionModal.stories.tsx b/src/stories/collection/edit-collection-dropdown/ConfirmDeleteCollectionModal.stories.tsx new file mode 100644 index 000000000..4c158545b --- /dev/null +++ b/src/stories/collection/edit-collection-dropdown/ConfirmDeleteCollectionModal.stories.tsx @@ -0,0 +1,52 @@ +import { WithI18next } from '@/stories/WithI18next' +import { Meta, StoryObj } from '@storybook/react' +import { ConfirmDeleteCollectionModal } from '@/sections/collection/edit-collection-dropdown/delete-collection-button/confirm-delete-collection-modal/ConfirmDeleteCollectionModal' + +const meta: Meta = { + title: 'Sections/Collection Page/EditCollectionDropdown/ConfirmDeleteCollectionModal', + component: ConfirmDeleteCollectionModal, + decorators: [WithI18next], + parameters: { + // Sets the delay for all stories. + chromatic: { delay: 15000, pauseAnimationAtEnd: true } + } +} + +export default meta +type Story = StoryObj + +export const Default: Story = { + render: () => ( + {}} + handleDelete={() => {}} + isDeletingCollection={false} + errorDeletingCollection={null} + show={true} + /> + ) +} + +export const DeleteInProgress: Story = { + render: () => ( + {}} + handleDelete={() => {}} + isDeletingCollection={true} + errorDeletingCollection={null} + show={true} + /> + ) +} + +export const WithError: Story = { + render: () => ( + {}} + handleDelete={() => {}} + isDeletingCollection={false} + errorDeletingCollection="Something went wrong deleting the collection. Try again later." + show={true} + /> + ) +} diff --git a/src/stories/collection/edit-collection-dropdown/EditCollectionDropdown.stories.tsx b/src/stories/collection/edit-collection-dropdown/EditCollectionDropdown.stories.tsx new file mode 100644 index 000000000..5eec5a625 --- /dev/null +++ b/src/stories/collection/edit-collection-dropdown/EditCollectionDropdown.stories.tsx @@ -0,0 +1,38 @@ +import { WithI18next } from '@/stories/WithI18next' +import { Meta, StoryObj } from '@storybook/react' +import { CollectionMockRepository } from '../CollectionMockRepository' +import { CollectionMother } from '@tests/component/collection/domain/models/CollectionMother' +import { EditCollectionDropdown } from '@/sections/collection/edit-collection-dropdown/EditCollectionDropdown' + +const meta: Meta = { + title: 'Sections/Collection Page/EditCollectionDropdown', + component: EditCollectionDropdown, + decorators: [WithI18next], + parameters: { + // Sets the delay for all stories. + chromatic: { delay: 15000, pauseAnimationAtEnd: true } + } +} + +export default meta +type Story = StoryObj + +export const Default: Story = { + render: () => ( + + ) +} + +export const WithDeleteCollectionButton: Story = { + render: () => ( + + ) +} diff --git a/tests/component/collection/domain/models/CollectionMother.ts b/tests/component/collection/domain/models/CollectionMother.ts index 069927809..71131ee03 100644 --- a/tests/component/collection/domain/models/CollectionMother.ts +++ b/tests/component/collection/domain/models/CollectionMother.ts @@ -21,6 +21,7 @@ export class CollectionMother { type: CollectionType.UNCATEGORIZED, isMetadataBlockRoot: true, isFacetRoot: true, + childCount: faker.datatype.number(), ...props } } @@ -73,6 +74,13 @@ export class CollectionMother { }) } + static createSubCollectionWithNoChildObjects(): Collection { + return CollectionMother.createWithOnlyRequiredFields({ + childCount: 0, + hierarchy: UpwardHierarchyNodeMother.createSubCollection() + }) + } + static createUserPermissions( props?: Partial ): CollectionUserPermissions { diff --git a/tests/component/sections/collection/edit-collection-dropdown/EditCollectionDropdown.spec.tsx b/tests/component/sections/collection/edit-collection-dropdown/EditCollectionDropdown.spec.tsx index b815cd122..2c8675909 100644 --- a/tests/component/sections/collection/edit-collection-dropdown/EditCollectionDropdown.spec.tsx +++ b/tests/component/sections/collection/edit-collection-dropdown/EditCollectionDropdown.spec.tsx @@ -1,7 +1,11 @@ +import { WriteError } from '@iqss/dataverse-client-javascript' +import { CollectionRepository } from '@/collection/domain/repositories/CollectionRepository' import { EditCollectionDropdown } from '@/sections/collection/edit-collection-dropdown/EditCollectionDropdown' import { CollectionMother } from '@tests/component/collection/domain/models/CollectionMother' import { UpwardHierarchyNodeMother } from '@tests/component/shared/hierarchy/domain/models/UpwardHierarchyNodeMother' +const collectionRepository = {} as CollectionRepository + const PARENT_COLLECTION_ID = 'root' const PARENT_COLLECTION_NAME = 'Root' const PARENT_COLLECTION_CONTACT_EMAIL = 'root@test.com' @@ -22,9 +26,18 @@ const rootCollection = CollectionMother.create({ const openDropdown = () => cy.findByRole('button', { name: /Edit/i }).click() describe('EditCollectionDropdown', () => { + beforeEach(() => { + collectionRepository.delete = cy.stub().resolves(undefined) + }) describe('dropdown header', () => { it('shows the collection name and id, but not affiliaton if not present', () => { - cy.mountAuthenticated() + cy.mountAuthenticated( + + ) openDropdown() @@ -46,7 +59,13 @@ describe('EditCollectionDropdown', () => { isFacetRoot: true, isMetadataBlockRoot: true }) - cy.mountAuthenticated() + cy.mountAuthenticated( + + ) openDropdown() @@ -57,7 +76,13 @@ describe('EditCollectionDropdown', () => { }) it('clicks the general info button', () => { - cy.mountAuthenticated() + cy.mountAuthenticated( + + ) openDropdown() @@ -65,10 +90,150 @@ describe('EditCollectionDropdown', () => { }) it('clicks the featured items button', () => { - cy.mountAuthenticated() + cy.mountAuthenticated( + + ) openDropdown() cy.findByRole('button', { name: /Featured Items/i }).click() }) + + describe('delete button', () => { + it('shows the delete button if user can delete collection, collection is not root and collection has no data', () => { + cy.mountAuthenticated( + + ) + + openDropdown() + + cy.findByRole('button', { name: /Delete Collection/i }).should('exist') + }) + + it('does not show the delete button if user cannot delete collection', () => { + cy.mountAuthenticated( + + ) + + openDropdown() + + cy.findByRole('button', { name: /Delete Collection/i }).should('not.exist') + }) + + it('does not show the delete button if collection is root', () => { + cy.mountAuthenticated( + + ) + + openDropdown() + + cy.findByRole('button', { name: /Delete Collection/i }).should('not.exist') + }) + + it('opens and close the delete collection confirmation modal', () => { + cy.mountAuthenticated( + + ) + + openDropdown() + + cy.findByRole('button', { name: /Delete Collection/i }).click() + cy.findByRole('dialog').should('exist') + + cy.findByRole('button', { name: /Cancel/i }).click() + + cy.findByRole('dialog').should('not.exist') + }) + + it('closes the modal and shows toast success message when delete collection succeeds', () => { + cy.mountAuthenticated( + + ) + + openDropdown() + + cy.findByRole('button', { name: /Delete Collection/i }).click() + cy.findByRole('dialog').should('exist') + + cy.findByRole('button', { name: /Delete/i }).click() + + // The loading spinner inside delete button + cy.findByRole('status').should('exist') + + cy.findByRole('dialog').should('not.exist') + cy.findByText(/Your collection has been deleted./) + .should('exist') + .should('be.visible') + }) + + it('shows the js-dataverse WriteError message if delete collection fails with a js-dataverse WriteError', () => { + collectionRepository.delete = cy + .stub() + .rejects(new WriteError('Testing delete error message.')) + + cy.mountAuthenticated( + + ) + + openDropdown() + + cy.findByRole('button', { name: /Delete Collection/i }).click() + cy.findByRole('dialog').should('exist') + + cy.findByRole('button', { name: /Delete/i }).click() + + cy.findByText('Testing delete error message.').should('exist') + }) + + it('shows the default error message if delete collection fails with not a js-dataverse WriteError', () => { + collectionRepository.delete = cy.stub().rejects('some error') + + cy.mountAuthenticated( + + ) + + openDropdown() + + cy.findByRole('button', { name: /Delete Collection/i }).click() + cy.findByRole('dialog').should('exist') + + cy.findByRole('button', { name: /Delete/i }).click() + + cy.findByText('Something went wrong deleting the collection. Try again later.').should( + 'exist' + ) + }) + }) }) diff --git a/tests/component/sections/edit-collection-featured-items/FeaturedItemsForm.spec.tsx b/tests/component/sections/edit-collection-featured-items/FeaturedItemsForm.spec.tsx index 9a1c16e1c..c404258fb 100644 --- a/tests/component/sections/edit-collection-featured-items/FeaturedItemsForm.spec.tsx +++ b/tests/component/sections/edit-collection-featured-items/FeaturedItemsForm.spec.tsx @@ -510,6 +510,40 @@ describe('FeaturedItemsForm', () => { .should('be.visible') }) }) + + it('should show toast error message when trying to add more than 10 featured items', () => { + const testFeaturedItems = Array.from({ length: 10 }, (_, index) => + CollectionFeaturedItemMother.createFeaturedItem({ + id: index, + displayOrder: index, + content: `

Featured Item ${index}

`, + imageFileUrl: undefined + }) + ) + + const formDefaultValues: FeaturedItemsFormData = { + featuredItems: FeaturedItemsFormHelper.defineFormDefaultFeaturedItems(testFeaturedItems) + } + + cy.mountAuthenticated( + + ) + + cy.findByTestId('featured-item-9').as('last-item').should('exist').should('be.visible') + + cy.get('@last-item').within(() => { + cy.get(`[aria-label="Add Featured Item"]`).should('exist').should('be.visible').click() + }) + + cy.findByText(/You can add up to 10 featured items./) + .should('exist') + .should('be.visible') + }) }) // TODO: This test is failing in CI sometimes, we need to investigate why and fix it diff --git a/tests/component/shared/hierarchy/domain/models/UpwardHierarchyNodeMother.ts b/tests/component/shared/hierarchy/domain/models/UpwardHierarchyNodeMother.ts index 73fc17ddb..6c37f6180 100644 --- a/tests/component/shared/hierarchy/domain/models/UpwardHierarchyNodeMother.ts +++ b/tests/component/shared/hierarchy/domain/models/UpwardHierarchyNodeMother.ts @@ -43,4 +43,13 @@ export class UpwardHierarchyNodeMother { isReleased: props?.isReleased ?? true }) } + + static createSubCollection(props?: Partial): UpwardHierarchyNode { + return this.create({ + ...props, + type: DvObjectType.COLLECTION, + name: props?.name ?? 'SubCollection', + parent: props?.parent ?? this.createCollection() + }) + } } diff --git a/tests/e2e-integration/integration/collection/CollectionJSDataverseRepository.spec.ts b/tests/e2e-integration/integration/collection/CollectionJSDataverseRepository.spec.ts index 0ca4b4e34..e68ae1978 100644 --- a/tests/e2e-integration/integration/collection/CollectionJSDataverseRepository.spec.ts +++ b/tests/e2e-integration/integration/collection/CollectionJSDataverseRepository.spec.ts @@ -39,7 +39,8 @@ const collectionExpected: Collection = { } ], isMetadataBlockRoot: false, - isFacetRoot: false + isFacetRoot: false, + childCount: 0 } describe('Collection JSDataverse Repository', () => { @@ -50,13 +51,12 @@ describe('Collection JSDataverse Repository', () => { it('gets the collection by id', async () => { const collectionResponse = await CollectionHelper.create('new-collection') - console.log('collectionResponse', collectionResponse.id) + await collectionRepository.getById(collectionResponse.id).then((collection) => { if (!collection) { throw new Error('Collection not found') } - console.log('collection', collection) expect(collection).to.deep.equal(collectionExpected) }) })