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
2 changes: 1 addition & 1 deletion assets/build/api/docs.jsonopenapi.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ import { createStyles } from 'antd-style'

export const useStyles = createStyles(({ token }) => ({
warningText: {
color: token.Colors.Brand.Warning.colorWarningText
color: token.Colors.Brand.Warning.colorWarningText,
// translations may carry line breaks (e.g. the restore note on its own line)
whiteSpace: 'pre-line'
},
pathList: {
maxHeight: 200,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@
*/

import React from 'react'
import { isUndefined } from 'lodash'
import { useTranslation } from 'react-i18next'
import { Accordion } from '@Pimcore/components/accordion/accordion'
import { useFormModal } from '@Pimcore/components/modal/form-modal/hooks/use-form-modal'
import { useAppDispatch } from '@Pimcore/app/store'
import { api as elementApi } from '@Pimcore/modules/element/element-api-slice.gen'
import trackError, { ApiError } from '@Pimcore/modules/app/error-handler'
import { api as elementApi } from '@Pimcore/modules/element/element-api-slice-enhanced'
import { type ElementType } from '@Pimcore/types/enums/element/element-type'
import { useStyles } from './use-batch-delete-confirm.styles'

Expand All @@ -28,6 +30,11 @@ export interface UseBatchDeleteConfirmReturn {
confirmBatchDelete: (params: ConfirmBatchDeleteParams) => Promise<void>
}

interface BatchDeleteInfo {
isPermanent: boolean
hasDependencies: boolean
}

export const useBatchDeleteConfirm = (): UseBatchDeleteConfirmReturn => {
const { t } = useTranslation()
const modal = useFormModal()
Expand All @@ -36,25 +43,55 @@ export const useBatchDeleteConfirm = (): UseBatchDeleteConfirmReturn => {

// The recycle bin threshold is evaluated per item on the backend (a plain item is always
// recoverable, a folder-like item only if its descendant count is within the configured
// limit) - so permanence can't be inferred from the selection size and has to be checked
// per item via the same delete-info endpoint the single-item/folder delete flow uses.
const isBatchDeletePermanent = async (elementType: ElementType, itemIds: number[]): Promise<boolean> => {
const canUseRecycleBinFlags = await Promise.all(
itemIds.map(async (id) => {
try {
const { data } = await dispatch(elementApi.endpoints.elementGetDeleteInfo.initiate({ elementType, id }))
return data?.canUseRecycleBin ?? true
} catch {
return true
// limit) - so permanence can't be inferred from the selection size. The batch endpoint
// aggregates it over all selected ids in a single request: canUseRecycleBin is false as
// soon as one item would be deleted permanently, hasDependencies is true as soon as one
// item has children or is referenced by other elements.
const fetchBatchDeleteInfo = async (elementType: ElementType, itemIds: number[]): Promise<BatchDeleteInfo | null> => {
const request = dispatch(elementApi.endpoints.elementBatchDeleteInfo.initiate({ elementType, body: { ids: itemIds } }))

try {
const response = await request

if ('error' in response) {
if (!isUndefined(response.error)) {
trackError(new ApiError(response.error))
}
})
)
return null
}

return canUseRecycleBinFlags.some((canUseRecycleBin) => !canUseRecycleBin)
return {
isPermanent: !response.data.canUseRecycleBin,
hasDependencies: response.data.hasDependencies
}
} finally {
request.reset()
}
}

// One complete sentence per state - composing fragments does not translate cleanly
const getWarningText = (info: BatchDeleteInfo, count: number): string | null => {
if (info.isPermanent && info.hasDependencies) {
return t('element.delete.batch.note.permanent-dependencies', { count })
}

if (info.isPermanent) {
return t('element.delete.batch.note', { count })
}

if (info.hasDependencies) {
return t('element.delete.batch.dependencies-warning.confirmed', { count })
}

return null
}

const confirmBatchDelete = async ({ elementType, itemIds, selectedRowsData, onOk }: ConfirmBatchDeleteParams): Promise<void> => {
const isPermanent = await isBatchDeletePermanent(elementType, itemIds)
const info = await fetchBatchDeleteInfo(elementType, itemIds)

if (info === null) {
return
}

const count = itemIds.length
const paths = itemIds.map(id => selectedRowsData?.[id]?.fullpath ?? String(id))
Expand All @@ -63,6 +100,7 @@ export const useBatchDeleteConfirm = (): UseBatchDeleteConfirmReturn => {
{paths.map((path) => <li key={ path }>{path}</li>)}
</ul>
)
const warningText = getWarningText(info, count)

modal.confirm({
title: t('element.delete.batch.title'),
Expand All @@ -72,10 +110,10 @@ export const useBatchDeleteConfirm = (): UseBatchDeleteConfirmReturn => {
{count > 5
? <Accordion items={ [{ key: 'paths', title: <span>{t('element.delete.batch.show-paths')}</span>, children: pathList }] } />
: pathList}
<p><span className={ styles.warningText }>{t('element.delete.batch.dependencies-warning')}</span></p>
{warningText !== null && <p><span className={ styles.warningText }>{warningText}</span></p>}
</>,
cancelText: t('cancel'),
okText: isPermanent ? t('element.delete.batch.ok.permanent') : t('element.delete.batch.ok'),
okText: info.isPermanent ? t('element.delete.batch.ok.permanent') : t('element.delete.batch.ok'),
onOk
})
}
Expand Down
64 changes: 32 additions & 32 deletions assets/js/src/core/modules/element/actions/delete/use-delete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import type { TreeNodeProps } from '@Pimcore/components/element-tree/node/tree-n
import type { GridContextMenuProps } from '@Pimcore/components/grid/grid'
import { Icon } from '@Pimcore/components/icon/icon'
import { useFormModal } from '@Pimcore/components/modal/form-modal/hooks/use-form-modal'
import trackError, { GeneralError } from '@Pimcore/modules/app/error-handler'
import { isUndefined } from 'lodash'
import trackError, { ApiError, GeneralError } from '@Pimcore/modules/app/error-handler'
import { useRefreshGrid } from '@Pimcore/modules/element/actions/refresh-grid/use-refresh-grid'
import { type Element, getElementKey } from '@Pimcore/modules/element/element-helper'
import { api as elementApi } from '@Pimcore/modules/element/element-api-slice.gen'
Expand Down Expand Up @@ -91,39 +92,38 @@ export const useDelete = (elementType: ElementType, cacheKey?: string): UseDelet
}
}

const confirmFolderDelete = async (id: number, label: string, parentId?: number, onFinish?: () => void): Promise<void> => {
const request = dispatch(elementApi.endpoints.elementGetDeleteInfo.initiate({ elementType, id }))

try {
const { data, error } = await request

if (!isUndefined(error)) {
trackError(new ApiError(error))
return
}

const canUseRecycleBin = data?.canUseRecycleBin ?? true

modal.confirm({
title: t('element.delete.folder.title'),
content: <>
<p><span className={ styles.warningText }>{t(canUseRecycleBin ? 'element.delete.folder.small.note' : 'element.delete.folder.large.note')}</span></p>
<p>{t('element.delete.folder.question')}</p>
<b>/{label}</b>
</>,
cancelText: t('cancel'),
okText: t(canUseRecycleBin ? 'element.delete.folder.ok' : 'element.delete.folder.ok.permanent'),
onOk: async () => { await runDeleteJob(id, parentId, onFinish) }
})
} finally {
request.unsubscribe()
}
}

const deleteElement = (id: number, label: string, parentId?: number, onFinish?: () => void, isFolder?: boolean): void => {
if (isFolder === true) {
void dispatch(elementApi.endpoints.elementGetDeleteInfo.initiate({ elementType, id }))
.then(({ data }) => {
const canUseRecycleBin = data?.canUseRecycleBin ?? true

if (canUseRecycleBin) {
modal.confirm({
title: t('element.delete.folder.title'),
content: <>
<p><span className={ styles.warningText }>{t('element.delete.folder.small.note')}</span></p>
<p>{t('element.delete.folder.question')}</p>
<b>/{label}</b>
</>,
cancelText: t('cancel'),
okText: t('element.delete.folder.ok'),
onOk: async () => { await runDeleteJob(id, parentId, onFinish) }
})
} else {
modal.confirm({
title: t('element.delete.folder.title'),
content: <>
<p><span className={ styles.warningText }>{t('element.delete.folder.large.note')}</span></p>
<p>{t('element.delete.folder.question')}</p>
<b>/{label}</b>
</>,
cancelText: t('cancel'),
okText: t('element.delete.folder.ok.permanent'),

onOk: async () => { await runDeleteJob(id, parentId, onFinish) }
})
}
})
void confirmFolderDelete(id, label, parentId, onFinish)
} else {
modal.confirm({
title: t('element.delete.confirmation.title'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ import { type DocumentPermissions } from '../document/document-api-slice.gen'
const api = baseApi.enhanceEndpoints({
addTagTypes: [tagNames.DATA_OBJECT_DETAIL, tagNames.ASSET_DETAIL, tagNames.ASSET_GRID, tagNames.DATA_OBJECT_GRID],
endpoints: {
// Read-only pre-check (POST only to carry the id list): must not invalidate element caches
elementBatchDeleteInfo: {
invalidatesTags: []
},
elementDelete: {
invalidatesTags: (result, error, args) => invalidatingTags.ELEMENT_DETAIL(args.elementType, args.id)
},
Expand Down Expand Up @@ -54,3 +58,5 @@ export const {
useElementLockMutation,
useElementUnlockMutation
} = api

export { api }
37 changes: 27 additions & 10 deletions assets/js/src/core/modules/element/element-api-slice.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ const injectedRtkApi = api
})
.injectEndpoints({
endpoints: (build) => ({
elementBatchDeleteInfo: build.mutation<ElementBatchDeleteInfoApiResponse, ElementBatchDeleteInfoApiArg>({
query: (queryArg) => ({
url: `/pimcore-studio/api/elements/${queryArg.elementType}/batch-delete-info`,
method: "POST",
body: queryArg.body,
}),
invalidatesTags: ["Elements"],
}),
elementDelete: build.mutation<ElementDeleteApiResponse, ElementDeleteApiArg>({
query: (queryArg) => ({
url: `/pimcore-studio/api/elements/${queryArg.elementType}/delete/${queryArg.id}`,
Expand Down Expand Up @@ -113,6 +121,14 @@ const injectedRtkApi = api
overrideExisting: false,
});
export { injectedRtkApi as api };
export type ElementBatchDeleteInfoApiResponse = /** status 200 Batch delete info for the given elements */ DeleteInfo;
export type ElementBatchDeleteInfoApiArg = {
/** Filter elements by matching element type. */
elementType: "asset" | "document" | "data-object";
body: {
ids?: number[];
};
};
export type ElementDeleteApiResponse =
/** status 201 Successfully created jobRun for deleting element and its children */ {
/** ID of created jobRun */
Expand Down Expand Up @@ -233,16 +249,6 @@ export type ElementResolveBySearchTermApiArg = {
/** Search term to filter elements by. */
searchTerm: string;
};
export type Error = {
/** Message */
message: string;
};
export type DevError = {
/** Message */
message: string;
/** Details */
details: string;
};
export type DeleteInfo = {
/** AdditionalAttributes */
additionalAttributes?: {
Expand All @@ -253,6 +259,16 @@ export type DeleteInfo = {
/** canUseRecycleBin */
canUseRecycleBin: boolean;
};
export type Error = {
/** Message */
message: string;
};
export type DevError = {
/** Message */
message: string;
/** Details */
details: string;
};
export type EditLockUser = {
/** Name of the user holding the lock */
name: string;
Expand Down Expand Up @@ -336,6 +352,7 @@ export type ElementUsage = {
totalCount?: number;
};
export const {
useElementBatchDeleteInfoMutation,
useElementDeleteMutation,
useElementGetDeleteInfoQuery,
useElementGetEditlockQuery,
Expand Down
Binary file not shown.
8 changes: 6 additions & 2 deletions translations/studio.de.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1193,13 +1193,17 @@ element.delete.folder.large.note: 'Bitte beachten: Der Ordner und seine Inhalte
element.delete.folder.ok: Löschen
element.delete.folder.ok.permanent: Endgültig löschen
element.delete.batch.title: Elemente löschen
element.delete.batch.note: 'Bitte beachten: Diese Elemente werden dauerhaft gelöscht und können nicht wiederhergestellt werden.'
element.delete.batch.note_one: 'Bitte beachten: Dieses Element wird dauerhaft gelöscht und kann nicht wiederhergestellt werden.'
element.delete.batch.note_other: 'Bitte beachten: Diese Elemente werden dauerhaft gelöscht und können nicht wiederhergestellt werden.'
element.delete.batch.note.permanent-dependencies_one: "Bitte beachten: Dieses Element hat Abhängigkeiten und wird dauerhaft gelöscht.\nEs kann nicht wiederhergestellt werden."
element.delete.batch.note.permanent-dependencies_other: "Bitte beachten: Diese Elemente haben Abhängigkeiten und werden dauerhaft gelöscht.\nSie können nicht wiederhergestellt werden."
element.delete.batch.question_one: 'Möchten Sie dieses Element wirklich löschen?'
element.delete.batch.question_other: 'Möchten Sie wirklich diese {{count}} Elemente löschen?'
element.delete.batch.ok: Löschen
element.delete.batch.ok.permanent: Endgültig löschen
element.delete.batch.show-paths: Elemente anzeigen
element.delete.batch.dependencies-warning: 'Es könnte Abhängigkeiten geben, trotzdem löschen?'
element.delete.batch.dependencies-warning.confirmed_one: 'Bitte beachten: Dieses Element hat Abhängigkeiten.'
element.delete.batch.dependencies-warning.confirmed_other: 'Bitte beachten: Diese Elemente haben Abhängigkeiten.'
element.open: Öffnen
element.toolbar.copy-id: ID kopieren
element.toolbar.copy-full-path-to-clipboard: Vollständigen Pfad kopieren
Expand Down
8 changes: 6 additions & 2 deletions translations/studio.en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1200,13 +1200,17 @@ element.delete.folder.large.note: 'Please note: The folder and its children will
element.delete.folder.ok: Delete
element.delete.folder.ok.permanent: Delete permanently
element.delete.batch.title: Delete items
element.delete.batch.note: 'Please note: These items will be permanently deleted and can not be restored.'
element.delete.batch.note_one: 'Please note: This item will be deleted permanently and can not be restored.'
element.delete.batch.note_other: 'Please note: These items will be deleted permanently and can not be restored.'
element.delete.batch.note.permanent-dependencies_one: "Please note: This item has dependencies and will be deleted permanently.\nIt can not be restored."
element.delete.batch.note.permanent-dependencies_other: "Please note: These items have dependencies and will be deleted permanently.\nThey can not be restored."
element.delete.batch.question_one: 'Do you really want to delete this item?'
element.delete.batch.question_other: 'Do you really want to delete these {{count}} items?'
element.delete.batch.ok: Delete
element.delete.batch.ok.permanent: Delete permanently
element.delete.batch.show-paths: Show items
element.delete.batch.dependencies-warning: 'There may be dependencies, delete anyway?'
element.delete.batch.dependencies-warning.confirmed_one: 'Please note: This item has dependencies.'
element.delete.batch.dependencies-warning.confirmed_other: 'Please note: These items have dependencies.'
element.open: Open
element.toolbar.copy-id: Copy ID
element.toolbar.copy-full-path-to-clipboard: Copy Full Path
Expand Down
8 changes: 6 additions & 2 deletions translations/studio.es.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1193,13 +1193,17 @@ element.delete.folder.large.note: 'Tenga en cuenta: La carpeta y su contenido se
element.delete.folder.ok: Eliminar
element.delete.folder.ok.permanent: Eliminar permanentemente
element.delete.batch.title: Eliminar elementos
element.delete.batch.note: 'Tenga en cuenta: Estos elementos se eliminarán permanentemente y no se podrán restaurar.'
element.delete.batch.note_one: 'Tenga en cuenta: Este elemento se eliminará permanentemente y no se podrá restaurar.'
element.delete.batch.note_other: 'Tenga en cuenta: Estos elementos se eliminarán permanentemente y no se podrán restaurar.'
element.delete.batch.note.permanent-dependencies_one: "Tenga en cuenta: Este elemento tiene dependencias y se eliminará permanentemente.\nNo se podrá restaurar."
element.delete.batch.note.permanent-dependencies_other: "Tenga en cuenta: Estos elementos tienen dependencias y se eliminarán permanentemente.\nNo se podrán restaurar."
element.delete.batch.question_one: '¿Realmente desea eliminar este elemento?'
element.delete.batch.question_other: '¿Realmente desea eliminar estos {{count}} elementos?'
element.delete.batch.ok: Eliminar
element.delete.batch.ok.permanent: Eliminar permanentemente
element.delete.batch.show-paths: Mostrar elementos
element.delete.batch.dependencies-warning: 'Puede haber dependencias, ¿eliminar de todas formas?'
element.delete.batch.dependencies-warning.confirmed_one: 'Tenga en cuenta: Este elemento tiene dependencias.'
element.delete.batch.dependencies-warning.confirmed_other: 'Tenga en cuenta: Estos elementos tienen dependencias.'
element.open: Abrir
element.toolbar.copy-id: Copiar ID
element.toolbar.copy-full-path-to-clipboard: Copiar ruta completa
Expand Down
8 changes: 6 additions & 2 deletions translations/studio.fr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1193,13 +1193,17 @@ element.delete.folder.large.note: 'Remarque : Le dossier et son contenu seront d
element.delete.folder.ok: Supprimer
element.delete.folder.ok.permanent: Supprimer définitivement
element.delete.batch.title: Supprimer des éléments
element.delete.batch.note: 'Remarque : Ces éléments seront supprimés définitivement et ne pourront pas être restaurés.'
element.delete.batch.note_one: 'Remarque : Cet élément sera supprimé définitivement et ne pourra pas être restauré.'
element.delete.batch.note_other: 'Remarque : Ces éléments seront supprimés définitivement et ne pourront pas être restaurés.'
element.delete.batch.note.permanent-dependencies_one: "Remarque : Cet élément a des dépendances et sera supprimé définitivement.\nIl ne pourra pas être restauré."
element.delete.batch.note.permanent-dependencies_other: "Remarque : Ces éléments ont des dépendances et seront supprimés définitivement.\nIls ne pourront pas être restaurés."
element.delete.batch.question_one: 'Voulez-vous vraiment supprimer cet élément ?'
element.delete.batch.question_other: 'Voulez-vous vraiment supprimer ces {{count}} éléments ?'
element.delete.batch.ok: Supprimer
element.delete.batch.ok.permanent: Supprimer définitivement
element.delete.batch.show-paths: Afficher les éléments
element.delete.batch.dependencies-warning: 'Il peut exister des dépendances, supprimer quand même ?'
element.delete.batch.dependencies-warning.confirmed_one: 'Remarque : Cet élément a des dépendances.'
element.delete.batch.dependencies-warning.confirmed_other: 'Remarque : Ces éléments ont des dépendances.'
element.open: Ouvrir
element.toolbar.copy-id: Copier l'ID
element.toolbar.copy-full-path-to-clipboard: Copier le chemin complet
Expand Down
Loading
Loading