1
0
mirror of https://github.com/immich-app/immich.git synced 2024-12-25 10:43:13 +02: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 onShowContextMenu = vi.fn();
describe('AlbumCard component', () => {
let sut: RenderResult<AlbumCard>;
@ -90,34 +91,30 @@ describe('AlbumCard component', () => {
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', () => {
const album = Object.freeze(albumFactory.build({ albumThumbnailAssetId: null }));
beforeEach(async () => {
sut = render(AlbumCard, { album });
sut = render(AlbumCard, { album, onShowContextMenu });
const albumImgElement = sut.getByTestId('album-image');
await waitFor(() => expect(albumImgElement).toHaveAttribute('src'));
});
it('dispatches custom "click" event with the album in context', async () => {
const onClickHandler = vi.fn();
sut.component.$on('click', onClickHandler);
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');
it('dispatches "onShowContextMenu" event on context menu click with mouse coordinates', async () => {
const contextMenuButton = sut.getByTestId('context-button-parent').children[0];
expect(contextMenuButton).toBeDefined();
// Mock getBoundingClientRect to return a bounding rectangle that will result in the expected position
contextMenuButtonParent.getBoundingClientRect = () => ({
contextMenuButton.getBoundingClientRect = () => ({
x: 123,
y: 456,
width: 0,
@ -130,14 +127,14 @@ describe('AlbumCard component', () => {
});
await fireEvent(
contextMenuButtonParent,
contextMenuButton,
new MouseEvent('click', {
clientX: 123,
clientY: 456,
}),
);
expect(onClickHandler).toHaveBeenCalledTimes(1);
expect(onClickHandler).toHaveBeenCalledWith(expect.objectContaining({ detail: { x: 123, y: 456 } }));
expect(onShowContextMenu).toHaveBeenCalledTimes(1);
expect(onShowContextMenu).toHaveBeenCalledWith(expect.objectContaining({ x: 123, y: 456 }));
});
});
});

View File

@ -5,25 +5,20 @@
import { getAssetThumbnailUrl } from '$lib/utils';
import { ThumbnailFormat, getAssetThumbnail, getUserById, type AlbumResponseDto } from '@immich/sdk';
import { mdiDotsVertical } from '@mdi/js';
import { createEventDispatcher, onMount } from 'svelte';
import { getContextMenuPosition } from '../../utils/context-menu';
import { onMount } from 'svelte';
import { getContextMenuPosition, type ContextMenuPosition } from '../../utils/context-menu';
import IconButton from '../elements/buttons/icon-button.svelte';
import type { OnClick, OnShowContextMenu } from './album-card';
export let album: AlbumResponseDto;
export let isSharingView = false;
export let showItemCount = true;
export let showContextMenu = true;
export let preload = false;
let showVerticalDots = false;
export let onShowContextMenu: ((position: ContextMenuPosition) => void) | undefined = undefined;
$: imageData = album.albumThumbnailAssetId
? getAssetThumbnailUrl(album.albumThumbnailAssetId, ThumbnailFormat.Webp)
: null;
const dispatchClick = createEventDispatcher<OnClick>();
const dispatchShowContextMenu = createEventDispatcher<OnShowContextMenu>();
const loadHighQualityThumbnail = async (assetId: string | null) => {
if (!assetId) {
return;
@ -33,8 +28,11 @@
return URL.createObjectURL(data);
};
const showAlbumContextMenu = (e: MouseEvent) =>
dispatchShowContextMenu('showalbumcontextmenu', getContextMenuPosition(e));
const showAlbumContextMenu = (e: MouseEvent) => {
e.stopPropagation();
e.preventDefault();
onShowContextMenu?.(getContextMenuPosition(e));
};
onMount(async () => {
imageData = (await loadHighQualityThumbnail(album.albumThumbnailAssetId)) || null;
@ -43,25 +41,17 @@
const getAlbumOwnerInfo = () => getUserById({ id: album.ownerId });
</script>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<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"
on:click={() => dispatchClick('click', album)}
on:keydown={() => dispatchClick('click', album)}
on:mouseenter={() => (showVerticalDots = true)}
on:mouseleave={() => (showVerticalDots = false)}
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"
data-testid="album-card"
>
<!-- svelte-ignore a11y-click-events-have-key-events -->
{#if showContextMenu}
{#if onShowContextMenu}
<div
id={`icon-${album.id}`}
class="absolute right-6 top-6 z-10"
on:click|stopPropagation|preventDefault={showAlbumContextMenu}
class:hidden={!showVerticalDots}
id="icon-{album.id}"
class="absolute right-6 top-6 z-10 opacity-0 group-hover:opacity-100 focus-within:opacity-100"
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" />
</IconButton>
</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">
import { AlbumViewMode, albumViewSettings } from '$lib/stores/preferences.store';
import { goto } from '$app/navigation';
import type { OnShowContextMenuDetail } from '$lib/components/album-page/album-card';
import { AppRoute } from '$lib/constants';
import { createAlbum, deleteAlbum, type AlbumResponseDto } from '@immich/sdk';
import { get } from 'svelte/store';
@ -118,6 +117,7 @@
import ContextMenu from '$lib/components/shared-components/context-menu/context-menu.svelte';
import AlbumsTable from '$lib/components/album-page/albums-table.svelte';
import { handleError } from '$lib/utils/handle-error';
import type { ContextMenuPosition } from '$lib/utils/context-menu';
export let albums: AlbumResponseDto[];
export let searchAlbum: string;
@ -125,7 +125,7 @@
let shouldShowEditAlbumForm = false;
let selectedAlbum: AlbumResponseDto;
let albumToDelete: AlbumResponseDto | null;
let contextMenuPosition: OnShowContextMenuDetail = { x: 0, y: 0 };
let contextMenuPosition: ContextMenuPosition = { x: 0, y: 0 };
let contextMenuTargetAlbum: AlbumResponseDto | undefined = undefined;
$: {
@ -145,7 +145,7 @@
await removeAlbumsIfEmpty();
});
function showAlbumContextMenu(contextMenuDetail: OnShowContextMenuDetail, album: AlbumResponseDto): void {
function showAlbumContextMenu(contextMenuDetail: ContextMenuPosition, album: AlbumResponseDto): void {
contextMenuTargetAlbum = album;
contextMenuPosition = {
x: contextMenuDetail.x,
@ -228,13 +228,13 @@
{#if albums.length > 0}
<!-- Album Card -->
{#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)}
<a data-sveltekit-preload-data="hover" href="{AppRoute.ALBUMS}/{album.id}" animate:flip={{ duration: 200 }}>
<AlbumCard
preload={index < 20}
{album}
on:showalbumcontextmenu={({ detail }) => showAlbumContextMenu(detail, album)}
onShowContextMenu={(position) => showAlbumContextMenu(position, album)}
/>
</a>
{/each}

View File

@ -1,6 +1,8 @@
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 box = ((currentTarget || target) as HTMLElement)?.getBoundingClientRect();
if (box) {

View File

@ -267,16 +267,10 @@
{#if searchResultAlbums.length > 0}
<section>
<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)}
<a data-sveltekit-preload-data="hover" href={`albums/${album.id}`} animate:flip={{ duration: 200 }}>
<AlbumCard
preload={index < 20}
{album}
isSharingView={false}
showItemCount={false}
showContextMenu={false}
/>
<AlbumCard preload={index < 20} {album} isSharingView={false} showItemCount={false} />
</a>
{/each}
</div>

View File

@ -80,10 +80,10 @@
<div>
<!-- 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)}
<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>
{/each}
</div>