chore(web): migration svelte 5 syntax (#13883)

This commit is contained in:
Alex 2024-11-14 08:43:25 -06:00 committed by GitHub
parent 9203a61709
commit 0b3742cf13
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
310 changed files with 6435 additions and 4176 deletions

View file

@ -1,14 +1,15 @@
import { sdkMock } from '$lib/__mocks__/sdk.mock';
import { albumFactory } from '@test-data/factories/album-factory';
import '@testing-library/jest-dom';
import { fireEvent, render, waitFor, type RenderResult } from '@testing-library/svelte';
import { render, waitFor, type RenderResult } from '@testing-library/svelte';
import userEvent from '@testing-library/user-event';
import { init, register, waitLocale } from 'svelte-i18n';
import AlbumCard from '../album-card.svelte';
const onShowContextMenu = vi.fn();
describe('AlbumCard component', () => {
let sut: RenderResult<AlbumCard>;
let sut: RenderResult<typeof AlbumCard>;
beforeAll(async () => {
await init({ fallbackLocale: 'en-US' });
@ -110,13 +111,9 @@ describe('AlbumCard component', () => {
toJSON: () => ({}),
});
await fireEvent(
contextMenuButton,
new MouseEvent('click', {
clientX: 123,
clientY: 456,
}),
);
const user = userEvent.setup();
await user.click(contextMenuButton);
expect(onShowContextMenu).toHaveBeenCalledTimes(1);
expect(onShowContextMenu).toHaveBeenCalledWith(expect.objectContaining({ x: 123, y: 456 }));
});

View file

@ -11,28 +11,43 @@
import Icon from '$lib/components/elements/icon.svelte';
import { t } from 'svelte-i18n';
export let albums: AlbumResponseDto[];
export let group: AlbumGroup | undefined = undefined;
export let showOwner = false;
export let showDateRange = false;
export let showItemCount = false;
export let onShowContextMenu: ((position: ContextMenuPosition, album: AlbumResponseDto) => unknown) | undefined =
undefined;
interface Props {
albums: AlbumResponseDto[];
group?: AlbumGroup | undefined;
showOwner?: boolean;
showDateRange?: boolean;
showItemCount?: boolean;
onShowContextMenu?: ((position: ContextMenuPosition, album: AlbumResponseDto) => unknown) | undefined;
}
$: isCollapsed = !!group && isAlbumGroupCollapsed($albumViewSettings, group.id);
let {
albums,
group = undefined,
showOwner = false,
showDateRange = false,
showItemCount = false,
onShowContextMenu = undefined,
}: Props = $props();
let isCollapsed = $derived(!!group && isAlbumGroupCollapsed($albumViewSettings, group.id));
const showContextMenu = (position: ContextMenuPosition, album: AlbumResponseDto) => {
onShowContextMenu?.(position, album);
};
$: iconRotation = isCollapsed ? 'rotate-0' : 'rotate-90';
let iconRotation = $derived(isCollapsed ? 'rotate-0' : 'rotate-90');
const oncontextmenu = (event: MouseEvent, album: AlbumResponseDto) => {
event.preventDefault();
showContextMenu({ x: event.x, y: event.y }, album);
};
</script>
{#if group}
<div class="grid">
<button
type="button"
on:click={() => toggleAlbumGroupCollapsing(group.id)}
onclick={() => toggleAlbumGroupCollapsing(group.id)}
class="w-fit mt-2 pt-2 pr-2 mb-2 dark:text-immich-dark-fg"
aria-expanded={!isCollapsed}
>
@ -56,7 +71,7 @@
data-sveltekit-preload-data="hover"
href="{AppRoute.ALBUMS}/{album.id}"
animate:flip={{ duration: 400 }}
on:contextmenu|preventDefault={(e) => showContextMenu({ x: e.x, y: e.y }, album)}
oncontextmenu={(event) => oncontextmenu(event, album)}
>
<AlbumCard
{album}

View file

@ -8,12 +8,23 @@
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import { t } from 'svelte-i18n';
export let album: AlbumResponseDto;
export let showOwner = false;
export let showDateRange = false;
export let showItemCount = false;
export let preload = false;
export let onShowContextMenu: ((position: ContextMenuPosition) => unknown) | undefined = undefined;
interface Props {
album: AlbumResponseDto;
showOwner?: boolean;
showDateRange?: boolean;
showItemCount?: boolean;
preload?: boolean;
onShowContextMenu?: ((position: ContextMenuPosition) => unknown) | undefined;
}
let {
album,
showOwner = false,
showDateRange = false,
showItemCount = false,
preload = false,
onShowContextMenu = undefined,
}: Props = $props();
const showAlbumContextMenu = (e: MouseEvent) => {
e.stopPropagation();
@ -39,7 +50,7 @@
size="20"
padding="2"
class="icon-white-drop-shadow"
on:click={showAlbumContextMenu}
onclick={showAlbumContextMenu}
/>
</div>
{/if}

View file

@ -5,13 +5,18 @@
import AssetCover from '$lib/components/sharedlinks-page/covers/asset-cover.svelte';
import { t } from 'svelte-i18n';
export let album: AlbumResponseDto;
export let preload = false;
let className = '';
export { className as class };
interface Props {
album: AlbumResponseDto;
preload?: boolean;
class?: string;
}
$: alt = album.albumName || $t('unnamed_album');
$: thumbnailUrl = album.albumThumbnailAssetId ? getAssetThumbnailUrl({ id: album.albumThumbnailAssetId }) : null;
let { album, preload = false, class: className = '' }: Props = $props();
let alt = $derived(album.albumName || $t('unnamed_album'));
let thumbnailUrl = $derived(
album.albumThumbnailAssetId ? getAssetThumbnailUrl({ id: album.albumThumbnailAssetId }) : null,
);
</script>
{#if thumbnailUrl}

View file

@ -4,9 +4,13 @@
import AutogrowTextarea from '$lib/components/shared-components/autogrow-textarea.svelte';
import { t } from 'svelte-i18n';
export let id: string;
export let description: string;
export let isOwned: boolean;
interface Props {
id: string;
description: string;
isOwned: boolean;
}
let { id, description = $bindable(), isOwned }: Props = $props();
const handleUpdateDescription = async (newDescription: string) => {
try {

View file

@ -23,24 +23,38 @@
import { notificationController, NotificationType } from '../shared-components/notification/notification';
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
export let album: AlbumResponseDto;
export let order: AssetOrder | undefined;
export let user: UserResponseDto; // Declare user as a prop
export let onChangeOrder: (order: AssetOrder) => void;
export let onClose: () => void;
export let onToggleEnabledActivity: () => void;
export let onShowSelectSharedUser: () => void;
export let onRemove: (userId: string) => void;
export let onRefreshAlbum: () => void;
interface Props {
album: AlbumResponseDto;
order: AssetOrder | undefined;
user: UserResponseDto;
onChangeOrder: (order: AssetOrder) => void;
onClose: () => void;
onToggleEnabledActivity: () => void;
onShowSelectSharedUser: () => void;
onRemove: (userId: string) => void;
onRefreshAlbum: () => void;
}
let selectedRemoveUser: UserResponseDto | null = null;
let {
album,
order,
user,
onChangeOrder,
onClose,
onToggleEnabledActivity,
onShowSelectSharedUser,
onRemove,
onRefreshAlbum,
}: Props = $props();
let selectedRemoveUser: UserResponseDto | null = $state(null);
const options: Record<AssetOrder, RenderedOption> = {
[AssetOrder.Asc]: { icon: mdiArrowUpThin, title: $t('oldest_first') },
[AssetOrder.Desc]: { icon: mdiArrowDownThin, title: $t('newest_first') },
};
$: selectedOption = order ? options[order] : options[AssetOrder.Desc];
let selectedOption = $derived(order ? options[order] : options[AssetOrder.Desc]);
const handleToggle = async (returnedOption: RenderedOption): Promise<void> => {
if (selectedOption === returnedOption) {
@ -125,7 +139,7 @@
<div class="py-2">
<div class="text-gray text-sm mb-3">{$t('people').toUpperCase()}</div>
<div class="p-2">
<button type="button" class="flex items-center gap-2" on:click={onShowSelectSharedUser}>
<button type="button" class="flex items-center gap-2" onclick={onShowSelectSharedUser}>
<div class="rounded-full w-10 h-10 border border-gray-500 flex items-center justify-center">
<div><Icon path={mdiPlus} size="25" /></div>
</div>

View file

@ -4,10 +4,11 @@
import type { AlbumResponseDto } from '@immich/sdk';
import { t } from 'svelte-i18n';
export let album: AlbumResponseDto;
interface Props {
album: AlbumResponseDto;
}
$: startDate = formatDate(album.startDate);
$: endDate = formatDate(album.endDate);
let { album }: Props = $props();
const formatDate = (date?: string) => {
return date ? new Date(date).toLocaleDateString($locale, dateFormats.album) : undefined;
@ -24,6 +25,8 @@
return '';
};
let startDate = $derived(formatDate(album.startDate));
let endDate = $derived(formatDate(album.endDate));
</script>
<span class="my-2 flex gap-2 text-sm font-medium text-gray-500" data-testid="album-details">

View file

@ -4,12 +4,20 @@
import { shortcut } from '$lib/actions/shortcut';
import { t } from 'svelte-i18n';
export let id: string;
export let albumName: string;
export let isOwned: boolean;
export let onUpdate: (albumName: string) => void;
interface Props {
id: string;
albumName: string;
isOwned: boolean;
onUpdate: (albumName: string) => void;
}
$: newAlbumName = albumName;
let { id, albumName = $bindable(), isOwned, onUpdate }: Props = $props();
let newAlbumName = $state(albumName);
$effect(() => {
newAlbumName = albumName;
});
const handleUpdateName = async () => {
if (newAlbumName === albumName) {
@ -33,7 +41,7 @@
<input
use:shortcut={{ shortcut: { key: 'Enter' }, onShortcut: (e) => e.currentTarget.blur() }}
on:blur={handleUpdateName}
onblur={handleUpdateName}
class="w-[99%] mb-2 border-b-2 border-transparent text-2xl md:text-4xl lg:text-6xl text-immich-primary outline-none transition-all dark:text-immich-dark-primary {isOwned
? 'hover:border-gray-400'
: 'hover:border-transparent'} bg-immich-bg focus:border-b-2 focus:border-immich-primary focus:outline-none dark:bg-immich-dark-bg dark:focus:border-immich-dark-primary dark:focus:bg-immich-dark-gray"

View file

@ -21,11 +21,15 @@
import { t } from 'svelte-i18n';
import { onDestroy } from 'svelte';
export let sharedLink: SharedLinkResponseDto;
export let user: UserResponseDto | undefined = undefined;
interface Props {
sharedLink: SharedLinkResponseDto;
user?: UserResponseDto | undefined;
}
let { sharedLink, user = undefined }: Props = $props();
const album = sharedLink.album as AlbumResponseDto;
let innerWidth: number;
let innerWidth: number = $state(0);
let { isViewing: showAssetViewer } = assetViewingStore;
@ -70,15 +74,15 @@
</AssetSelectControlBar>
{:else}
<ControlAppBar showBackButton={false}>
<svelte:fragment slot="leading">
{#snippet leading()}
<ImmichLogoSmallLink width={innerWidth} />
</svelte:fragment>
{/snippet}
<svelte:fragment slot="trailing">
{#snippet trailing()}
{#if sharedLink.allowUpload}
<CircleIconButton
title={$t('add_photos')}
on:click={() => openFileUploadDialog({ albumId: album.id })}
onclick={() => openFileUploadDialog({ albumId: album.id })}
icon={mdiFileImagePlusOutline}
/>
{/if}
@ -86,13 +90,13 @@
{#if album.assetCount > 0 && sharedLink.allowDownload}
<CircleIconButton
title={$t('download')}
on:click={() => downloadAlbum(album)}
onclick={() => downloadAlbum(album)}
icon={mdiFolderDownloadOutline}
/>
{/if}
<ThemeButton />
</svelte:fragment>
{/snippet}
</ControlAppBar>
{/if}
</header>

View file

@ -38,8 +38,12 @@
import { fly } from 'svelte/transition';
import { t } from 'svelte-i18n';
export let albumGroups: string[];
export let searchQuery: string;
interface Props {
albumGroups: string[];
searchQuery: string;
}
let { albumGroups, searchQuery = $bindable() }: Props = $props();
const flipOrdering = (ordering: string) => {
return ordering === SortOrder.Asc ? SortOrder.Desc : SortOrder.Asc;
@ -73,62 +77,38 @@
$albumViewSettings.view === AlbumViewMode.Cover ? AlbumViewMode.List : AlbumViewMode.Cover;
};
let selectedGroupOption: AlbumGroupOptionMetadata;
let groupIcon: string;
$: selectedFilterOption = albumFilterNames[findFilterOption($albumViewSettings.filter)];
$: selectedSortOption = findSortOptionMetadata($albumViewSettings.sortBy);
$: {
selectedGroupOption = findGroupOptionMetadata($albumViewSettings.groupBy);
if (selectedGroupOption.isDisabled()) {
selectedGroupOption = findGroupOptionMetadata(AlbumGroupBy.None);
let groupIcon = $derived.by(() => {
if (selectedGroupOption?.id === AlbumGroupBy.None) {
return mdiFolderRemoveOutline;
}
}
return $albumViewSettings.groupOrder === SortOrder.Desc ? mdiFolderArrowDownOutline : mdiFolderArrowUpOutline;
});
// svelte-ignore reactive_declaration_non_reactive_property
$: {
if (selectedGroupOption.id === AlbumGroupBy.None) {
groupIcon = mdiFolderRemoveOutline;
} else {
groupIcon =
$albumViewSettings.groupOrder === SortOrder.Desc ? mdiFolderArrowDownOutline : mdiFolderArrowUpOutline;
}
}
let albumFilterNames: Record<AlbumFilter, string> = $derived({
[AlbumFilter.All]: $t('all'),
[AlbumFilter.Owned]: $t('owned'),
[AlbumFilter.Shared]: $t('shared'),
});
// svelte-ignore reactive_declaration_non_reactive_property
$: sortIcon = $albumViewSettings.sortOrder === SortOrder.Desc ? mdiArrowDownThin : mdiArrowUpThin;
let selectedFilterOption = $derived(albumFilterNames[findFilterOption($albumViewSettings.filter)]);
let selectedSortOption = $derived(findSortOptionMetadata($albumViewSettings.sortBy));
let selectedGroupOption = $derived(findGroupOptionMetadata($albumViewSettings.groupBy));
let sortIcon = $derived($albumViewSettings.sortOrder === SortOrder.Desc ? mdiArrowDownThin : mdiArrowUpThin);
// svelte-ignore reactive_declaration_non_reactive_property
$: albumFilterNames = ((): Record<AlbumFilter, string> => {
return {
[AlbumFilter.All]: $t('all'),
[AlbumFilter.Owned]: $t('owned'),
[AlbumFilter.Shared]: $t('shared'),
};
})();
let albumSortByNames: Record<AlbumSortBy, string> = $derived({
[AlbumSortBy.Title]: $t('sort_title'),
[AlbumSortBy.ItemCount]: $t('sort_items'),
[AlbumSortBy.DateModified]: $t('sort_modified'),
[AlbumSortBy.DateCreated]: $t('sort_created'),
[AlbumSortBy.MostRecentPhoto]: $t('sort_recent'),
[AlbumSortBy.OldestPhoto]: $t('sort_oldest'),
});
// svelte-ignore reactive_declaration_non_reactive_property
$: albumSortByNames = ((): Record<AlbumSortBy, string> => {
return {
[AlbumSortBy.Title]: $t('sort_title'),
[AlbumSortBy.ItemCount]: $t('sort_items'),
[AlbumSortBy.DateModified]: $t('sort_modified'),
[AlbumSortBy.DateCreated]: $t('sort_created'),
[AlbumSortBy.MostRecentPhoto]: $t('sort_recent'),
[AlbumSortBy.OldestPhoto]: $t('sort_oldest'),
};
})();
// svelte-ignore reactive_declaration_non_reactive_property
$: albumGroupByNames = ((): Record<AlbumGroupBy, string> => {
return {
[AlbumGroupBy.None]: $t('group_no'),
[AlbumGroupBy.Owner]: $t('group_owner'),
[AlbumGroupBy.Year]: $t('group_year'),
};
})();
let albumGroupByNames: Record<AlbumGroupBy, string> = $derived({
[AlbumGroupBy.None]: $t('group_no'),
[AlbumGroupBy.Owner]: $t('group_owner'),
[AlbumGroupBy.Year]: $t('group_year'),
});
</script>
<!-- Filter Albums by Sharing Status (All, Owned, Shared) -->
@ -147,7 +127,7 @@
</div>
<!-- Create Album -->
<LinkButton on:click={() => createAlbumAndRedirect()}>
<LinkButton onclick={() => createAlbumAndRedirect()}>
<div class="flex place-items-center gap-2 text-sm">
<Icon path={mdiPlusBoxOutline} size="18" />
<p class="hidden md:block">{$t('create_album')}</p>
@ -184,7 +164,7 @@
<!-- Expand Album Groups -->
<div class="hidden xl:flex gap-0">
<div class="block">
<LinkButton title={$t('expand_all')} on:click={() => expandAllAlbumGroups()}>
<LinkButton title={$t('expand_all')} onclick={() => expandAllAlbumGroups()}>
<div class="flex place-items-center gap-2 text-sm">
<Icon path={mdiUnfoldMoreHorizontal} size="18" />
</div>
@ -193,7 +173,7 @@
<!-- Collapse Album Groups -->
<div class="block">
<LinkButton title={$t('collapse_all')} on:click={() => collapseAllAlbumGroups(albumGroups)}>
<LinkButton title={$t('collapse_all')} onclick={() => collapseAllAlbumGroups(albumGroups)}>
<div class="flex place-items-center gap-2 text-sm">
<Icon path={mdiUnfoldLessHorizontal} size="18" />
</div>
@ -204,7 +184,7 @@
{/if}
<!-- Cover/List Display Toggle -->
<LinkButton on:click={() => handleChangeListMode()}>
<LinkButton onclick={() => handleChangeListMode()}>
<div class="flex place-items-center gap-2 text-sm">
{#if $albumViewSettings.view === AlbumViewMode.List}
<Icon path={mdiViewGridOutline} size="18" />

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { onMount } from 'svelte';
import { onMount, type Snippet } from 'svelte';
import { groupBy } from 'lodash-es';
import { addUsersToAlbum, deleteAlbum, type AlbumUserAddDto, type AlbumResponseDto, isHttpError } from '@immich/sdk';
import { mdiDeleteOutline, mdiShareVariantOutline, mdiFolderDownloadOutline, mdiRenameOutline } from '@mdi/js';
@ -38,14 +38,29 @@
import { goto } from '$app/navigation';
import { AppRoute } from '$lib/constants';
import { t } from 'svelte-i18n';
import { run } from 'svelte/legacy';
export let ownedAlbums: AlbumResponseDto[] = [];
export let sharedAlbums: AlbumResponseDto[] = [];
export let searchQuery: string = '';
export let userSettings: AlbumViewSettings;
export let allowEdit = false;
export let showOwner = false;
export let albumGroupIds: string[] = [];
interface Props {
ownedAlbums?: AlbumResponseDto[];
sharedAlbums?: AlbumResponseDto[];
searchQuery?: string;
userSettings: AlbumViewSettings;
allowEdit?: boolean;
showOwner?: boolean;
albumGroupIds?: string[];
empty?: Snippet;
}
let {
ownedAlbums = $bindable([]),
sharedAlbums = $bindable([]),
searchQuery = '',
userSettings,
allowEdit = false,
showOwner = false,
albumGroupIds = $bindable([]),
empty,
}: Props = $props();
interface AlbumGroupOption {
[option: string]: (order: SortOrder, albums: AlbumResponseDto[]) => AlbumGroup[];
@ -118,25 +133,24 @@
},
};
let albums: AlbumResponseDto[] = [];
let filteredAlbums: AlbumResponseDto[] = [];
let groupedAlbums: AlbumGroup[] = [];
let albums: AlbumResponseDto[] = $state([]);
let filteredAlbums: AlbumResponseDto[] = $state([]);
let groupedAlbums: AlbumGroup[] = $state([]);
let albumGroupOption: string = AlbumGroupBy.None;
let albumGroupOption: string = $state(AlbumGroupBy.None);
let showShareByURLModal = false;
let showShareByURLModal = $state(false);
let albumToEdit: AlbumResponseDto | null = null;
let albumToShare: AlbumResponseDto | null = null;
let albumToEdit: AlbumResponseDto | null = $state(null);
let albumToShare: AlbumResponseDto | null = $state(null);
let albumToDelete: AlbumResponseDto | null = null;
let contextMenuPosition: ContextMenuPosition = { x: 0, y: 0 };
let contextMenuTargetAlbum: AlbumResponseDto | null = null;
let isOpen = false;
let contextMenuPosition: ContextMenuPosition = $state({ x: 0, y: 0 });
let contextMenuTargetAlbum: AlbumResponseDto | undefined = $state();
let isOpen = $state(false);
// Step 1: Filter between Owned and Shared albums, or both.
// svelte-ignore reactive_declaration_non_reactive_property
$: {
run(() => {
switch (userSettings.filter) {
case AlbumFilter.Owned: {
albums = ownedAlbums;
@ -152,10 +166,10 @@
albums = nonOwnedAlbums.length > 0 ? ownedAlbums.concat(nonOwnedAlbums) : ownedAlbums;
}
}
}
});
// Step 2: Filter using the given search query.
$: {
run(() => {
if (searchQuery) {
const searchAlbumNormalized = normalizeSearchString(searchQuery);
@ -165,17 +179,17 @@
} else {
filteredAlbums = albums;
}
}
});
// Step 3: Group albums.
$: {
run(() => {
albumGroupOption = getSelectedAlbumGroupOption(userSettings);
const groupFunc = groupOptions[albumGroupOption] ?? groupOptions[AlbumGroupBy.None];
groupedAlbums = groupFunc(stringToSortOrder(userSettings.groupOrder), filteredAlbums);
}
});
// Step 4: Sort albums amongst each group.
$: {
run(() => {
groupedAlbums = groupedAlbums.map((group) => ({
id: group.id,
name: group.name,
@ -183,9 +197,11 @@
}));
albumGroupIds = groupedAlbums.map(({ id }) => id);
}
});
$: showFullContextMenu = allowEdit && contextMenuTargetAlbum && contextMenuTargetAlbum.ownerId === $user.id;
let showFullContextMenu = $derived(
allowEdit && contextMenuTargetAlbum && contextMenuTargetAlbum.ownerId === $user.id,
);
onMount(async () => {
if (allowEdit) {
@ -320,6 +336,10 @@
};
const openShareModal = () => {
if (!contextMenuTargetAlbum) {
return;
}
albumToShare = contextMenuTargetAlbum;
closeAlbumContextMenu();
};
@ -359,7 +379,7 @@
{/if}
{:else}
<!-- Empty Message -->
<slot name="empty" />
{@render empty?.()}
{/if}
<!-- Context Menu -->

View file

@ -3,7 +3,11 @@
import type { AlbumSortOptionMetadata } from '$lib/utils/album-utils';
import { t } from 'svelte-i18n';
export let option: AlbumSortOptionMetadata;
interface Props {
option: AlbumSortOptionMetadata;
}
let { option }: Props = $props();
const handleSort = () => {
if ($albumViewSettings.sortBy === option.id) {
@ -13,24 +17,22 @@
$albumViewSettings.sortOrder = option.defaultOrder;
}
};
// svelte-ignore reactive_declaration_non_reactive_property
$: albumSortByNames = ((): Record<AlbumSortBy, string> => {
return {
[AlbumSortBy.Title]: $t('sort_title'),
[AlbumSortBy.ItemCount]: $t('sort_items'),
[AlbumSortBy.DateModified]: $t('sort_modified'),
[AlbumSortBy.DateCreated]: $t('sort_created'),
[AlbumSortBy.MostRecentPhoto]: $t('sort_recent'),
[AlbumSortBy.OldestPhoto]: $t('sort_oldest'),
};
})();
let albumSortByNames: Record<AlbumSortBy, string> = $derived({
[AlbumSortBy.Title]: $t('sort_title'),
[AlbumSortBy.ItemCount]: $t('sort_items'),
[AlbumSortBy.DateModified]: $t('sort_modified'),
[AlbumSortBy.DateCreated]: $t('sort_created'),
[AlbumSortBy.MostRecentPhoto]: $t('sort_recent'),
[AlbumSortBy.OldestPhoto]: $t('sort_oldest'),
});
</script>
<th class="text-sm font-medium {option.columnStyle}">
<button
type="button"
class="rounded-lg p-2 hover:bg-immich-dark-primary hover:dark:bg-immich-dark-primary/50"
on:click={handleSort}
onclick={handleSort}
>
{#if $albumViewSettings.sortBy === option.id}
{#if $albumViewSettings.sortOrder === SortOrder.Desc}

View file

@ -9,9 +9,12 @@
import Icon from '$lib/components/elements/icon.svelte';
import { t } from 'svelte-i18n';
export let album: AlbumResponseDto;
export let onShowContextMenu: ((position: ContextMenuPosition, album: AlbumResponseDto) => unknown) | undefined =
undefined;
interface Props {
album: AlbumResponseDto;
onShowContextMenu?: ((position: ContextMenuPosition, album: AlbumResponseDto) => unknown) | undefined;
}
let { album, onShowContextMenu = undefined }: Props = $props();
const showContextMenu = (position: ContextMenuPosition) => {
onShowContextMenu?.(position, album);
@ -20,12 +23,17 @@
const dateLocaleString = (dateString: string) => {
return new Date(dateString).toLocaleDateString($locale, dateFormats.album);
};
const oncontextmenu = (event: MouseEvent) => {
event.preventDefault();
showContextMenu({ x: event.x, y: event.y });
};
</script>
<tr
class="flex h-[50px] w-full place-items-center border-[3px] border-transparent p-2 text-center odd:bg-immich-gray even:bg-immich-bg hover:cursor-pointer hover:border-immich-primary/75 odd:dark:bg-immich-dark-gray/75 even:dark:bg-immich-dark-gray/50 dark:hover:border-immich-dark-primary/75 md:p-5"
on:click={() => goto(`${AppRoute.ALBUMS}/${album.id}`)}
on:contextmenu|preventDefault={(e) => showContextMenu({ x: e.x, y: e.y })}
onclick={() => goto(`${AppRoute.ALBUMS}/${album.id}`)}
{oncontextmenu}
>
<td class="text-md text-ellipsis text-left w-8/12 sm:w-4/12 md:w-4/12 xl:w-[30%] 2xl:w-[40%] items-center">
{album.albumName}

View file

@ -15,10 +15,13 @@
} from '$lib/utils/album-utils';
import { t } from 'svelte-i18n';
export let groupedAlbums: AlbumGroup[];
export let albumGroupOption: string = AlbumGroupBy.None;
export let onShowContextMenu: ((position: ContextMenuPosition, album: AlbumResponseDto) => unknown) | undefined =
undefined;
interface Props {
groupedAlbums: AlbumGroup[];
albumGroupOption?: string;
onShowContextMenu?: ((position: ContextMenuPosition, album: AlbumResponseDto) => unknown) | undefined;
}
let { groupedAlbums, albumGroupOption = AlbumGroupBy.None, onShowContextMenu }: Props = $props();
</script>
<table class="mt-2 w-full text-left">
@ -46,7 +49,7 @@
>
<tr
class="flex w-full place-items-center p-2 md:pl-5 md:pr-5 md:pt-3 md:pb-3"
on:click={() => toggleAlbumGroupCollapsing(albumGroup.id)}
onclick={() => toggleAlbumGroupCollapsing(albumGroup.id)}
aria-expanded={!isCollapsed}
>
<td class="text-md text-left -mb-1">

View file

@ -18,15 +18,19 @@
import { t } from 'svelte-i18n';
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
export let album: AlbumResponseDto;
export let onClose: () => void;
export let onRemove: (userId: string) => void;
export let onRefreshAlbum: () => void;
interface Props {
album: AlbumResponseDto;
onClose: () => void;
onRemove: (userId: string) => void;
onRefreshAlbum: () => void;
}
let currentUser: UserResponseDto;
let selectedRemoveUser: UserResponseDto | null = null;
let { album, onClose, onRemove, onRefreshAlbum }: Props = $props();
$: isOwned = currentUser?.id == album.ownerId;
let currentUser: UserResponseDto | undefined = $state();
let selectedRemoveUser: UserResponseDto | null = $state(null);
let isOwned = $derived(currentUser?.id == album.ownerId);
onMount(async () => {
try {
@ -123,7 +127,7 @@
{:else if user.id == currentUser?.id}
<button
type="button"
on:click={() => (selectedRemoveUser = user)}
onclick={() => (selectedRemoveUser = user)}
class="text-sm font-medium text-immich-primary transition-colors hover:text-immich-primary/75 dark:text-immich-dark-primary"
>{$t('leave')}</button
>

View file

@ -18,13 +18,17 @@
import UserAvatar from '../shared-components/user-avatar.svelte';
import { t } from 'svelte-i18n';
export let album: AlbumResponseDto;
export let onClose: () => void;
export let onSelect: (selectedUsers: AlbumUserAddDto[]) => void;
export let onShare: () => void;
interface Props {
album: AlbumResponseDto;
onClose: () => void;
onSelect: (selectedUsers: AlbumUserAddDto[]) => void;
onShare: () => void;
}
let users: UserResponseDto[] = [];
let selectedUsers: Record<string, { user: UserResponseDto; role: AlbumUserRole }> = {};
let { album, onClose, onSelect, onShare }: Props = $props();
let users: UserResponseDto[] = $state([]);
let selectedUsers: Record<string, { user: UserResponseDto; role: AlbumUserRole }> = $state({});
const roleOptions: Array<{ title: string; value: AlbumUserRole | 'none'; icon?: string }> = [
{ title: $t('role_editor'), value: AlbumUserRole.Editor, icon: mdiPencil },
@ -32,7 +36,7 @@
{ title: $t('remove_user'), value: 'none' },
];
let sharedLinks: SharedLinkResponseDto[] = [];
let sharedLinks: SharedLinkResponseDto[] = $state([]);
onMount(async () => {
await getSharedLinks();
const data = await searchUsers();
@ -121,11 +125,7 @@
{#each users as user}
{#if !Object.keys(selectedUsers).includes(user.id)}
<div class="flex place-items-center transition-all hover:bg-gray-200 dark:hover:bg-gray-700 rounded-xl">
<button
type="button"
on:click={() => handleToggle(user)}
class="flex w-full place-items-center gap-4 p-4"
>
<button type="button" onclick={() => handleToggle(user)} class="flex w-full place-items-center gap-4 p-4">
<UserAvatar {user} size="md" />
<div class="text-left flex-grow">
<p class="text-immich-fg dark:text-immich-dark-fg">
@ -150,7 +150,7 @@
fullwidth
rounded="full"
disabled={Object.keys(selectedUsers).length === 0}
on:click={() =>
onclick={() =>
onSelect(Object.values(selectedUsers).map(({ user, ...rest }) => ({ userId: user.id, ...rest })))}
>{$t('add')}</Button
>
@ -163,7 +163,7 @@
<button
type="button"
class="flex flex-col place-content-center place-items-center gap-2 hover:cursor-pointer"
on:click={onShare}
onclick={onShare}
>
<Icon path={mdiLink} size={24} />
<p class="text-sm">{$t('create_link')}</p>