1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-03-01 15:11:21 +01:00

fix(web): minor album card issues (#7975)

* fix(web): minor album card issues

* fix album grid gap
This commit is contained in:
Michel Heusschen 2024-03-15 17:03:54 +01:00 committed by GitHub
parent 0f79c4ff46
commit cfb14ca80b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 42 additions and 71 deletions

View file

@ -13,6 +13,7 @@ vi.mock('@immich/sdk', async (originalImport) => {
}); });
const sdkMock: MockedObject<typeof sdk> = sdk as MockedObject<typeof sdk>; const sdkMock: MockedObject<typeof sdk> = sdk as MockedObject<typeof sdk>;
const onShowContextMenu = vi.fn();
describe('AlbumCard component', () => { describe('AlbumCard component', () => {
let sut: RenderResult<AlbumCard>; let sut: RenderResult<AlbumCard>;
@ -90,34 +91,30 @@ describe('AlbumCard component', () => {
expect(albumDetailsElement).toHaveTextContent('0 items'); expect(albumDetailsElement).toHaveTextContent('0 items');
}); });
it('hides context menu when "onShowContextMenu" is undefined', () => {
const album = Object.freeze(albumFactory.build({ albumThumbnailAssetId: null }));
sut = render(AlbumCard, { album });
const contextButtonParent = sut.queryByTestId('context-button-parent');
expect(contextButtonParent).not.toBeInTheDocument();
});
describe('with rendered component - no thumbnail', () => { describe('with rendered component - no thumbnail', () => {
const album = Object.freeze(albumFactory.build({ albumThumbnailAssetId: null })); const album = Object.freeze(albumFactory.build({ albumThumbnailAssetId: null }));
beforeEach(async () => { beforeEach(async () => {
sut = render(AlbumCard, { album }); sut = render(AlbumCard, { album, onShowContextMenu });
const albumImgElement = sut.getByTestId('album-image'); const albumImgElement = sut.getByTestId('album-image');
await waitFor(() => expect(albumImgElement).toHaveAttribute('src')); await waitFor(() => expect(albumImgElement).toHaveAttribute('src'));
}); });
it('dispatches custom "click" event with the album in context', async () => { it('dispatches "onShowContextMenu" event on context menu click with mouse coordinates', async () => {
const onClickHandler = vi.fn(); const contextMenuButton = sut.getByTestId('context-button-parent').children[0];
sut.component.$on('click', onClickHandler); expect(contextMenuButton).toBeDefined();
const albumCardElement = sut.getByTestId('album-card');
await fireEvent.click(albumCardElement);
expect(onClickHandler).toHaveBeenCalledTimes(1);
expect(onClickHandler).toHaveBeenCalledWith(expect.objectContaining({ detail: album }));
});
it('dispatches custom "click" event on context menu click with mouse coordinates', async () => {
const onClickHandler = vi.fn();
sut.component.$on('showalbumcontextmenu', onClickHandler);
const contextMenuButtonParent = sut.getByTestId('context-button-parent');
// Mock getBoundingClientRect to return a bounding rectangle that will result in the expected position // Mock getBoundingClientRect to return a bounding rectangle that will result in the expected position
contextMenuButtonParent.getBoundingClientRect = () => ({ contextMenuButton.getBoundingClientRect = () => ({
x: 123, x: 123,
y: 456, y: 456,
width: 0, width: 0,
@ -130,14 +127,14 @@ describe('AlbumCard component', () => {
}); });
await fireEvent( await fireEvent(
contextMenuButtonParent, contextMenuButton,
new MouseEvent('click', { new MouseEvent('click', {
clientX: 123, clientX: 123,
clientY: 456, clientY: 456,
}), }),
); );
expect(onClickHandler).toHaveBeenCalledTimes(1); expect(onShowContextMenu).toHaveBeenCalledTimes(1);
expect(onClickHandler).toHaveBeenCalledWith(expect.objectContaining({ detail: { x: 123, y: 456 } })); expect(onShowContextMenu).toHaveBeenCalledWith(expect.objectContaining({ x: 123, y: 456 }));
}); });
}); });
}); });

View file

