mirror of
https://github.com/immich-app/immich.git
synced 2024-12-26 10:50:29 +02:00
fix(web): minor album card issues (#7975)
* fix(web): minor album card issues * fix album grid gap
This commit is contained in:
parent
0f79c4ff46
commit
cfb14ca80b
@ -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 }));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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>
|
||||
|
@ -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;
|
@ -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}
|
||||
|
@ -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) {
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user