refactor: album edit modal (#24786)

This commit is contained in:
Jason Rasmussen
2025-12-22 13:33:49 -05:00
committed by GitHub
parent f6f9a3abb4
commit dd744f8ee3
4 changed files with 74 additions and 87 deletions

View File

@@ -1,13 +1,9 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation';
import { resolve } from '$app/paths';
import AlbumCardGroup from '$lib/components/album-page/album-card-group.svelte'; import AlbumCardGroup from '$lib/components/album-page/album-card-group.svelte';
import AlbumsTable from '$lib/components/album-page/albums-table.svelte'; import AlbumsTable from '$lib/components/album-page/albums-table.svelte';
import OnEvents from '$lib/components/OnEvents.svelte'; import OnEvents from '$lib/components/OnEvents.svelte';
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte'; import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
import RightClickContextMenu from '$lib/components/shared-components/context-menu/right-click-context-menu.svelte'; import RightClickContextMenu from '$lib/components/shared-components/context-menu/right-click-context-menu.svelte';
import ToastAction from '$lib/components/ToastAction.svelte';
import { AppRoute } from '$lib/constants';
import AlbumEditModal from '$lib/modals/AlbumEditModal.svelte'; import AlbumEditModal from '$lib/modals/AlbumEditModal.svelte';
import AlbumShareModal from '$lib/modals/AlbumShareModal.svelte'; import AlbumShareModal from '$lib/modals/AlbumShareModal.svelte';
import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte'; import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte';
@@ -28,7 +24,7 @@
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { normalizeSearchString } from '$lib/utils/string-utils'; import { normalizeSearchString } from '$lib/utils/string-utils';
import { addUsersToAlbum, type AlbumResponseDto, type AlbumUserAddDto } from '@immich/sdk'; import { addUsersToAlbum, type AlbumResponseDto, type AlbumUserAddDto } from '@immich/sdk';
import { modalManager, toastManager } from '@immich/ui'; import { modalManager } from '@immich/ui';
import { mdiDeleteOutline, mdiDownload, mdiRenameOutline, mdiShareVariantOutline } from '@mdi/js'; import { mdiDeleteOutline, mdiDownload, mdiRenameOutline, mdiShareVariantOutline } from '@mdi/js';
import { groupBy } from 'lodash-es'; import { groupBy } from 'lodash-es';
import { onMount, type Snippet } from 'svelte'; import { onMount, type Snippet } from 'svelte';
@@ -199,10 +195,7 @@
switch (action) { switch (action) {
case 'edit': { case 'edit': {
const editedAlbum = await modalManager.show(AlbumEditModal, { album: selectedAlbum }); await modalManager.show(AlbumEditModal, { album: selectedAlbum });
if (editedAlbum) {
successEditAlbumInfo(editedAlbum);
}
break; break;
} }
@@ -219,7 +212,7 @@
if (success) { if (success) {
selectedAlbum.shared = true; selectedAlbum.shared = true;
selectedAlbum.hasSharedLink = true; selectedAlbum.hasSharedLink = true;
updateAlbumInfo(selectedAlbum); onUpdate(selectedAlbum);
} }
break; break;
} }
@@ -244,39 +237,18 @@
await Promise.allSettled(albumsToRemove.map((album) => handleDeleteAlbum(album, { prompt: false, notify: false }))); await Promise.allSettled(albumsToRemove.map((album) => handleDeleteAlbum(album, { prompt: false, notify: false })));
}; };
const updateAlbumInfo = (album: AlbumResponseDto) => { const findAndUpdate = (albums: AlbumResponseDto[], album: AlbumResponseDto) => {
ownedAlbums[ownedAlbums.findIndex(({ id }) => id === album.id)] = album; const target = albums.find(({ id }) => id === album.id);
sharedAlbums[sharedAlbums.findIndex(({ id }) => id === album.id)] = album; if (target) {
}; Object.assign(target, album);
const updateRecentAlbumInfo = (album: AlbumResponseDto) => {
for (const cachedAlbum of userInteraction.recentAlbums || []) {
if (cachedAlbum.id === album.id) {
Object.assign(cachedAlbum, { ...cachedAlbum, ...album });
break;
}
} }
return albums;
}; };
const successEditAlbumInfo = (album: AlbumResponseDto) => { const onUpdate = (album: AlbumResponseDto) => {
toastManager.custom({ ownedAlbums = findAndUpdate(ownedAlbums, album);
component: ToastAction, sharedAlbums = findAndUpdate(sharedAlbums, album);
props: {
color: 'primary',
title: $t('success'),
description: $t('album_info_updated'),
button: {
text: $t('view_album'),
color: 'primary',
onClick() {
return goto(resolve(`${AppRoute.ALBUMS}/${album.id}`));
},
},
},
});
updateAlbumInfo(album);
updateRecentAlbumInfo(album);
}; };
const handleAddUsers = async (album: AlbumResponseDto, albumUsers: AlbumUserAddDto[]) => { const handleAddUsers = async (album: AlbumResponseDto, albumUsers: AlbumUserAddDto[]) => {
@@ -287,19 +259,24 @@
albumUsers, albumUsers,
}, },
}); });
updateAlbumInfo(updatedAlbum); onUpdate(updatedAlbum);
} catch (error) { } catch (error) {
handleError(error, $t('errors.unable_to_add_album_users')); handleError(error, $t('errors.unable_to_add_album_users'));
} }
}; };
const onAlbumUpdate = (album: AlbumResponseDto) => {
onUpdate(album);
userInteraction.recentAlbums = findAndUpdate(userInteraction.recentAlbums || [], album);
};
const onAlbumDelete = (album: AlbumResponseDto) => { const onAlbumDelete = (album: AlbumResponseDto) => {
ownedAlbums = ownedAlbums.filter(({ id }) => id !== album.id); ownedAlbums = ownedAlbums.filter(({ id }) => id !== album.id);
sharedAlbums = sharedAlbums.filter(({ id }) => id !== album.id); sharedAlbums = sharedAlbums.filter(({ id }) => id !== album.id);
}; };
</script> </script>
<OnEvents {onAlbumDelete} /> <OnEvents {onAlbumUpdate} {onAlbumDelete} />
{#if albums.length > 0} {#if albums.length > 0}
{#if userSettings.view === AlbumViewMode.Cover} {#if userSettings.view === AlbumViewMode.Cover}

View File

@@ -26,6 +26,7 @@ export type Events = {
AssetReplace: [{ oldAssetId: string; newAssetId: string }]; AssetReplace: [{ oldAssetId: string; newAssetId: string }];
AlbumUpdate: [AlbumResponseDto];
AlbumDelete: [AlbumResponseDto]; AlbumDelete: [AlbumResponseDto];
QueueUpdate: [QueueResponseDto]; QueueUpdate: [QueueResponseDto];

View File

@@ -1,63 +1,41 @@
<script lang="ts"> <script lang="ts">
import AlbumCover from '$lib/components/album-page/album-cover.svelte'; import AlbumCover from '$lib/components/album-page/album-cover.svelte';
import { handleError } from '$lib/utils/handle-error'; import { handleUpdateAlbum } from '$lib/services/album.service';
import { updateAlbumInfo, type AlbumResponseDto } from '@immich/sdk'; import { type AlbumResponseDto } from '@immich/sdk';
import { Button, Field, HStack, Input, Modal, ModalBody, ModalFooter, Textarea } from '@immich/ui'; import { Field, FormModal, Input, Textarea } from '@immich/ui';
import { mdiRenameOutline } from '@mdi/js'; import { mdiRenameOutline } from '@mdi/js';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
type Props = { type Props = {
album: AlbumResponseDto; album: AlbumResponseDto;
onClose: (album?: AlbumResponseDto) => void; onClose: () => void;
}; };
let { album = $bindable(), onClose }: Props = $props(); let { album, onClose }: Props = $props();
let albumName = $state(album.albumName); let albumName = $state(album.albumName);
let description = $state(album.description); let description = $state(album.description);
let isSubmitting = $state(false);
const handleSubmit = async (event: Event) => { const onSubmit = async () => {
event.preventDefault(); const success = await handleUpdateAlbum(album, { albumName, description });
if (success) {
isSubmitting = true; onClose();
try {
await updateAlbumInfo({ id: album.id, updateAlbumDto: { albumName, description } });
album.albumName = albumName;
album.description = description;
onClose(album);
} catch (error) {
handleError(error, $t('errors.unable_to_update_album_info'));
} finally {
isSubmitting = false;
} }
}; };
</script> </script>
<Modal icon={mdiRenameOutline} title={$t('edit_album')} size="medium" {onClose}> <FormModal icon={mdiRenameOutline} title={$t('edit_album')} size="medium" {onClose} {onSubmit} submitText={$t('save')}>
<ModalBody> <div class="flex items-center gap-8 m-4">
<form onsubmit={handleSubmit} autocomplete="off" id="edit-album-form"> <AlbumCover {album} class="h-50 w-50 shadow-lg hidden sm:flex" />
<div class="flex items-center gap-8 m-4">
<AlbumCover {album} class="h-50 w-50 shadow-lg hidden sm:flex" />
<div class="grow flex flex-col gap-4"> <div class="grow flex flex-col gap-4">
<Field label={$t('name')}> <Field label={$t('name')}>
<Input bind:value={albumName} /> <Input bind:value={albumName} />
</Field> </Field>
<Field label={$t('description')}> <Field label={$t('description')}>
<Textarea bind:value={description} /> <Textarea bind:value={description} />
</Field> </Field>
</div> </div>
</div> </div>
</form> </FormModal>
</ModalBody>
<ModalFooter>
<HStack fullWidth>
<Button shape="round" color="secondary" fullWidth onclick={() => onClose()}>{$t('cancel')}</Button>
<Button shape="round" type="submit" fullWidth disabled={isSubmitting} form="edit-album-form">{$t('save')}</Button>
</HStack>
</ModalFooter>
</Modal>

View File

@@ -1,10 +1,41 @@
import { goto } from '$app/navigation';
import ToastAction from '$lib/components/ToastAction.svelte';
import { AppRoute } from '$lib/constants';
import { eventManager } from '$lib/managers/event-manager.svelte'; import { eventManager } from '$lib/managers/event-manager.svelte';
import { downloadArchive } from '$lib/utils/asset-utils'; import { downloadArchive } from '$lib/utils/asset-utils';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { getFormatter } from '$lib/utils/i18n'; import { getFormatter } from '$lib/utils/i18n';
import { deleteAlbum, type AlbumResponseDto } from '@immich/sdk'; import { deleteAlbum, updateAlbumInfo, type AlbumResponseDto, type UpdateAlbumDto } from '@immich/sdk';
import { modalManager, toastManager } from '@immich/ui'; import { modalManager, toastManager } from '@immich/ui';
export const handleUpdateAlbum = async ({ id }: { id: string }, dto: UpdateAlbumDto) => {
const $t = await getFormatter();
try {
const response = await updateAlbumInfo({ id, updateAlbumDto: dto });
eventManager.emit('AlbumUpdate', response);
toastManager.custom({
component: ToastAction,
props: {
color: 'primary',
title: $t('success'),
description: $t('album_info_updated'),
button: {
text: $t('view_album'),
color: 'primary',
onClick() {
return goto(`${AppRoute.ALBUMS}/${id}`);
},
},
},
});
return true;
} catch (error) {
handleError(error, $t('errors.unable_to_update_album_info'));
}
};
export const handleDeleteAlbum = async (album: AlbumResponseDto, options?: { prompt?: boolean; notify?: boolean }) => { export const handleDeleteAlbum = async (album: AlbumResponseDto, options?: { prompt?: boolean; notify?: boolean }) => {
const $t = await getFormatter(); const $t = await getFormatter();
const { prompt = true, notify = true } = options ?? {}; const { prompt = true, notify = true } = options ?? {};