chore(web): merge "Add to album" and "Add to shared album" actions into a single action (#24669)

* refactor: simplify album selection actions by removing shared option

* Removed the shared option from AddToAlbumAction and related components.
* Updated AlbumPickerModal and other components to reflect this change.
* Cleaned up related tests and documentation for consistency.

* fix lint
This commit is contained in:
Timon
2026-02-19 16:15:26 +01:00
committed by GitHub
parent 72a5ccaa53
commit 208c07af1f
18 changed files with 67 additions and 132 deletions
@@ -8,19 +8,18 @@
import { toTimelineAsset } from '$lib/utils/timeline-util';
import type { AssetResponseDto } from '@immich/sdk';
import { modalManager } from '@immich/ui';
import { mdiImageAlbum, mdiShareVariantOutline } from '@mdi/js';
import { mdiImageAlbum } from '@mdi/js';
import { t } from 'svelte-i18n';
interface Props {
asset: AssetResponseDto;
onAction: OnAction;
shared?: boolean;
}
let { asset, onAction, shared = false }: Props = $props();
let { asset, onAction }: Props = $props();
const onClick = async () => {
const albums = await modalManager.show(AlbumPickerModal, { shared });
const albums = await modalManager.show(AlbumPickerModal, {});
if (!albums || albums.length === 0) {
return;
@@ -40,10 +39,6 @@
};
</script>
<svelte:document use:shortcut={{ shortcut: { key: 'l', shift: shared }, onShortcut: onClick }} />
<svelte:document use:shortcut={{ shortcut: { key: 'l' }, onShortcut: onClick }} />
<MenuOption
icon={shared ? mdiShareVariantOutline : mdiImageAlbum}
text={shared ? $t('add_to_shared_album') : $t('add_to_album')}
{onClick}
/>
<MenuOption icon={mdiImageAlbum} text={$t('add_to_album')} {onClick} />
@@ -186,7 +186,6 @@
<RestoreAction {asset} {onAction} />
{:else}
<AddToAlbumAction {asset} {onAction} />
<AddToAlbumAction {asset} {onAction} shared />
{/if}
{/if}
@@ -48,7 +48,6 @@
mdiImageSearch,
mdiPause,
mdiPlay,
mdiPlus,
mdiSelectAll,
mdiVolumeHigh,
mdiVolumeOff,
@@ -339,10 +338,7 @@
onclick={handleSelectAll}
/>
<ButtonContextMenu icon={mdiPlus} title={$t('add_to')}>
<AddToAlbum />
<AddToAlbum shared />
</ButtonContextMenu>
<AddToAlbum />
<FavoriteAction removeFavorite={assetInteraction.isAllFavorite} />
@@ -28,15 +28,15 @@ const createAlbumRow = (album: AlbumResponseDto, selected: boolean) => ({
});
describe('Album Modal', () => {
it('non-shared with no albums configured yet shows message and new', () => {
const converter = new AlbumModalRowConverter(false, AlbumSortBy.MostRecentPhoto, SortOrder.Desc);
it('no albums configured yet shows message and new', () => {
const converter = new AlbumModalRowConverter(AlbumSortBy.MostRecentPhoto, SortOrder.Desc);
const modalRows = converter.toModalRows('', [], [], -1, []);
expect(modalRows).toStrictEqual([createNewAlbumRow(false), createMessageRow('no_albums_yet')]);
});
it('non-shared with no matching albums shows message and new', () => {
const converter = new AlbumModalRowConverter(false, AlbumSortBy.MostRecentPhoto, SortOrder.Desc);
it('no matching albums shows message and new', () => {
const converter = new AlbumModalRowConverter(AlbumSortBy.MostRecentPhoto, SortOrder.Desc);
const modalRows = converter.toModalRows(
'matches_nothing',
[],
@@ -48,8 +48,8 @@ describe('Album Modal', () => {
expect(modalRows).toStrictEqual([createNewAlbumRow(false), createMessageRow('no_albums_with_name_yet')]);
});
it('non-shared displays single albums', () => {
const converter = new AlbumModalRowConverter(false, AlbumSortBy.MostRecentPhoto, SortOrder.Desc);
it('displays single albums', () => {
const converter = new AlbumModalRowConverter(AlbumSortBy.MostRecentPhoto, SortOrder.Desc);
const holidayAlbum = albumFactory.build({ albumName: 'Holidays' });
const modalRows = converter.toModalRows('', [], [holidayAlbum], -1, []);
@@ -60,8 +60,8 @@ describe('Album Modal', () => {
]);
});
it('non-shared displays multiple albums and recents', () => {
const converter = new AlbumModalRowConverter(false, AlbumSortBy.MostRecentPhoto, SortOrder.Desc);
it('displays multiple albums and recents', () => {
const converter = new AlbumModalRowConverter(AlbumSortBy.MostRecentPhoto, SortOrder.Desc);
const holidayAlbum = albumFactory.build({ albumName: 'Holidays' });
const constructionAlbum = albumFactory.build({ albumName: 'Construction' });
const birthdayAlbum = albumFactory.build({ albumName: 'Birthday' });
@@ -87,31 +87,8 @@ describe('Album Modal', () => {
]);
});
it('shared only displays albums and no recents', () => {
const converter = new AlbumModalRowConverter(true, AlbumSortBy.MostRecentPhoto, SortOrder.Desc);
const holidayAlbum = albumFactory.build({ albumName: 'Holidays' });
const constructionAlbum = albumFactory.build({ albumName: 'Construction' });
const birthdayAlbum = albumFactory.build({ albumName: 'Birthday' });
const christmasAlbum = albumFactory.build({ albumName: 'Christmas' });
const modalRows = converter.toModalRows(
'',
[holidayAlbum, constructionAlbum],
[holidayAlbum, constructionAlbum, birthdayAlbum, christmasAlbum],
-1,
[],
);
expect(modalRows).toStrictEqual([
createNewAlbumRow(false),
createAlbumRow(holidayAlbum, false),
createAlbumRow(constructionAlbum, false),
createAlbumRow(birthdayAlbum, false),
createAlbumRow(christmasAlbum, false),
]);
});
it('search changes messaging and removes recent and non-matching albums', () => {
const converter = new AlbumModalRowConverter(false, AlbumSortBy.MostRecentPhoto, SortOrder.Desc);
const converter = new AlbumModalRowConverter(AlbumSortBy.MostRecentPhoto, SortOrder.Desc);
const holidayAlbum = albumFactory.build({ albumName: 'Holidays' });
const constructionAlbum = albumFactory.build({ albumName: 'Construction' });
const birthdayAlbum = albumFactory.build({ albumName: 'Birthday' });
@@ -132,7 +109,7 @@ describe('Album Modal', () => {
});
it('selection can select new album row', () => {
const converter = new AlbumModalRowConverter(false, AlbumSortBy.MostRecentPhoto, SortOrder.Desc);
const converter = new AlbumModalRowConverter(AlbumSortBy.MostRecentPhoto, SortOrder.Desc);
const holidayAlbum = albumFactory.build({ albumName: 'Holidays' });
const constructionAlbum = albumFactory.build({ albumName: 'Construction' });
const modalRows = converter.toModalRows('', [holidayAlbum], [holidayAlbum, constructionAlbum], 0, []);
@@ -148,7 +125,7 @@ describe('Album Modal', () => {
});
it('selection can select recent row', () => {
const converter = new AlbumModalRowConverter(false, AlbumSortBy.MostRecentPhoto, SortOrder.Desc);
const converter = new AlbumModalRowConverter(AlbumSortBy.MostRecentPhoto, SortOrder.Desc);
const holidayAlbum = albumFactory.build({ albumName: 'Holidays' });
const constructionAlbum = albumFactory.build({ albumName: 'Construction' });
const modalRows = converter.toModalRows('', [holidayAlbum], [holidayAlbum, constructionAlbum], 1, []);
@@ -164,7 +141,7 @@ describe('Album Modal', () => {
});
it('selection can select last row', () => {
const converter = new AlbumModalRowConverter(false, AlbumSortBy.MostRecentPhoto, SortOrder.Desc);
const converter = new AlbumModalRowConverter(AlbumSortBy.MostRecentPhoto, SortOrder.Desc);
const holidayAlbum = albumFactory.build({ albumName: 'Holidays' });
const constructionAlbum = albumFactory.build({ albumName: 'Construction' });
const modalRows = converter.toModalRows('', [holidayAlbum], [holidayAlbum, constructionAlbum], 3, []);
@@ -27,12 +27,10 @@ export const isSelectableRowType = (type: AlbumModalRowType) =>
const $t = get(t);
export class AlbumModalRowConverter {
private readonly shared: boolean;
private readonly sortBy: string;
private readonly orderBy: string;
constructor(shared: boolean, sortBy: string, orderBy: string) {
this.shared = shared;
constructor(sortBy: string, orderBy: string) {
this.sortBy = sortBy;
this.orderBy = orderBy;
}
@@ -44,8 +42,8 @@ export class AlbumModalRowConverter {
selectedRowIndex: number,
multiSelectedAlbumIds: string[],
): AlbumModalRow[] {
// only show recent albums if no search was entered, or we're in the normal albums (non-shared) modal.
const recentAlbumsToShow = !this.shared && search.length === 0 ? recentAlbums : [];
// only show recent albums if no search was entered
const recentAlbumsToShow = search.length === 0 ? recentAlbums : [];
const rows: AlbumModalRow[] = [{ type: AlbumModalRowType.NEW_ALBUM, selected: selectedRowIndex === 0 }];
const filteredAlbums = sortAlbums(
@@ -71,12 +69,10 @@ export class AlbumModalRowConverter {
}
}
if (!this.shared) {
rows.push({
type: AlbumModalRowType.SECTION,
text: (search.length === 0 ? $t('all_albums') : $t('albums')).toUpperCase(),
});
}
rows.push({
type: AlbumModalRowType.SECTION,
text: (search.length === 0 ? $t('all_albums') : $t('albums')).toUpperCase(),
});
const selectedOffsetDueToNewAndRecents = 1 + recentAlbumsToShow.length;
for (const [i, album] of filteredAlbums.entries()) {
@@ -4,21 +4,21 @@
import type { OnAddToAlbum } from '$lib/utils/actions';
import { addAssetsToAlbum, addAssetsToAlbums } from '$lib/utils/asset-utils';
import { getAssetControlContext } from '$lib/utils/context';
import { modalManager } from '@immich/ui';
import { mdiImageAlbum, mdiShareVariantOutline } from '@mdi/js';
import { IconButton, modalManager } from '@immich/ui';
import { mdiImageAlbum, mdiPlus } from '@mdi/js';
import { t } from 'svelte-i18n';
interface Props {
shared?: boolean;
onAddToAlbum?: OnAddToAlbum;
menuItem?: boolean;
}
let { shared = false, onAddToAlbum = () => {} }: Props = $props();
let { onAddToAlbum = () => {}, menuItem = false }: Props = $props();
const { getAssets } = getAssetControlContext();
const onClick = async () => {
const albums = await modalManager.show(AlbumPickerModal, { shared });
const albums = await modalManager.show(AlbumPickerModal, {});
if (!albums || albums.length === 0) {
return;
}
@@ -38,9 +38,17 @@
};
</script>
<MenuOption
{onClick}
text={shared ? $t('add_to_shared_album') : $t('add_to_album')}
icon={shared ? mdiShareVariantOutline : mdiImageAlbum}
shortcut={{ key: 'l', shift: shared }}
/>
{#if menuItem}
<MenuOption {onClick} text={$t('add_to_album')} icon={mdiImageAlbum} shortcut={{ key: 'l' }} />
{/if}
{#if !menuItem}
<IconButton
shape="round"
color="secondary"
variant="ghost"
icon={mdiPlus}
aria-label={$t('add_to_album')}
onclick={onClick}
/>
{/if}
@@ -42,7 +42,7 @@
const handlePicker = async () => {
if (isAlbum) {
const albums = await modalManager.show(AlbumPickerModal, { shared: false });
const albums = await modalManager.show(AlbumPickerModal);
if (albums && albums.length > 0) {
const newValue = multiple ? albums.map((album) => album.id) : albums[0].id;
onchange(newValue);