Compare commits

...

1 Commits

Author SHA1 Message Date
Yaros
caa6902a2c feat(web): undo delete single asset 2025-12-07 19:02:14 +01:00
5 changed files with 32 additions and 12 deletions

View File

@@ -13,7 +13,7 @@ describe('DeleteAction component', () => {
}); });
it('displays a button to move the asset to the trash bin', () => { 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(getByTitle('delete')).toBeInTheDocument();
expect(queryByTitle('deletePermanently')).toBeNull(); expect(queryByTitle('deletePermanently')).toBeNull();
}); });
@@ -25,7 +25,7 @@ describe('DeleteAction component', () => {
}); });
it('displays a button to permanently delete the asset', () => { 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(getByTitle('permanently_delete')).toBeInTheDocument();
expect(queryByTitle('delete')).toBeNull(); expect(queryByTitle('delete')).toBeNull();
}); });

View File

@@ -5,6 +5,7 @@
import Portal from '$lib/elements/Portal.svelte'; import Portal from '$lib/elements/Portal.svelte';
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte'; import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
import { showDeleteModal } from '$lib/stores/preferences.store'; 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 { handleError } from '$lib/utils/handle-error';
import { toTimelineAsset } from '$lib/utils/timeline-util'; import { toTimelineAsset } from '$lib/utils/timeline-util';
import { deleteAssets, type AssetResponseDto } from '@immich/sdk'; import { deleteAssets, type AssetResponseDto } from '@immich/sdk';
@@ -17,9 +18,10 @@
asset: AssetResponseDto; asset: AssetResponseDto;
onAction: OnAction; onAction: OnAction;
preAction: PreAction; preAction: PreAction;
onUndoDelete?: OnUndoDelete;
} }
let { asset, onAction, preAction }: Props = $props(); let { asset, onAction, preAction, onUndoDelete = undefined }: Props = $props();
let showConfirmModal = $state(false); let showConfirmModal = $state(false);
@@ -38,14 +40,14 @@
}; };
const trashAsset = async () => { const trashAsset = async () => {
try { const timelineAsset = toTimelineAsset(asset);
preAction({ type: AssetAction.TRASH, asset: toTimelineAsset(asset) }); preAction({ type: AssetAction.TRASH, asset: timelineAsset });
await deleteAssets({ assetBulkDeleteDto: { ids: [asset.id] } }); await deleteAssetsUtil(
onAction({ type: AssetAction.TRASH, asset: toTimelineAsset(asset) }); false,
toastManager.success($t('moved_to_trash')); () => onAction({ type: AssetAction.TRASH, asset: timelineAsset }),
} catch (error) { [timelineAsset],
handleError(error, $t('errors.unable_to_trash_asset')); onUndoDelete,
} );
}; };
const deleteAsset = async () => { const deleteAsset = async () => {

View File

@@ -30,6 +30,7 @@
import { user } from '$lib/stores/user.store'; import { user } from '$lib/stores/user.store';
import { photoZoomState } from '$lib/stores/zoom-image.store'; import { photoZoomState } from '$lib/stores/zoom-image.store';
import { getAssetJobName, getSharedLink } from '$lib/utils'; import { getAssetJobName, getSharedLink } from '$lib/utils';
import type { OnUndoDelete } from '$lib/utils/actions';
import { canCopyImageToClipboard } from '$lib/utils/asset-utils'; import { canCopyImageToClipboard } from '$lib/utils/asset-utils';
import { toTimelineAsset } from '$lib/utils/timeline-util'; import { toTimelineAsset } from '$lib/utils/timeline-util';
import { import {
@@ -73,6 +74,7 @@
onCopyImage?: () => Promise<void>; onCopyImage?: () => Promise<void>;
preAction: PreAction; preAction: PreAction;
onAction: OnAction; onAction: OnAction;
onUndoDelete?: OnUndoDelete;
onRunJob: (name: AssetJobName) => void; onRunJob: (name: AssetJobName) => void;
onPlaySlideshow: () => void; onPlaySlideshow: () => void;
onShowDetail: () => void; onShowDetail: () => void;
@@ -95,6 +97,7 @@
onCopyImage, onCopyImage,
preAction, preAction,
onAction, onAction,
onUndoDelete = undefined,
onRunJob, onRunJob,
onPlaySlideshow, onPlaySlideshow,
onShowDetail, onShowDetail,
@@ -182,7 +185,7 @@
{/if} {/if}
{#if isOwner} {#if isOwner}
<DeleteAction {asset} {onAction} {preAction} /> <DeleteAction {asset} {onAction} {preAction} {onUndoDelete} />
<ButtonContextMenu direction="left" align="top-right" color="secondary" title={$t('more')} icon={mdiDotsVertical}> <ButtonContextMenu direction="left" align="top-right" color="secondary" title={$t('more')} icon={mdiDotsVertical}>
{#if showSlideshow && !isLocked} {#if showSlideshow && !isLocked}

View File

@@ -19,6 +19,7 @@
import { user } from '$lib/stores/user.store'; import { user } from '$lib/stores/user.store';
import { websocketEvents } from '$lib/stores/websocket'; import { websocketEvents } from '$lib/stores/websocket';
import { getAssetJobMessage, getSharedLink, handlePromiseError } from '$lib/utils'; import { getAssetJobMessage, getSharedLink, handlePromiseError } from '$lib/utils';
import type { OnUndoDelete } from '$lib/utils/actions';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { SlideshowHistory } from '$lib/utils/slideshow-history'; import { SlideshowHistory } from '$lib/utils/slideshow-history';
import { toTimelineAsset } from '$lib/utils/timeline-util'; import { toTimelineAsset } from '$lib/utils/timeline-util';
@@ -62,6 +63,7 @@
person?: PersonResponseDto | null; person?: PersonResponseDto | null;
preAction?: PreAction | undefined; preAction?: PreAction | undefined;
onAction?: OnAction | undefined; onAction?: OnAction | undefined;
onUndoDelete?: OnUndoDelete | undefined;
showCloseButton?: boolean; showCloseButton?: boolean;
onClose: (asset: AssetResponseDto) => void; onClose: (asset: AssetResponseDto) => void;
onNext: () => Promise<HasAsset>; onNext: () => Promise<HasAsset>;
@@ -80,6 +82,7 @@
person = null, person = null,
preAction = undefined, preAction = undefined,
onAction = undefined, onAction = undefined,
onUndoDelete = undefined,
showCloseButton, showCloseButton,
onClose, onClose,
onNext, onNext,
@@ -430,6 +433,7 @@
onCopyImage={copyImage} onCopyImage={copyImage}
preAction={handlePreAction} preAction={handlePreAction}
onAction={handleAction} onAction={handleAction}
{onUndoDelete}
onRunJob={handleRunJob} onRunJob={handleRunJob}
onPlaySlideshow={() => ($slideshowState = SlideshowState.PlaySlideshow)} onPlaySlideshow={() => ($slideshowState = SlideshowState.PlaySlideshow)}
onShowDetail={toggleDetailPanel} onShowDetail={toggleDetailPanel}

View File

@@ -3,6 +3,7 @@
import { AssetAction } from '$lib/constants'; import { AssetAction } from '$lib/constants';
import { authManager } from '$lib/managers/auth-manager.svelte'; import { authManager } from '$lib/managers/auth-manager.svelte';
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-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 { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { updateStackedAssetInTimeline, updateUnstackedAssetInTimeline } from '$lib/utils/actions'; import { updateStackedAssetInTimeline, updateUnstackedAssetInTimeline } from '$lib/utils/actions';
import { navigate } from '$lib/utils/navigation'; 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 });
}
};
</script> </script>
{#await import('$lib/components/asset-viewer/asset-viewer.svelte') then { default: AssetViewer }} {#await import('$lib/components/asset-viewer/asset-viewer.svelte') then { default: AssetViewer }}
@@ -175,6 +185,7 @@
{person} {person}
preAction={handlePreAction} preAction={handlePreAction}
onAction={handleAction} onAction={handleAction}
onUndoDelete={handleUndoDelete}
onPrevious={handlePrevious} onPrevious={handlePrevious}
onNext={handleNext} onNext={handleNext}
onRandom={handleRandom} onRandom={handleRandom}