diff --git a/web/src/lib/components/shared-components/search-bar/SearchFilters.svelte b/web/src/lib/components/shared-components/search-bar/SearchFilters.svelte index d475eeb21..cba45cec4 100644 --- a/web/src/lib/components/shared-components/search-bar/SearchFilters.svelte +++ b/web/src/lib/components/shared-components/search-bar/SearchFilters.svelte @@ -161,8 +161,10 @@ }); $effect(() => { - if (tags) { - tagsTitle = getSearchTagsTitle(tags, searchManager.filter.tagIds!); + if (searchManager.filter.tagIds === null) { + tagsTitle = $t('untagged'); + } else if (tags) { + tagsTitle = getSearchTagsTitle(tags, searchManager.filter.tagIds); } }); diff --git a/web/src/lib/components/shared-components/search-bar/SearchTagsSection.spec.ts b/web/src/lib/components/shared-components/search-bar/SearchTagsSection.spec.ts index e2cd45703..72c6e43cb 100644 --- a/web/src/lib/components/shared-components/search-bar/SearchTagsSection.spec.ts +++ b/web/src/lib/components/shared-components/search-bar/SearchTagsSection.spec.ts @@ -57,4 +57,27 @@ describe('SearchTagsSection component', () => { expect(document.activeElement).not.toBe(document.body); expect(container.contains(document.activeElement)).toBe(true); }); + + it('sets tagIds to null when untagged is toggled on', async () => { + const user = userEvent.setup(); + render(SearchTagsSection, { + props: { title: undefined, parentPromise: Promise.resolve([tag('tag-1', 'holiday')]) }, + }); + + await user.click(screen.getByRole('button', { name: 'untagged' })); + + expect(searchManager.filter.tagIds).toBeNull(); + }); + + it('restores an empty tag selection when untagged is toggled off', async () => { + const user = userEvent.setup(); + searchManager.setQuery({ tagIds: null }); + render(SearchTagsSection, { + props: { title: undefined, parentPromise: Promise.resolve([tag('tag-1', 'holiday')]) }, + }); + + await user.click(screen.getByRole('button', { name: 'untagged' })); + + expect([...(searchManager.filter.tagIds ?? [])]).toEqual([]); + }); }); diff --git a/web/src/lib/components/shared-components/search-bar/SearchTagsSection.svelte b/web/src/lib/components/shared-components/search-bar/SearchTagsSection.svelte index d2512554b..d8bd48439 100644 --- a/web/src/lib/components/shared-components/search-bar/SearchTagsSection.svelte +++ b/web/src/lib/components/shared-components/search-bar/SearchTagsSection.svelte @@ -5,7 +5,9 @@ import { Button, Text } from '@immich/ui'; import { onMount } from 'svelte'; import { t } from 'svelte-i18n'; + import { SvelteSet } from 'svelte/reactivity'; import { mdiClose } from '@mdi/js'; + import SearchButton from './SearchButton.svelte'; import { getSearchTagsTitle } from './search-bar-utils'; import { searchManager } from '$lib/managers/search-manager.svelte'; @@ -51,6 +53,13 @@ selectedTags.delete(tag); title = getSearchTagsTitle(allTags, selectedTags); }; + + const handleToggleUntagged = () => { + const isSearchUntagged = selectedTags === null; + + searchManager.filter.tagIds = isSearchUntagged ? new SvelteSet() : null; + title = isSearchUntagged ? undefined : $t('untagged'); + }; {#if authManager.authenticated && authManager.preferences.tags.enabled} @@ -87,5 +96,10 @@ {/each} {/if} + +
+ {$t('untagged')} +
{/if}