From 8db666bc384c86de193a908b29a90be6e7a6182b Mon Sep 17 00:00:00 2001
From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com>
Date: Thu, 8 May 2025 22:36:05 +0200
Subject: [PATCH] refactor: search filter modal (#18159)

---
 .../search-bar/search-bar.svelte              | 69 +++++++++----------
 .../SearchFilterModal.svelte}                 | 33 ++++-----
 2 files changed, 49 insertions(+), 53 deletions(-)
 rename web/src/lib/{components/shared-components/search-bar/search-filter-modal.svelte => modals/SearchFilterModal.svelte} (81%)

diff --git a/web/src/lib/components/shared-components/search-bar/search-bar.svelte b/web/src/lib/components/shared-components/search-bar/search-bar.svelte
index e01b0edf1b..29a63fe049 100644
--- a/web/src/lib/components/shared-components/search-bar/search-bar.svelte
+++ b/web/src/lib/components/shared-components/search-bar/search-bar.svelte
@@ -1,19 +1,20 @@
 <script lang="ts">
-  import { AppRoute } from '$lib/constants';
   import { goto } from '$app/navigation';
-  import { searchStore } from '$lib/stores/search.svelte';
-  import { mdiClose, mdiMagnify, mdiTune } from '@mdi/js';
-  import SearchHistoryBox from './search-history-box.svelte';
-  import SearchFilterModal from './search-filter-modal.svelte';
-  import type { MetadataSearchDto, SmartSearchDto } from '@immich/sdk';
-  import { getMetadataSearchQuery } from '$lib/utils/metadata-search';
-  import { handlePromiseError } from '$lib/utils';
-  import { shortcuts } from '$lib/actions/shortcut';
   import { focusOutside } from '$lib/actions/focus-outside';
+  import { shortcuts } from '$lib/actions/shortcut';
   import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
-  import { t } from 'svelte-i18n';
+  import { AppRoute } from '$lib/constants';
+  import { modalManager } from '$lib/managers/modal-manager.svelte';
+  import SearchFilterModal from '$lib/modals/SearchFilterModal.svelte';
+  import { searchStore } from '$lib/stores/search.svelte';
+  import { handlePromiseError } from '$lib/utils';
   import { generateId } from '$lib/utils/generate-id';
+  import { getMetadataSearchQuery } from '$lib/utils/metadata-search';
+  import type { MetadataSearchDto, SmartSearchDto } from '@immich/sdk';
+  import { mdiClose, mdiMagnify, mdiTune } from '@mdi/js';
   import { onDestroy, tick } from 'svelte';
+  import { t } from 'svelte-i18n';
+  import SearchHistoryBox from './search-history-box.svelte';
 
   interface Props {
     value?: string;
@@ -28,10 +29,10 @@
   let input = $state<HTMLInputElement>();
   let searchHistoryBox = $state<ReturnType<typeof SearchHistoryBox>>();
   let showSuggestions = $state(false);
-  let showFilter = $state(false);
   let isSearchSuggestions = $state(false);
   let selectedId: string | undefined = $state();
   let isFocus = $state(false);
+  let close: (() => Promise<void>) | undefined;
 
   const listboxId = generateId();
 
@@ -43,7 +44,6 @@
     const params = getMetadataSearchQuery(payload);
 
     closeDropdown();
-    showFilter = false;
     searchStore.isSearchEnabled = false;
     await goto(`${AppRoute.SEARCH}?${params}`);
   };
@@ -83,13 +83,27 @@
     await handleSearch(searchPayload);
   };
 
