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', () => {
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();
});

View File

@@ -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 () => {

View File

@@ -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<void>;
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}
<DeleteAction {asset} {onAction} {preAction} />
<DeleteAction {asset} {onAction} {preAction} {onUndoDelete} />
<ButtonContextMenu direction="left" align="top-right" color="secondary" title={$t('more')} icon={mdiDotsVertical}>
{#if showSlideshow && !isLocked}

View File

@@ -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<HasAsset>;
@@ -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}

View File

@@ -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 });
}
};
</script>
{#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}