mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-04 03:27:09 -05:00 
			
		
		
		
	refactor(web): Allow dropdown for more general use (#4515)
This commit is contained in:
		
							parent
							
								
									4b59f83288
								
							
						
					
					
						commit
						5a7ef02387
					
				@ -1,17 +1,31 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
  import Check from 'svelte-material-icons/Check.svelte';
 | 
			
		||||
<script lang="ts" context="module">
 | 
			
		||||
  // 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 { clickOutside } from '$lib/utils/click-outside';
 | 
			
		||||
  import { fly } from 'svelte/transition';
 | 
			
		||||
  import type Icon from 'svelte-material-icons/DotsVertical.svelte';
 | 
			
		||||
  import Check from 'svelte-material-icons/Check.svelte';
 | 
			
		||||
  import { createEventDispatcher } from 'svelte';
 | 
			
		||||
 | 
			
		||||
  const dispatch = createEventDispatcher<{
 | 
			
		||||
    select: string;
 | 
			
		||||
    select: T;
 | 
			
		||||
  }>();
 | 
			
		||||
  export let options: string[];
 | 
			
		||||
  export let value = options[0];
 | 
			
		||||
  export let icons: (typeof Icon)[] | undefined = undefined;
 | 
			
		||||
 | 
			
		||||
  export let options: T[];
 | 
			
		||||
  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;
 | 
			
		||||
 | 
			
		||||
@ -19,28 +33,37 @@
 | 
			
		||||
    showMenu = false;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleSelectOption = (index: number) => {
 | 
			
		||||
    if (options[index] === value) {
 | 
			
		||||
      dispatch('select', value);
 | 
			
		||||
    } else {
 | 
			
		||||
      value = options[index];
 | 
			
		||||
    }
 | 
			
		||||
  const handleSelectOption = (option: T) => {
 | 
			
		||||
    dispatch('select', option);
 | 
			
		||||
    selectedOption = option;
 | 
			
		||||
 | 
			
		||||
    showMenu = false;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  $: index = options.findIndex((option) => option === value);
 | 
			
		||||
  $: icon = icons?.[index];
 | 
			
		||||
  const renderOption = (option: T): RenderedOption => {
 | 
			
		||||
    const renderedOption = render(option);
 | 
			
		||||
    switch (typeof renderedOption) {
 | 
			
		||||
      case 'string':
 | 
			
		||||
        return { title: renderedOption };
 | 
			
		||||
      default:
 | 
			
		||||
        return {
 | 
			
		||||
          title: renderedOption.title,
 | 
			
		||||
          icon: renderedOption.icon,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  $: renderedSelectedOption = renderOption(selectedOption);
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div id="dropdown-button" use:clickOutside on:outclick={handleClickOutside} on:escape={handleClickOutside}>
 | 
			
		||||
  <!-- BUTTON TITLE -->
 | 
			
		||||
  <LinkButton on:click={() => (showMenu = true)}>
 | 
			
		||||
    <div class="flex place-items-center gap-2 text-sm">
 | 
			
		||||
      {#if icon}
 | 
			
		||||
        <svelte:component this={icon} size="18" />
 | 
			
		||||
      {#if renderedSelectedOption?.icon}
 | 
			
		||||
        <svelte:component this={renderedSelectedOption.icon} size="18" />
 | 
			
		||||
      {/if}
 | 
			
		||||
      <p class="hidden sm:block">{value}</p>
 | 
			
		||||
      <p class="hidden sm:block">{renderedSelectedOption.title}</p>
 | 
			
		||||
    </div>
 | 
			
		||||
  </LinkButton>
 | 
			
		||||
 | 
			
		||||
@ -50,22 +73,23 @@
 | 
			
		||||
      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"
 | 
			
		||||
    >
 | 
			
		||||
      {#each options as option, index (option)}
 | 
			
		||||
      {#each options as option (option)}
 | 
			
		||||
        {@const renderedOption = renderOption(option)}
 | 
			
		||||
        <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"
 | 
			
		||||
          on:click={() => handleSelectOption(index)}
 | 
			
		||||
          on:click={() => handleSelectOption(option)}
 | 
			
		||||
        >
 | 
			
		||||
          {#if value == option}
 | 
			
		||||
            <div class="font-medium text-immich-primary dark:text-immich-dark-primary">
 | 
			
		||||
          {#if _.isEqual(selectedOption, option)}
 | 
			
		||||
            <div class="text-immich-primary dark:text-immich-dark-primary">
 | 
			
		||||
              <Check size="18" />
 | 
			
		||||
            </div>
 | 
			
		||||
            <p class="justify-self-start font-medium text-immich-primary dark:text-immich-dark-primary">
 | 
			
		||||
              {option}
 | 
			
		||||
            <p class="justify-self-start text-immich-primary dark:text-immich-dark-primary">
 | 
			
		||||
              {renderedOption.title}
 | 
			
		||||
            </p>
 | 
			
		||||
          {:else}
 | 
			
		||||
            <div />
 | 
			
		||||
            <p class="justify-self-start">
 | 
			
		||||
              {option}
 | 
			
		||||
              {renderedOption.title}
 | 
			
		||||
            </p>
 | 
			
		||||
          {/if}
 | 
			
		||||
        </button>
 | 
			
		||||
 | 
			
		||||
@ -216,15 +216,20 @@
 | 
			
		||||
    </LinkButton>
 | 
			
		||||
 | 
			
		||||
    <Dropdown
 | 
			
		||||
      options={Object.values(sortByOptions).map((CourseInfo) => CourseInfo.sortTitle)}
 | 
			
		||||
      bind:value={$albumViewSettings.sortBy}
 | 
			
		||||
      icons={Object.keys(sortByOptions).map((key) => (sortByOptions[key].sortDesc ? ArrowDownThin : ArrowUpThin))}
 | 
			
		||||
      options={Object.values(sortByOptions)}
 | 
			
		||||
      render={(option) => {
 | 
			
		||||
        return {
 | 
			
		||||
          title: option.sortTitle,
 | 
			
		||||
          icon: option.sortDesc ? ArrowDownThin : ArrowUpThin,
 | 
			
		||||
        };
 | 
			
		||||
      }}
 | 
			
		||||
      on:select={(event) => {
 | 
			
		||||
        for (const key in sortByOptions) {
 | 
			
		||||
          if (sortByOptions[key].sortTitle === event.detail) {
 | 
			
		||||
          if (sortByOptions[key].sortTitle === event.detail.sortTitle) {
 | 
			
		||||
            sortByOptions[key].sortDesc = !sortByOptions[key].sortDesc;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        $albumViewSettings.sortBy = event.detail.sortTitle;
 | 
			
		||||
      }}
 | 
			
		||||
    />
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user