@ -5,25 +5,20 @@
import { getAssetThumbnailUrl } from '$lib/utils'; import { getAssetThumbnailUrl } from '$lib/utils';
import { ThumbnailFormat, getAssetThumbnail, getUserById, type AlbumResponseDto } from '@immich/sdk'; import { ThumbnailFormat, getAssetThumbnail, getUserById, type AlbumResponseDto } from '@immich/sdk';
import { mdiDotsVertical } from '@mdi/js'; import { mdiDotsVertical } from '@mdi/js';
import { createEventDispatcher, onMount } from 'svelte'; import { onMount } from 'svelte';
import { getContextMenuPosition } from '../../utils/context-menu'; import { getContextMenuPosition, type ContextMenuPosition } from '../../utils/context-menu';
import IconButton from '../elements/buttons/icon-button.svelte'; import IconButton from '../elements/buttons/icon-button.svelte';
import type { OnClick, OnShowContextMenu } from './album-card';
export let album: AlbumResponseDto; export let album: AlbumResponseDto;
export let isSharingView = false; export let isSharingView = false;
export let showItemCount = true; export let showItemCount = true;
export let showContextMenu = true;
export let preload = false; export let preload = false;
let showVerticalDots = false; export let onShowContextMenu: ((position: ContextMenuPosition) => void) | undefined = undefined;
$: imageData = album.albumThumbnailAssetId $: imageData = album.albumThumbnailAssetId
? getAssetThumbnailUrl(album.albumThumbnailAssetId, ThumbnailFormat.Webp) ? getAssetThumbnailUrl(album.albumThumbnailAssetId, ThumbnailFormat.Webp)
: null; : null;
const dispatchClick = createEventDispatcher<OnClick>();
const dispatchShowContextMenu = createEventDispatcher<OnShowContextMenu>();
const loadHighQualityThumbnail = async (assetId: string | null) => { const loadHighQualityThumbnail = async (assetId: string | null) => {
if (!assetId) { if (!assetId) {
return; return;
@ -33,8 +28,11 @@
return URL.createObjectURL(data); return URL.createObjectURL(data);
}; };
const showAlbumContextMenu = (e: MouseEvent) => const showAlbumContextMenu = (e: MouseEvent) => {
dispatchShowContextMenu('showalbumcontextmenu', getContextMenuPosition(e)); e.stopPropagation();
e.preventDefault();
onShowContextMenu?.(getContextMenuPosition(e));
};
onMount(async () => { onMount(async () => {
imageData = (await loadHighQualityThumbnail(album.albumThumbnailAssetId)) || null; imageData = (await loadHighQualityThumbnail(album.albumThumbnailAssetId)) || null;
@ -43,25 +41,17 @@
const getAlbumOwnerInfo = () => getUserById({ id: album.ownerId }); const getAlbumOwnerInfo = () => getUserById({ id: album.ownerId });
</script> </script>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div <div
class="group relative mt-4 rounded-2xl border-[1px] border-transparent p-5 hover:cursor-pointer hover:bg-gray-100 hover:border-gray-200 dark:hover:border-gray-800 dark:hover:bg-gray-900" class="group relative rounded-2xl border border-transparent p-5 hover:bg-gray-100 hover:border-gray-200 dark:hover:border-gray-800 dark:hover:bg-gray-900"
on:click={() => dispatchClick('click', album)}
on:keydown={() => dispatchClick('click', album)}
on:mouseenter={() => (showVerticalDots = true)}
on:mouseleave={() => (showVerticalDots = false)}
data-testid="album-card" data-testid="album-card"
> >
<!-- svelte-ignore a11y-click-events-have-key-events --> {#if onShowContextMenu}
{#if showContextMenu}
<div <div
id={`icon-${album.id}`} id="icon-{album.id}"
class="absolute right-6 top-6 z-10" class="absolute right-6 top-6 z-10 opacity-0 group-hover:opacity-100 focus-within:opacity-100"
on:click|stopPropagation|preventDefault={showAlbumContextMenu}
class:hidden={!showVerticalDots}
data-testid="context-button-parent" data-testid="context-button-parent"
> >
<IconButton color="transparent-primary"> <IconButton color="transparent-primary" title="Show album options" on:click={showAlbumContextMenu}>
<Icon path={mdiDotsVertical} size="20" class="icon-white-drop-shadow text-white" /> <Icon path={mdiDotsVertical} size="20" class="icon-white-drop-shadow text-white" />
</IconButton> </IconButton>
</div> </div>

View file

@ -1,12 +0,0 @@
import type { AlbumResponseDto } from '@immich/sdk';
export type OnShowContextMenu = {
showalbumcontextmenu: OnShowContextMenuDetail;
};
export type OnClick = {
click: OnClickDetail;
};
export type OnShowContextMenuDetail = { x: number; y: number };
export type OnClickDetail = AlbumResponseDto;

View file

@ -1,7 +1,6 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
import { AlbumViewMode, albumViewSettings } from '$lib/stores/preferences.store'; import { AlbumViewMode, albumViewSettings } from '$lib/stores/preferences.store';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import type { OnShowContextMenuDetail } from '$lib/components/album-page/album-card';
import { AppRoute } from '$lib/constants'; import { AppRoute } from '$lib/constants';
import { createAlbum, deleteAlbum, type AlbumResponseDto } from '@immich/sdk'; import { createAlbum, deleteAlbum, type AlbumResponseDto } from '@immich/sdk';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
@ -118,6 +117,7 @@
import ContextMenu from '$lib/components/shared-components/context-menu/context-menu.svelte'; import ContextMenu from '$lib/components/shared-components/context-menu/context-menu.svelte';
import AlbumsTable from '$lib/components/album-page/albums-table.svelte'; import AlbumsTable from '$lib/components/album-page/albums-table.svelte';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import type { ContextMenuPosition } from '$lib/utils/context-menu';
export let albums: AlbumResponseDto[]; export let albums: AlbumResponseDto[];
export let searchAlbum: string; export let searchAlbum: string;
@ -125,7 +125,7 @@
let shouldShowEditAlbumForm = false; let shouldShowEditAlbumForm = false;
let selectedAlbum: AlbumResponseDto; let selectedAlbum: AlbumResponseDto;
let albumToDelete: AlbumResponseDto | null; let albumToDelete: AlbumResponseDto | null;
let contextMenuPosition: OnShowContextMenuDetail = { x: 0, y: 0 }; let contextMenuPosition: ContextMenuPosition = { x: 0, y: 0 };
let contextMenuTargetAlbum: AlbumResponseDto | undefined = undefined; let contextMenuTargetAlbum: AlbumResponseDto | undefined = undefined;
$: { $: {
@ -145,7 +145,7 @@
await removeAlbumsIfEmpty(); await removeAlbumsIfEmpty();
}); });
function showAlbumContextMenu(contextMenuDetail: OnShowContextMenuDetail, album: AlbumResponseDto): void { function showAlbumContextMenu(contextMenuDetail: ContextMenuPosition, album: AlbumResponseDto): void {
contextMenuTargetAlbum = album; contextMenuTargetAlbum = album;
contextMenuPosition = { contextMenuPosition = {
x: contextMenuDetail.x, x: contextMenuDetail.x,
@ -228,13 +228,13 @@
{#if albums.length > 0} {#if albums.length > 0}
<!-- Album Card --> <!-- Album Card -->
{#if $albumViewSettings.view === AlbumViewMode.Cover} {#if $albumViewSettings.view === AlbumViewMode.Cover}
<div class="grid grid-cols-[repeat(auto-fill,minmax(14rem,1fr))]"> <div class="grid grid-cols-[repeat(auto-fill,minmax(14rem,1fr))] mt-4 gap-y-4">
{#each albumsFiltered as album, index (album.id)} {#each albumsFiltered as album, index (album.id)}
<a data-sveltekit-preload-data="hover" href="{AppRoute.ALBUMS}/{album.id}" animate:flip={{ duration: 200 }}> <a data-sveltekit-preload-data="hover" href="{AppRoute.ALBUMS}/{album.id}" animate:flip={{ duration: 200 }}>
<AlbumCard <AlbumCard
preload={index < 20} preload={index < 20}
{album} {album}
on:showalbumcontextmenu={({ detail }) => showAlbumContextMenu(detail, album)} onShowContextMenu={(position) => showAlbumContextMenu(position, album)}
/> />
</a> </a>
{/each} {/each}

View file

@ -1,6 +1,8 @@
export type Align = 'middle' | 'top-left' | 'top-right'; export type Align = 'middle' | 'top-left' | 'top-right';
export const getContextMenuPosition = (event: MouseEvent, align: Align = 'middle') => { export type ContextMenuPosition = { x: number; y: number };
export const getContextMenuPosition = (event: MouseEvent, align: Align = 'middle'): ContextMenuPosition => {
const { x, y, currentTarget, target } = event; const { x, y, currentTarget, target } = event;
const box = ((currentTarget || target) as HTMLElement)?.getBoundingClientRect(); const box = ((currentTarget || target) as HTMLElement)?.getBoundingClientRect();
if (box) { if (box) {

View file

@ -267,16 +267,10 @@
{#if searchResultAlbums.length > 0} {#if searchResultAlbums.length > 0}
<section> <section>
<div class="ml-6 text-4xl font-medium text-black/70 dark:text-white/80">ALBUMS</div> <div class="ml-6 text-4xl font-medium text-black/70 dark:text-white/80">ALBUMS</div>
<div class="grid grid-cols-[repeat(auto-fill,minmax(14rem,1fr))]"> <div class="grid grid-cols-[repeat(auto-fill,minmax(14rem,1fr))] mt-4 gap-y-4">
{#each searchResultAlbums as album, index (album.id)} {#each searchResultAlbums as album, index (album.id)}
<a data-sveltekit-preload-data="hover" href={`albums/${album.id}`} animate:flip={{ duration: 200 }}> <a data-sveltekit-preload-data="hover" href={`albums/${album.id}`} animate:flip={{ duration: 200 }}>
<AlbumCard <AlbumCard preload={index < 20} {album} isSharingView={false} showItemCount={false} />
preload={index < 20}
{album}
isSharingView={false}
showItemCount={false}
showContextMenu={false}
/>
</a> </a>
{/each} {/each}
</div> </div>

View file

@ -80,10 +80,10 @@
<div> <div>
<!-- Share Album List --> <!-- Share Album List -->
<div class="grid grid-cols-[repeat(auto-fill,minmax(14rem,1fr))]"> <div class="grid grid-cols-[repeat(auto-fill,minmax(14rem,1fr))] mt-4 gap-y-4">
{#each data.sharedAlbums as album, index (album.id)} {#each data.sharedAlbums as album, index (album.id)}
<a data-sveltekit-preload-data="hover" href={`albums/${album.id}`} animate:flip={{ duration: 200 }}> <a data-sveltekit-preload-data="hover" href={`albums/${album.id}`} animate:flip={{ duration: 200 }}>
<AlbumCard preload={index < 20} {album} isSharingView showContextMenu={false} /> <AlbumCard preload={index < 20} {album} isSharingView />
</a> </a>
{/each} {/each}
</div> </div>