Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/copy_labels.yml
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion dev-env/.env.example
Original file line number Diff line number Diff line change
@@ -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_ACCESS_KEY>
S3_SECRET_KEY=<S3_SECRET_KEY>
12 changes: 10 additions & 2 deletions public/locales/en/collection.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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."
}
1 change: 1 addition & 0 deletions src/collection/domain/models/Collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ export interface Collection {
contacts: CollectionContact[]
isMetadataBlockRoot: boolean
isFacetRoot: boolean
childCount: number
}
1 change: 1 addition & 0 deletions src/collection/domain/repositories/CollectionRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { CollectionFeaturedItemsDTO } from '../useCases/DTOs/CollectionFeaturedI
export interface CollectionRepository {
getById: (id?: string) => Promise<Collection>
create(collection: CollectionDTO, hostCollection?: string): Promise<number>
delete(collectionIdOrAlias: number | string): Promise<void>
getFacets(collectionIdOrAlias?: number | string): Promise<CollectionFacet[]>
getUserPermissions(collectionIdOrAlias?: number | string): Promise<CollectionUserPermissions>
publish(collectionIdOrAlias: number | string): Promise<void>
Expand Down
11 changes: 11 additions & 0 deletions src/collection/domain/useCases/deleteCollection.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
return collectionRepository.delete(collectionIdOrAlias).catch((error: WriteError | unknown) => {
throw error
})
}
3 changes: 2 additions & 1 deletion src/collection/infrastructure/mappers/JSCollectionMapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -36,6 +37,10 @@ export class CollectionJSDataverseRepository implements CollectionRepository {
.then((newCollectionIdentifier) => newCollectionIdentifier)
}

delete(collectionIdOrAlias: number | string): Promise<void> {
return deleteCollection.execute(collectionIdOrAlias)
}

getFacets(collectionIdOrAlias?: number | string): Promise<CollectionFacet[]> {
return getCollectionFacets.execute(collectionIdOrAlias).then((facets) => facets)
}
Expand Down
9 changes: 8 additions & 1 deletion src/sections/collection/Collection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -107,7 +108,13 @@ export function Collection({
collectionId={collection.id}
/>
)}
{showEditButton && <EditCollectionDropdown collection={collection} />}
{showEditButton && (
<EditCollectionDropdown
collection={collection}
canUserDeleteCollection={canUserDeleteCollection}
collectionRepository={collectionRepository}
/>
)}
</ButtonGroup>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export function CollectionCardInfo({
<LinkToPage
type={DvObjectType.COLLECTION}
page={Route.COLLECTIONS}
searchParams={{ id: collectionPreview.parentCollectionAlias.toString() }}>
searchParams={{ id: collectionPreview.parentCollectionAlias?.toString() }}>
{collectionPreview.parentCollectionName}
</LinkToPage>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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 (
<DropdownButton
id="edit-collection-dropdown"
Expand All @@ -55,6 +69,17 @@ export const EditCollectionDropdown = ({ collection }: EditCollectionDropdownPro
<DropdownButtonItem onClick={onClickEditFeaturedItems}>
{t('featuredItems.title')}
</DropdownButtonItem>

{canCollectionBeDeleted && (
<>
<DropdownSeparator />
<DeleteCollectionButton
collectionId={collection.id}
parentCollection={CollectionHelper.getParentCollection(collection.hierarchy)}
collectionRepository={collectionRepository}
/>
</>
)}
</DropdownButton>
)
}
Original file line number Diff line number Diff line change
@@ -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 (
<>
<DropdownButtonItem onClick={handleOpenModal}>
{t('editCollection.deleteCollection')}
</DropdownButtonItem>
<ConfirmDeleteCollectionModal
show={showConfirmationModal}
handleClose={handleCloseModal}
handleDelete={() => handleDeleteCollection(collectionId)}
isDeletingCollection={isDeletingCollection}
errorDeletingCollection={errorDeletingCollection}
/>
</>
)
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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 (
<Modal show={show} onHide={isDeletingCollection ? () => {} : handleClose} centered size="lg">
<Modal.Header>
<Modal.Title>{t('deleteCollectionModal.title')}</Modal.Title>
</Modal.Header>
<Modal.Body>
<Stack gap={2}>
<Stack direction="horizontal" gap={2} className={styles.message}>
<ExclamationTriangle /> <span>{t('deleteCollectionModal.message')}</span>
</Stack>
{errorDeletingCollection && (
<Stack direction="horizontal" gap={2} className={`${styles.message} ${styles.error}`}>
<ExclamationCircleFill /> <span>{errorDeletingCollection}</span>
</Stack>
)}
</Stack>
</Modal.Body>
<Modal.Footer>
<Button
variant="secondary"
onClick={handleClose}
type="button"
disabled={isDeletingCollection}>
{tShared('cancel')}
</Button>
<Button
variant="danger"
onClick={handleDelete}
type="button"
disabled={isDeletingCollection}>
<Stack direction="horizontal" gap={1}>
{tShared('delete')}
{isDeletingCollection && <Spinner variant="light" animation="border" size="sm" />}
</Stack>
</Button>
</Modal.Footer>
</Modal>
)
}
Original file line number Diff line number Diff line change
@@ -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<void>
}

export const useDeleteCollection = ({
collectionRepository,
onSuccessfulDelete
}: UseDeleteCollection): UseDeleteCollectionReturn => {
const { t } = useTranslation('collection')
const [isDeletingCollection, setIsDeletingCollection] = useState<boolean>(false)
const [errorDeletingCollection, setErrorDeletingCollection] = useState<string | null>(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
}
}
Loading