From 0bae88bef6b8b076db99bca0d7a629a6901d760a Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Wed, 21 Jan 2026 10:40:09 -0500 Subject: [PATCH] refactor(web): person service actions (#25402) * refactor(web): person service actions * fix: timeline e2e tests --- .../asset-viewer.parallel-e2e-spec.ts | 3 +- .../timeline/timeline.parallel-e2e-spec.ts | 2 - e2e/src/web/specs/timeline/utils.ts | 14 --- web/src/lib/services/person.service.ts | 86 ++++++++++++++++- .../[[assetId=id]]/+page.svelte | 95 ++++++------------- 5 files changed, 116 insertions(+), 84 deletions(-) diff --git a/e2e/src/web/specs/asset-viewer/asset-viewer.parallel-e2e-spec.ts b/e2e/src/web/specs/asset-viewer/asset-viewer.parallel-e2e-spec.ts index 3d65b20c87..669f1b815c 100644 --- a/e2e/src/web/specs/asset-viewer/asset-viewer.parallel-e2e-spec.ts +++ b/e2e/src/web/specs/asset-viewer/asset-viewer.parallel-e2e-spec.ts @@ -12,7 +12,7 @@ import { import { setupBaseMockApiRoutes } from 'src/mock-network/base-network'; import { setupTimelineMockApiRoutes, TimelineTestContext } from 'src/mock-network/timeline-network'; import { utils } from 'src/utils'; -import { assetViewerUtils, cancelAllPollers } from 'src/web/specs/timeline/utils'; +import { assetViewerUtils } from 'src/web/specs/timeline/utils'; test.describe.configure({ mode: 'parallel' }); test.describe('asset-viewer', () => { @@ -49,7 +49,6 @@ test.describe('asset-viewer', () => { }); test.afterEach(() => { - cancelAllPollers(); testContext.slowBucket = false; changes.albumAdditions = []; changes.assetDeletions = []; diff --git a/e2e/src/web/specs/timeline/timeline.parallel-e2e-spec.ts b/e2e/src/web/specs/timeline/timeline.parallel-e2e-spec.ts index 5faf8380d1..47026e2cd4 100644 --- a/e2e/src/web/specs/timeline/timeline.parallel-e2e-spec.ts +++ b/e2e/src/web/specs/timeline/timeline.parallel-e2e-spec.ts @@ -18,7 +18,6 @@ import { pageRoutePromise, setupTimelineMockApiRoutes, TimelineTestContext } fro import { utils } from 'src/utils'; import { assetViewerUtils, - cancelAllPollers, padYearMonth, pageUtils, poll, @@ -64,7 +63,6 @@ test.describe('Timeline', () => { }); test.afterEach(() => { - cancelAllPollers(); testContext.slowBucket = false; changes.albumAdditions = []; changes.assetDeletions = []; diff --git a/e2e/src/web/specs/timeline/utils.ts b/e2e/src/web/specs/timeline/utils.ts index 397a1656e8..0f04bf9361 100644 --- a/e2e/src/web/specs/timeline/utils.ts +++ b/e2e/src/web/specs/timeline/utils.ts @@ -23,13 +23,6 @@ export async function throttlePage(context: BrowserContext, page: Page) { await session.send('Emulation.setCPUThrottlingRate', { rate: 10 }); } -let activePollsAbortController = new AbortController(); - -export const cancelAllPollers = () => { - activePollsAbortController.abort(); - activePollsAbortController = new AbortController(); -}; - export const poll = async ( page: Page, query: () => Promise, @@ -37,21 +30,14 @@ export const poll = async ( ) => { let result; const timeout = Date.now() + 10_000; - const signal = activePollsAbortController.signal; const terminate = callback || ((result: Awaited | undefined) => !!result); while (!terminate(result) && Date.now() < timeout) { - if (signal.aborted) { - return; - } try { result = await query(); } catch { // ignore } - if (signal.aborted) { - return; - } if (page.isClosed()) { return; } diff --git a/web/src/lib/services/person.service.ts b/web/src/lib/services/person.service.ts index f6b8289f13..b40028258a 100644 --- a/web/src/lib/services/person.service.ts +++ b/web/src/lib/services/person.service.ts @@ -4,7 +4,13 @@ import { handleError } from '$lib/utils/handle-error'; import { getFormatter } from '$lib/utils/i18n'; import { updatePerson, type PersonResponseDto } from '@immich/sdk'; import { modalManager, toastManager, type ActionItem } from '@immich/ui'; -import { mdiCalendarEditOutline } from '@mdi/js'; +import { + mdiCalendarEditOutline, + mdiEyeOffOutline, + mdiEyeOutline, + mdiHeartMinusOutline, + mdiHeartOutline, +} from '@mdi/js'; import type { MessageFormatter } from 'svelte-i18n'; export const getPersonActions = ($t: MessageFormatter, person: PersonResponseDto) => { @@ -14,7 +20,83 @@ export const getPersonActions = ($t: MessageFormatter, person: PersonResponseDto onAction: () => modalManager.show(PersonEditBirthDateModal, { person }), }; - return { SetDateOfBirth }; + const Favorite: ActionItem = { + title: $t('to_favorite'), + icon: mdiHeartOutline, + $if: () => !person.isFavorite, + onAction: () => handleFavoritePerson(person), + }; + + const Unfavorite: ActionItem = { + title: $t('unfavorite'), + icon: mdiHeartMinusOutline, + $if: () => !!person.isFavorite, + onAction: () => handleUnfavoritePerson(person), + }; + + const HidePerson: ActionItem = { + title: $t('hide_person'), + icon: mdiEyeOffOutline, + $if: () => !person.isHidden, + onAction: () => handleHidePerson(person), + }; + + const ShowPerson: ActionItem = { + title: $t('unhide_person'), + icon: mdiEyeOutline, + $if: () => !!person.isHidden, + onAction: () => handleShowPerson(person), + }; + + return { SetDateOfBirth, Favorite, Unfavorite, HidePerson, ShowPerson }; +}; + +const handleFavoritePerson = async (person: { id: string }) => { + const $t = await getFormatter(); + + try { + const response = await updatePerson({ id: person.id, personUpdateDto: { isFavorite: true } }); + eventManager.emit('PersonUpdate', response); + toastManager.success($t('added_to_favorites')); + } catch (error) { + handleError(error, $t('errors.unable_to_add_remove_favorites', { values: { favorite: false } })); + } +}; + +const handleUnfavoritePerson = async (person: { id: string }) => { + const $t = await getFormatter(); + + try { + const response = await updatePerson({ id: person.id, personUpdateDto: { isFavorite: false } }); + eventManager.emit('PersonUpdate', response); + toastManager.success($t('removed_from_favorites')); + } catch (error) { + handleError(error, $t('errors.unable_to_add_remove_favorites', { values: { favorite: false } })); + } +}; + +const handleHidePerson = async (person: { id: string }) => { + const $t = await getFormatter(); + + try { + const response = await updatePerson({ id: person.id, personUpdateDto: { isHidden: true } }); + toastManager.success($t('changed_visibility_successfully')); + eventManager.emit('PersonUpdate', response); + } catch (error) { + handleError(error, $t('errors.unable_to_hide_person')); + } +}; + +const handleShowPerson = async (person: { id: string }) => { + const $t = await getFormatter(); + + try { + const response = await updatePerson({ id: person.id, personUpdateDto: { isHidden: false } }); + toastManager.success($t('changed_visibility_successfully')); + eventManager.emit('PersonUpdate', response); + } catch (error) { + handleError(error, $t('errors.something_went_wrong')); + } }; export const handleUpdatePersonBirthDate = async (person: PersonResponseDto, birthDate: string) => { diff --git a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 0d6c7bd914..ed005b1f9c 100644 --- a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -4,7 +4,6 @@ import { clickOutside } from '$lib/actions/click-outside'; import { listNavigation } from '$lib/actions/list-navigation'; import { scrollMemoryClearer } from '$lib/actions/scroll-memory'; - import ActionMenuItem from '$lib/components/ActionMenuItem.svelte'; import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte'; import EditNameInput from '$lib/components/faces-page/edit-name-input.svelte'; import MergeFaceSelector from '$lib/components/faces-page/merge-face-selector.svelte'; @@ -42,16 +41,12 @@ import { handleError } from '$lib/utils/handle-error'; import { isExternalUrl } from '$lib/utils/navigation'; import { AssetVisibility, searchPerson, updatePerson, type PersonResponseDto } from '@immich/sdk'; - import { LoadingSpinner, modalManager, toastManager } from '@immich/ui'; + import { ContextMenuButton, LoadingSpinner, modalManager, toastManager, type ActionItem } from '@immich/ui'; import { mdiAccountBoxOutline, mdiAccountMultipleCheckOutline, mdiArrowLeft, mdiDotsVertical, - mdiEyeOffOutline, - mdiEyeOutline, - mdiHeartMinusOutline, - mdiHeartOutline, mdiPlus, } from '@mdi/js'; import { DateTime } from 'luxon'; @@ -144,37 +139,6 @@ viewMode = PersonPageViewMode.UNASSIGN_ASSETS; }; - const toggleHidePerson = async () => { - try { - await updatePerson({ - id: person.id, - personUpdateDto: { isHidden: !person.isHidden }, - }); - - toastManager.success($t('changed_visibility_successfully')); - - await goto(previousRoute); - } catch (error) { - handleError(error, $t('errors.unable_to_hide_person')); - } - }; - - const handleToggleFavorite = async () => { - try { - const updatedPerson = await updatePerson({ - id: person.id, - personUpdateDto: { isFavorite: !person.isFavorite }, - }); - - // Invalidate to reload the page data and have the favorite status updated - await invalidateAll(); - - toastManager.success(updatedPerson.isFavorite ? $t('added_to_favorites') : $t('removed_from_favorites')); - } catch (error) { - handleError(error, $t('errors.unable_to_add_remove_favorites', { values: { favorite: person.isFavorite } })); - } - }; - const handleMerge = async (person: PersonResponseDto) => { await updateAssetCount(); await handleGoBack(); @@ -325,13 +289,35 @@ assetInteraction.clearMultiselect(); }; - const onPersonUpdate = (response: PersonResponseDto) => { - if (person.id === response.id) { - return (person = response); + const onPersonUpdate = async (response: PersonResponseDto) => { + if (response.id !== person.id) { + return; } + + if (response.isHidden) { + await goto(previousRoute); + return; + } + + person = response; }; - const { SetDateOfBirth } = $derived(getPersonActions($t, person)); + const { SetDateOfBirth, Favorite, Unfavorite, HidePerson, ShowPerson } = $derived(getPersonActions($t, person)); + const SelectFeaturePhoto: ActionItem = { + title: $t('select_featured_photo'), + icon: mdiAccountBoxOutline, + onAction: () => { + viewMode = PersonPageViewMode.SELECT_PERSON; + }, + }; + + const Merge: ActionItem = { + title: $t('merge_people'), + icon: mdiAccountMultipleCheckOutline, + onAction: () => { + viewMode = PersonPageViewMode.MERGE_PEOPLE; + }, + }; @@ -507,29 +493,10 @@ {#if viewMode === PersonPageViewMode.VIEW_ASSETS} goto(previousRoute)}> {#snippet trailing()} - - (viewMode = PersonPageViewMode.SELECT_PERSON)} - /> - toggleHidePerson()} - /> - - (viewMode = PersonPageViewMode.MERGE_PEOPLE)} - /> - - + {/snippet} {/if}