mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	refactor(web): Allow dropdown for more general use (#4515)
This commit is contained in:
		| @@ -1,17 +1,31 @@ | |||||||
| <script lang="ts"> | <script lang="ts" context="module"> | ||||||
|   import Check from 'svelte-material-icons/Check.svelte'; |   // Necessary for eslint | ||||||
|  |   /* eslint-disable @typescript-eslint/no-explicit-any */ | ||||||
|  |   type T = any; | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <script lang="ts" generics="T"> | ||||||
|  |   import _ from 'lodash'; | ||||||
|   import LinkButton from './buttons/link-button.svelte'; |   import LinkButton from './buttons/link-button.svelte'; | ||||||
|   import { clickOutside } from '$lib/utils/click-outside'; |   import { clickOutside } from '$lib/utils/click-outside'; | ||||||
|   import { fly } from 'svelte/transition'; |   import { fly } from 'svelte/transition'; | ||||||
|   import type Icon from 'svelte-material-icons/DotsVertical.svelte'; |   import type Icon from 'svelte-material-icons/DotsVertical.svelte'; | ||||||
|  |   import Check from 'svelte-material-icons/Check.svelte'; | ||||||
|   import { createEventDispatcher } from 'svelte'; |   import { createEventDispatcher } from 'svelte'; | ||||||
|  |  | ||||||
|   const dispatch = createEventDispatcher<{ |   const dispatch = createEventDispatcher<{ | ||||||
|     select: string; |     select: T; | ||||||
|   }>(); |   }>(); | ||||||
|   export let options: string[]; |  | ||||||
|   export let value = options[0]; |   export let options: T[]; | ||||||
|   export let icons: (typeof Icon)[] | undefined = undefined; |   export let selectedOption = options[0]; | ||||||
|  |  | ||||||
|  |   export let render: (item: T) => string | RenderedOption = (item) => String(item); | ||||||
|  |  | ||||||
|  |   type RenderedOption = { | ||||||
|  |     title: string; | ||||||
|  |     icon?: typeof Icon; | ||||||
|  |   }; | ||||||
|  |  | ||||||
|   let showMenu = false; |   let showMenu = false; | ||||||
|  |  | ||||||
| @@ -19,28 +33,37 @@ | |||||||
|     showMenu = false; |     showMenu = false; | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   const handleSelectOption = (index: number) => { |   const handleSelectOption = (option: T) => { | ||||||
|     if (options[index] === value) { |     dispatch('select', option); | ||||||
|       dispatch('select', value); |     selectedOption = option; | ||||||
|     } else { |  | ||||||
|       value = options[index]; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     showMenu = false; |     showMenu = false; | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   $: index = options.findIndex((option) => option === value); |   const renderOption = (option: T): RenderedOption => { | ||||||
|   $: icon = icons?.[index]; |     const renderedOption = render(option); | ||||||
|  |     switch (typeof renderedOption) { | ||||||
|  |       case 'string': | ||||||
|  |         return { title: renderedOption }; | ||||||
|  |       default: | ||||||
|  |         return { | ||||||
|  |           title: renderedOption.title, | ||||||
|  |           icon: renderedOption.icon, | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   $: renderedSelectedOption = renderOption(selectedOption); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <div id="dropdown-button" use:clickOutside on:outclick={handleClickOutside} on:escape={handleClickOutside}> | <div id="dropdown-button" use:clickOutside on:outclick={handleClickOutside} on:escape={handleClickOutside}> | ||||||
|   <!-- BUTTON TITLE --> |   <!-- BUTTON TITLE --> | ||||||
|   <LinkButton on:click={() => (showMenu = true)}> |   <LinkButton on:click={() => (showMenu = true)}> | ||||||
|     <div class="flex place-items-center gap-2 text-sm"> |     <div class="flex place-items-center gap-2 text-sm"> | ||||||
|       {#if icon} |       {#if renderedSelectedOption?.icon} | ||||||
|         <svelte:component this={icon} size="18" /> |         <svelte:component this={renderedSelectedOption.icon} size="18" /> | ||||||
|       {/if} |       {/if} | ||||||
|       <p class="hidden sm:block">{value}</p> |       <p class="hidden sm:block">{renderedSelectedOption.title}</p> | ||||||
|     </div> |     </div> | ||||||
|   </LinkButton> |   </LinkButton> | ||||||
|  |  | ||||||
| @@ -50,22 +73,23 @@ | |||||||
|       transition:fly={{ y: -30, x: 30, duration: 200 }} |       transition:fly={{ y: -30, x: 30, duration: 200 }} | ||||||
|       class="text-md absolute right-0 top-5 z-50 flex min-w-[250px] flex-col rounded-2xl bg-gray-100 py-4 text-black shadow-lg dark:bg-gray-700 dark:text-white" |       class="text-md absolute right-0 top-5 z-50 flex min-w-[250px] flex-col rounded-2xl bg-gray-100 py-4 text-black shadow-lg dark:bg-gray-700 dark:text-white" | ||||||
|     > |     > | ||||||
|       {#each options as option, index (option)} |       {#each options as option (option)} | ||||||
|  |         {@const renderedOption = renderOption(option)} | ||||||
|         <button |         <button | ||||||
|           class="grid grid-cols-[20px,1fr] place-items-center gap-2 p-4 transition-all hover:bg-gray-300 dark:hover:bg-gray-800" |           class="grid grid-cols-[20px,1fr] place-items-center gap-2 p-4 transition-all hover:bg-gray-300 dark:hover:bg-gray-800" | ||||||
|           on:click={() => handleSelectOption(index)} |           on:click={() => handleSelectOption(option)} | ||||||
|         > |         > | ||||||
|           {#if value == option} |           {#if _.isEqual(selectedOption, option)} | ||||||
|             <div class="font-medium text-immich-primary dark:text-immich-dark-primary"> |             <div class="text-immich-primary dark:text-immich-dark-primary"> | ||||||
|               <Check size="18" /> |               <Check size="18" /> | ||||||
|             </div> |             </div> | ||||||
|             <p class="justify-self-start font-medium text-immich-primary dark:text-immich-dark-primary"> |             <p class="justify-self-start text-immich-primary dark:text-immich-dark-primary"> | ||||||
|               {option} |               {renderedOption.title} | ||||||
|             </p> |             </p> | ||||||
|           {:else} |           {:else} | ||||||
|             <div /> |             <div /> | ||||||
|             <p class="justify-self-start"> |             <p class="justify-self-start"> | ||||||
|               {option} |               {renderedOption.title} | ||||||
|             </p> |             </p> | ||||||
|           {/if} |           {/if} | ||||||
|         </button> |         </button> | ||||||
|   | |||||||
| @@ -216,15 +216,20 @@ | |||||||
|     </LinkButton> |     </LinkButton> | ||||||
|  |  | ||||||
|     <Dropdown |     <Dropdown | ||||||
|       options={Object.values(sortByOptions).map((CourseInfo) => CourseInfo.sortTitle)} |       options={Object.values(sortByOptions)} | ||||||
|       bind:value={$albumViewSettings.sortBy} |       render={(option) => { | ||||||
|       icons={Object.keys(sortByOptions).map((key) => (sortByOptions[key].sortDesc ? ArrowDownThin : ArrowUpThin))} |         return { | ||||||
|  |           title: option.sortTitle, | ||||||
|  |           icon: option.sortDesc ? ArrowDownThin : ArrowUpThin, | ||||||
|  |         }; | ||||||
|  |       }} | ||||||
|       on:select={(event) => { |       on:select={(event) => { | ||||||
|         for (const key in sortByOptions) { |         for (const key in sortByOptions) { | ||||||
|           if (sortByOptions[key].sortTitle === event.detail) { |           if (sortByOptions[key].sortTitle === event.detail.sortTitle) { | ||||||
|             sortByOptions[key].sortDesc = !sortByOptions[key].sortDesc; |             sortByOptions[key].sortDesc = !sortByOptions[key].sortDesc; | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
|  |         $albumViewSettings.sortBy = event.detail.sortTitle; | ||||||
|       }} |       }} | ||||||
|     /> |     /> | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user