-  const onFilterClick = () => {
-    showFilter = !showFilter;
+  const onFilterClick = async () => {
     value = '';
 
-    if (showFilter) {
-      closeDropdown();
+    if (close) {
+      await close();
+      close = undefined;
+      return;
     }
+
+    const result = modalManager.open(SearchFilterModal, { searchQuery });
+    close = result.close;
+    closeDropdown();
+
+    const searchResult = await result.onClose;
+    close = undefined;
+
+    if (!searchResult) {
+      return;
+    }
+
+    await handleSearch(searchResult);
   };
 
   const onSubmit = () => {
@@ -122,7 +136,6 @@
 
   const onEscape = () => {
     closeDropdown();
-    showFilter = false;
   };
 
   const onArrow = async (direction: 1 | -1) => {
@@ -221,9 +234,7 @@
         class="w-full transition-all border-2 px-14 py-4 max-md:py-2 text-immich-fg/75 dark:text-immich-dark-fg
         {grayTheme ? 'dark:bg-immich-dark-gray' : 'dark:bg-immich-dark-bg'}
         {showSuggestions && isSearchSuggestions ? 'rounded-t-3xl' : 'rounded-3xl bg-gray-200'}
-        {searchStore.isSearchEnabled && !showFilter
-          ? 'border-gray-200 dark:border-gray-700 bg-white'
-          : 'border-transparent'}"
+        {searchStore.isSearchEnabled ? 'border-gray-200 dark:border-gray-700 bg-white' : 'border-transparent'}"
         placeholder={$t('search_your_photos')}
         required
         pattern="^(?!m:$).*$"
@@ -231,7 +242,6 @@
         bind:this={input}
         onfocus={openDropdown}
         oninput={onInput}
-        disabled={showFilter}
         role="combobox"
         aria-controls={listboxId}
         aria-activedescendant={selectedId ?? ''}
@@ -285,22 +295,7 @@
       </div>
     {/if}
     <div class="absolute inset-y-0 start-0 flex items-center ps-2">
-      <CircleIconButton
-        type="submit"
-        disabled={showFilter}
-        title={$t('search')}
-        icon={mdiMagnify}
-        size="20"
-        onclick={() => {}}
-      />
+      <CircleIconButton type="submit" title={$t('search')} icon={mdiMagnify} size="20" onclick={() => {}} />
     </div>
   </form>
-
-  {#if showFilter}
-    <SearchFilterModal
-      {searchQuery}
-      onSearch={(payload) => handleSearch(payload)}
-      onClose={() => (showFilter = false)}
-    />
-  {/if}
 </div>
diff --git a/web/src/lib/components/shared-components/search-bar/search-filter-modal.svelte b/web/src/lib/modals/SearchFilterModal.svelte
similarity index 81%
rename from web/src/lib/components/shared-components/search-bar/search-filter-modal.svelte
rename to web/src/lib/modals/SearchFilterModal.svelte
index d5c9b02a2d..1b2f32731e 100644
--- a/web/src/lib/components/shared-components/search-bar/search-filter-modal.svelte
+++ b/web/src/lib/modals/SearchFilterModal.svelte
@@ -1,8 +1,8 @@
 <script lang="ts" module>
   import { MediaType, QueryType, validQueryTypes } from '$lib/constants';
-  import type { SearchDateFilter } from './search-date-section.svelte';
-  import type { SearchDisplayFilters } from './search-display-section.svelte';
-  import type { SearchLocationFilter } from './search-location-section.svelte';
+  import type { SearchDateFilter } from '../components/shared-components/search-bar/search-date-section.svelte';
+  import type { SearchDisplayFilters } from '../components/shared-components/search-bar/search-display-section.svelte';
+  import type { SearchLocationFilter } from '../components/shared-components/search-bar/search-location-section.svelte';
 
   export type SearchFilter = {
     query: string;
@@ -20,6 +20,17 @@
 
 <script lang="ts">
   import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
+  import SearchCameraSection, {
+    type SearchCameraFilter,
+  } from '$lib/components/shared-components/search-bar/search-camera-section.svelte';
+  import SearchDateSection from '$lib/components/shared-components/search-bar/search-date-section.svelte';
+  import SearchDisplaySection from '$lib/components/shared-components/search-bar/search-display-section.svelte';
+  import SearchLocationSection from '$lib/components/shared-components/search-bar/search-location-section.svelte';
+  import SearchMediaSection from '$lib/components/shared-components/search-bar/search-media-section.svelte';
+  import SearchPeopleSection from '$lib/components/shared-components/search-bar/search-people-section.svelte';
+  import SearchRatingsSection from '$lib/components/shared-components/search-bar/search-ratings-section.svelte';
+  import SearchTagsSection from '$lib/components/shared-components/search-bar/search-tags-section.svelte';
+  import SearchTextSection from '$lib/components/shared-components/search-bar/search-text-section.svelte';
   import { preferences } from '$lib/stores/user.store';
   import { parseUtcDate } from '$lib/utils/date-time';
   import { generateId } from '$lib/utils/generate-id';
@@ -28,23 +39,13 @@
   import { mdiTune } from '@mdi/js';
   import { t } from 'svelte-i18n';
   import { SvelteSet } from 'svelte/reactivity';
-  import SearchCameraSection, { type SearchCameraFilter } from './search-camera-section.svelte';
-  import SearchDateSection from './search-date-section.svelte';
-  import SearchDisplaySection from './search-display-section.svelte';
-  import SearchLocationSection from './search-location-section.svelte';
-  import SearchMediaSection from './search-media-section.svelte';
-  import SearchPeopleSection from './search-people-section.svelte';
-  import SearchRatingsSection from './search-ratings-section.svelte';
-  import SearchTagsSection from './search-tags-section.svelte';
-  import SearchTextSection from './search-text-section.svelte';
 
   interface Props {
     searchQuery: MetadataSearchDto | SmartSearchDto;
-    onClose: () => void;
-    onSearch: (search: SmartSearchDto | MetadataSearchDto) => void;
+    onClose: (search?: SmartSearchDto | MetadataSearchDto) => void;
   }
 
-  let { searchQuery, onClose, onSearch }: Props = $props();
+  let { searchQuery, onClose }: Props = $props();
 
   const parseOptionalDate = (dateString?: string) => (dateString ? parseUtcDate(dateString) : undefined);
   const toStartOfDayDate = (dateString: string) => parseUtcDate(dateString)?.startOf('day').toISODate() || undefined;
@@ -141,7 +142,7 @@
       rating: filter.rating,
     };
 
-    onSearch(payload);
+    onClose(payload);
   };
 
   const onreset = (event: Event) => {