From caa6902a2cb174bd153554811fe9c606b39d9d45 Mon Sep 17 00:00:00 2001 From: Yaros Date: Sun, 7 Dec 2025 19:02:14 +0100 Subject: [PATCH] feat(web): undo delete single asset --- .../actions/delete-action.spec.ts | 4 ++-- .../asset-viewer/actions/delete-action.svelte | 20 ++++++++++--------- .../asset-viewer/asset-viewer-nav-bar.svelte | 5 ++++- .../asset-viewer/asset-viewer.svelte | 4 ++++ .../timeline/TimelineAssetViewer.svelte | 11 ++++++++++ 5 files changed, 32 insertions(+), 12 deletions(-) diff --git a/web/src/lib/components/asset-viewer/actions/delete-action.spec.ts b/web/src/lib/components/asset-viewer/actions/delete-action.spec.ts index e0b33ff48b..98d99d8d77 100644 --- a/web/src/lib/components/asset-viewer/actions/delete-action.spec.ts +++ b/web/src/lib/components/asset-viewer/actions/delete-action.spec.ts @@ -13,7 +13,7 @@ describe('DeleteAction component', () => { }); it('displays a button to move the asset to the trash bin', () => { - const { getByTitle, queryByTitle } = render(DeleteAction, { asset, onAction: vi.fn() }); + const { getByTitle, queryByTitle } = render(DeleteAction, { asset, onAction: vi.fn(), preAction: vi.fn() }); expect(getByTitle('delete')).toBeInTheDocument(); expect(queryByTitle('deletePermanently')).toBeNull(); }); @@ -25,7 +25,7 @@ describe('DeleteAction component', () => { }); it('displays a button to permanently delete the asset', () => { - const { getByTitle, queryByTitle } = render(DeleteAction, { asset, onAction: vi.fn() }); + const { getByTitle, queryByTitle } = render(DeleteAction, { asset, onAction: vi.fn(), preAction: vi.fn() }); expect(getByTitle('permanently_delete')).toBeInTheDocument(); expect(queryByTitle('delete')).toBeNull(); }); diff --git a/web/src/lib/components/asset-viewer/actions/delete-action.svelte b/web/src/lib/components/asset-viewer/actions/delete-action.svelte index 74d40c7cee..be9c9ccf9c 100644 --- a/web/src/lib/components/asset-viewer/actions/delete-action.svelte +++ b/web/src/lib/components/asset-viewer/actions/delete-action.svelte @@ -5,6 +5,7 @@ import Portal from '$lib/elements/Portal.svelte'; import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte'; import { showDeleteModal } from '$lib/stores/preferences.store'; + import { deleteAssets as deleteAssetsUtil, type OnUndoDelete } from '$lib/utils/actions'; import { handleError } from '$lib/utils/handle-error'; import { toTimelineAsset } from '$lib/utils/timeline-util'; import { deleteAssets, type AssetResponseDto } from '@immich/sdk'; @@ -17,9 +18,10 @@ asset: AssetResponseDto; onAction: OnAction; preAction: PreAction; + onUndoDelete?: OnUndoDelete; } - let { asset, onAction, preAction }: Props = $props(); + let { asset, onAction, preAction, onUndoDelete = undefined }: Props = $props(); let showConfirmModal = $state(false); @@ -38,14 +40,14 @@ }; const trashAsset = async () => { - try { - preAction({ type: AssetAction.TRASH, asset: toTimelineAsset(asset) }); - await deleteAssets({ assetBulkDeleteDto: { ids: [asset.id] } }); - onAction({ type: AssetAction.TRASH, asset: toTimelineAsset(asset) }); - toastManager.success($t('moved_to_trash')); - } catch (error) { - handleError(error, $t('errors.unable_to_trash_asset')); - } + const timelineAsset = toTimelineAsset(asset); + preAction({ type: AssetAction.TRASH, asset: timelineAsset }); + await deleteAssetsUtil( + false, + () => onAction({ type: AssetAction.TRASH, asset: timelineAsset }), + [timelineAsset], + onUndoDelete, + ); }; const deleteAsset = async () => { diff --git a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte index 0dad2793bf..78138ae4ae 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte @@ -30,6 +30,7 @@ import { user } from '$lib/stores/user.store'; import { photoZoomState } from '$lib/stores/zoom-image.store'; import { getAssetJobName, getSharedLink } from '$lib/utils'; + import type { OnUndoDelete } from '$lib/utils/actions'; import { canCopyImageToClipboard } from '$lib/utils/asset-utils'; import { toTimelineAsset } from '$lib/utils/timeline-util'; import { @@ -73,6 +74,7 @@ onCopyImage?: () => Promise; preAction: PreAction; onAction: OnAction; + onUndoDelete?: OnUndoDelete; onRunJob: (name: AssetJobName) => void; onPlaySlideshow: () => void; onShowDetail: () => void; @@ -95,6 +97,7 @@ onCopyImage, preAction, onAction, + onUndoDelete = undefined, onRunJob, onPlaySlideshow, onShowDetail, @@ -182,7 +185,7 @@ {/if} {#if isOwner} - + {#if showSlideshow && !isLocked} diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index b657f34ece..e316fa8db5 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -19,6 +19,7 @@ import { user } from '$lib/stores/user.store'; import { websocketEvents } from '$lib/stores/websocket'; import { getAssetJobMessage, getSharedLink, handlePromiseError } from '$lib/utils'; + import type { OnUndoDelete } from '$lib/utils/actions'; import { handleError } from '$lib/utils/handle-error'; import { SlideshowHistory } from '$lib/utils/slideshow-history'; import { toTimelineAsset } from '$lib/utils/timeline-util'; @@ -62,6 +63,7 @@ person?: PersonResponseDto | null; preAction?: PreAction | undefined; onAction?: OnAction | undefined; + onUndoDelete?: OnUndoDelete | undefined; showCloseButton?: boolean; onClose: (asset: AssetResponseDto) => void; onNext: () => Promise; @@ -80,6 +82,7 @@ person = null, preAction = undefined, onAction = undefined, + onUndoDelete = undefined, showCloseButton, onClose, onNext, @@ -430,6 +433,7 @@ onCopyImage={copyImage} preAction={handlePreAction} onAction={handleAction} + {onUndoDelete} onRunJob={handleRunJob} onPlaySlideshow={() => ($slideshowState = SlideshowState.PlaySlideshow)} onShowDetail={toggleDetailPanel} diff --git a/web/src/lib/components/timeline/TimelineAssetViewer.svelte b/web/src/lib/components/timeline/TimelineAssetViewer.svelte index a121bd1938..9f8b5fe36b 100644 --- a/web/src/lib/components/timeline/TimelineAssetViewer.svelte +++ b/web/src/lib/components/timeline/TimelineAssetViewer.svelte @@ -3,6 +3,7 @@ import { AssetAction } from '$lib/constants'; import { authManager } from '$lib/managers/auth-manager.svelte'; import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte'; + import type { TimelineAsset } from '$lib/managers/timeline-manager/types'; import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { updateStackedAssetInTimeline, updateUnstackedAssetInTimeline } from '$lib/utils/actions'; import { navigate } from '$lib/utils/navigation'; @@ -163,6 +164,15 @@ } } }; + const handleUndoDelete = async (assets: TimelineAsset[]) => { + timelineManager.upsertAssets(assets); + if (assets.length > 0) { + const restoredAsset = assets[0]; + const asset = await getAssetInfo({ ...authManager.params, id: restoredAsset.id }); + assetViewingStore.setAsset(asset); + await navigate({ targetRoute: 'current', assetId: restoredAsset.id }); + } + }; {#await import('$lib/components/asset-viewer/asset-viewer.svelte') then { default: AssetViewer }} @@ -175,6 +185,7 @@ {person} preAction={handlePreAction} onAction={handleAction} + onUndoDelete={handleUndoDelete} onPrevious={handlePrevious} onNext={handleNext} onRandom={handleRandom}