mirror of
https://github.com/immich-app/immich.git
synced 2025-12-22 17:24:56 +03:00
feat(web): undo delete (#18729)
* feat(web): Undo asset delete * - lints and checks - Update English translation * Update delete-assets.svelte Make onUndoDelete optional in Props interface * - Ensure undo button not available on permanent delete, or trash disabled. - Enforce lint requirement for no-negated-condition * Fix formatting * fix: lint --------- Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { featureFlags } from '$lib/stores/server-config.store';
|
||||
import { type OnDelete, deleteAssets } from '$lib/utils/actions';
|
||||
import { type OnDelete, type OnUndoDelete, deleteAssets } from '$lib/utils/actions';
|
||||
import { mdiDeleteForeverOutline, mdiDeleteOutline, mdiTimerSand } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
|
||||
@@ -10,11 +10,12 @@
|
||||
|
||||
interface Props {
|
||||
onAssetDelete: OnDelete;
|
||||
onUndoDelete?: OnUndoDelete | undefined;
|
||||
menuItem?: boolean;
|
||||
force?: boolean;
|
||||
}
|
||||
|
||||
let { onAssetDelete, menuItem = false, force = !$featureFlags.trash }: Props = $props();
|
||||
let { onAssetDelete, onUndoDelete = undefined, menuItem = false, force = !$featureFlags.trash }: Props = $props();
|
||||
|
||||
const { clearSelect, getOwnedAssets } = getAssetControlContext();
|
||||
|
||||
@@ -34,8 +35,8 @@
|
||||
|
||||
const handleDelete = async () => {
|
||||
loading = true;
|
||||
const ids = [...getOwnedAssets()].map((a) => a.id);
|
||||
await deleteAssets(force, onAssetDelete, ids);
|
||||
const assets = [...getOwnedAssets()];
|
||||
await deleteAssets(force, onAssetDelete, assets, onUndoDelete);
|
||||
clearSelect();
|
||||
isShowConfirmation = false;
|
||||
loading = false;
|
||||
|
||||
@@ -382,7 +382,12 @@
|
||||
|
||||
const trashOrDelete = async (force: boolean = false) => {
|
||||
isShowDeleteConfirmation = false;
|
||||
await deleteAssets(!(isTrashEnabled && !force), (assetIds) => assetStore.removeAssets(assetIds), idsSelectedAssets);
|
||||
await deleteAssets(
|
||||
!(isTrashEnabled && !force),
|
||||
(assetIds) => assetStore.removeAssets(assetIds),
|
||||
assetInteraction.selectedAssets,
|
||||
!isTrashEnabled || force ? undefined : (assets) => assetStore.addAssets(assets),
|
||||
);
|
||||
assetInteraction.clearMultiselect();
|
||||
};
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
onPrevious?: (() => Promise<{ id: string } | undefined>) | undefined;
|
||||
onNext?: (() => Promise<{ id: string } | undefined>) | undefined;
|
||||
onRandom?: (() => Promise<{ id: string } | undefined>) | undefined;
|
||||
onReload?: (() => void) | undefined;
|
||||
pageHeaderOffset?: number;
|
||||
slidingWindowOffset?: number;
|
||||
}
|
||||
@@ -54,6 +55,7 @@
|
||||
onPrevious = undefined,
|
||||
onNext = undefined,
|
||||
onRandom = undefined,
|
||||
onReload = undefined,
|
||||
slidingWindowOffset = 0,
|
||||
pageHeaderOffset = 0,
|
||||
}: Props = $props();
|
||||
@@ -255,7 +257,8 @@
|
||||
await deleteAssets(
|
||||
!(isTrashEnabled && !force),
|
||||
(assetIds) => (assets = assets.filter((asset) => !assetIds.includes(asset.id))),
|
||||
idsSelectedAssets,
|
||||
assetInteraction.selectedAssets,
|
||||
onReload,
|
||||
);
|
||||
assetInteraction.clearMultiselect();
|
||||
};
|
||||
@@ -426,7 +429,6 @@
|
||||
};
|
||||
|
||||
let isTrashEnabled = $derived($featureFlags.loaded && $featureFlags.trash);
|
||||
let idsSelectedAssets = $derived(assetInteraction.selectedAssets.map((selectedAsset) => selectedAsset.id));
|
||||
|
||||
$effect(() => {
|
||||
if (!lastAssetMouseEvent) {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { notificationController, NotificationType } from '$lib/components/shared-components/notification/notification';
|
||||
import type { AssetStore, TimelineAsset } from '$lib/stores/assets-store.svelte';
|
||||
import type { StackResponse } from '$lib/utils/asset-utils';
|
||||
import { AssetVisibility, deleteAssets as deleteBulk } from '@immich/sdk';
|
||||
import { AssetVisibility, deleteAssets as deleteBulk, restoreAssets } from '@immich/sdk';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { get } from 'svelte/store';
|
||||
import { handleError } from './handle-error';
|
||||
|
||||
export type OnDelete = (assetIds: string[]) => void;
|
||||
export type OnUndoDelete = (assets: TimelineAsset[]) => void;
|
||||
export type OnRestore = (ids: string[]) => void;
|
||||
export type OnLink = (assets: { still: TimelineAsset; motion: TimelineAsset }) => void;
|
||||
export type OnUnlink = (assets: { still: TimelineAsset; motion: TimelineAsset }) => void;
|
||||
@@ -17,9 +18,15 @@ export type OnStack = (result: StackResponse) => void;
|
||||
export type OnUnstack = (assets: TimelineAsset[]) => void;
|
||||
export type OnSetVisibility = (ids: string[]) => void;
|
||||
|
||||
export const deleteAssets = async (force: boolean, onAssetDelete: OnDelete, ids: string[]) => {
|
||||
export const deleteAssets = async (
|
||||
force: boolean,
|
||||
onAssetDelete: OnDelete,
|
||||
assets: TimelineAsset[],
|
||||
onUndoDelete: OnUndoDelete | undefined = undefined,
|
||||
) => {
|
||||
const $t = get(t);
|
||||
try {
|
||||
const ids = assets.map((a) => a.id);
|
||||
await deleteBulk({ assetBulkDeleteDto: { ids, force } });
|
||||
onAssetDelete(ids);
|
||||
|
||||
@@ -28,12 +35,28 @@ export const deleteAssets = async (force: boolean, onAssetDelete: OnDelete, ids:
|
||||
? $t('assets_permanently_deleted_count', { values: { count: ids.length } })
|
||||
: $t('assets_trashed_count', { values: { count: ids.length } }),
|
||||
type: NotificationType.Info,
|
||||
...(onUndoDelete &&
|
||||
!force && {
|
||||
button: { text: $t('undo'), onClick: () => undoDeleteAssets(onUndoDelete, assets) },
|
||||
timeout: 5000,
|
||||
}),
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_delete_assets'));
|
||||
}
|
||||
};
|
||||
|
||||
const undoDeleteAssets = async (onUndoDelete: OnUndoDelete, assets: TimelineAsset[]) => {
|
||||
const $t = get(t);
|
||||
try {
|
||||
const ids = assets.map((a) => a.id);
|
||||
await restoreAssets({ bulkIdsDto: { ids } });
|
||||
onUndoDelete?.(assets);
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_restore_assets'));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the asset stack state in the asset store based on the provided stack response.
|
||||
* This function updates the stack information so that the icon is shown for the primary asset
|
||||
|
||||
Reference in New Issue
Block a user