From 288ba44825c994d123d666c323efdb79edfe8a26 Mon Sep 17 00:00:00 2001 From: Alex Tran Date: Thu, 4 Dec 2025 03:26:00 +0000 Subject: [PATCH] refactor: picker field --- i18n/en.json | 3 +- .../workflows/SchemaFormFields.svelte | 247 ++---------------- .../workflows/WorkflowPickerField.svelte | 162 ++++++++++++ .../workflows/[workflowId]/+page.svelte | 10 +- 4 files changed, 195 insertions(+), 227 deletions(-) create mode 100644 web/src/lib/components/workflows/WorkflowPickerField.svelte diff --git a/i18n/en.json b/i18n/en.json index 3d2a7625f4..6d8449dcc7 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1,5 +1,4 @@ { - "get_people_error": "Error getting people", "about": "About", "account": "Account", "account_settings": "Account Settings", @@ -1160,6 +1159,7 @@ "general": "General", "geolocation_instruction_location": "Click on an asset with GPS coordinates to use its location, or select a location directly from the map", "get_help": "Get Help", + "get_people_error": "Error getting people", "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", "getting_started": "Getting Started", "go_back": "Go back", @@ -2256,6 +2256,7 @@ "viewer_stack_use_as_main_asset": "Use as Main Asset", "viewer_unstack": "Un-Stack", "visibility_changed": "Visibility changed for {count, plural, one {# person} other {# people}}", + "visual": "Visual", "visual_builder": "Visual builder", "waiting": "Waiting", "warning": "Warning", diff --git a/web/src/lib/components/workflows/SchemaFormFields.svelte b/web/src/lib/components/workflows/SchemaFormFields.svelte index b88d6bbd97..090683c77f 100644 --- a/web/src/lib/components/workflows/SchemaFormFields.svelte +++ b/web/src/lib/components/workflows/SchemaFormFields.svelte @@ -1,12 +1,7 @@ -{#snippet pickerItemCard( - item: AlbumResponseDto | PersonResponseDto, - isAlbum: boolean, - size: 'large' | 'small', - onRemove: () => void, -)} - {@const sizeClass = size === 'large' ? 'h-16 w-16' : 'h-12 w-12'} - {@const textSizeClass = size === 'large' ? 'font-medium' : 'font-medium text-sm'} - {@const iconSizeClass = size === 'large' ? 'h-5 w-5' : 'h-4 w-4'} - {@const countSizeClass = size === 'large' ? 'text-sm' : 'text-xs'} - -
-
- {#if isAlbum && 'albumThumbnailAssetId' in item} - {#if item.albumThumbnailAssetId} - {item.albumName} - {:else} -
- {/if} - {:else if !isAlbum && 'name' in item} - {item.name} - {/if} -
-
-

- {isAlbum && 'albumName' in item ? item.albumName : 'name' in item ? item.name : ''} -

- {#if isAlbum && 'assetCount' in item} -

- {$t('items_count', { values: { count: item.assetCount } })} -

- {/if} -
- -
-{/snippet} - -{#snippet pickerField( - subType: string, - key: string, - label: string, - component: { required?: boolean; description?: string }, - multiple: boolean, -)} - {@const picker = renderPicker(subType as 'album-picker' | 'people-picker', multiple)} - {@const metadata = pickerMetadata[key]} - {@const isAlbum = subType === 'album-picker'} - - -
- {#if metadata && !Array.isArray(metadata)} - {@render pickerItemCard(metadata, isAlbum, 'large', () => removeSelection(key))} - {:else if metadata && Array.isArray(metadata) && metadata.length > 0} -
- {#each metadata as item (item.id)} - {@render pickerItemCard(item, isAlbum, 'small', () => removeItemFromSelection(key, item.id))} - {/each} -
- {/if} - -
-
-{/snippet} - {#if components}
{#each Object.entries(components) as [key, component] (key)} {@const label = component.title || component.label || key} -
+
{#if component.type === 'select'} - {#if component.subType === 'album-picker' || component.subType === 'people-picker'} - {@render pickerField(component.subType, key, label, component, false)} + {#if isPickerField(component.subType)} + updateConfig(key, value)} + /> {:else} {@const options = component.options?.map((opt) => { return { label: opt.label, value: String(opt.value) }; @@ -322,8 +117,13 @@ {:else if component.type === 'multiselect'} - {#if component.subType === 'album-picker' || component.subType === 'people-picker'} - {@render pickerField(component.subType, key, label, component, true)} + {#if isPickerField(component.subType)} + updateConfig(key, value)} + /> {:else} {@const options = component.options?.map((opt) => { return { label: opt.label, value: String(opt.value) }; @@ -359,8 +159,13 @@ - {:else if component.subType === 'album-picker' || component.subType === 'people-picker'} - {@render pickerField(component.subType, key, label, component, false)} + {:else if isPickerField(component.subType)} + updateConfig(key, value)} + /> {:else} + import AlbumPickerModal from '$lib/modals/AlbumPickerModal.svelte'; + import PeoplePickerModal from '$lib/modals/PeoplePickerModal.svelte'; + import { getAssetThumbnailUrl, getPeopleThumbnailUrl } from '$lib/utils'; + import type { ComponentConfig } from '$lib/utils/workflow'; + import { getAlbumInfo, getPerson, type AlbumResponseDto, type PersonResponseDto } from '@immich/sdk'; + import { Button, Card, CardBody, Field, IconButton, modalManager, Text } from '@immich/ui'; + import { mdiClose, mdiPlus } from '@mdi/js'; + import { t } from 'svelte-i18n'; + + interface Props { + component: ComponentConfig; + configKey: string; + value: string | string[] | undefined; + onchange: (value: string | string[]) => void; + } + + let { component, configKey, value = $bindable(), onchange }: Props = $props(); + + const label = $derived(component.title || component.label || configKey); + const subType = $derived(component.subType as 'album-picker' | 'people-picker'); + const isAlbum = $derived(subType === 'album-picker'); + const multiple = $derived(component.type === 'multiselect' || Array.isArray(value)); + + let pickerMetadata = $state(); + + // Fetch metadata for existing picker values (albums/people) + $effect(() => { + if (!value) { + pickerMetadata = undefined; + return; + } + + void fetchMetadata(); + }); + + const fetchMetadata = async () => { + if (!value || pickerMetadata) { + return; + } + + try { + if (Array.isArray(value) && value.length > 0) { + // Multiple selection + pickerMetadata = await (isAlbum + ? Promise.all(value.map((id) => getAlbumInfo({ id }))) + : Promise.all(value.map((id) => getPerson({ id })))); + } else if (typeof value === 'string' && value) { + // Single selection + pickerMetadata = await (isAlbum ? getAlbumInfo({ id: value }) : getPerson({ id: value })); + } + } catch (error) { + console.error(`Failed to fetch metadata for ${configKey}:`, error); + } + }; + + const handlePicker = async () => { + if (isAlbum) { + const albums = await modalManager.show(AlbumPickerModal, { shared: false }); + if (albums && albums.length > 0) { + const newValue = multiple ? albums.map((a) => a.id) : albums[0].id; + onchange(newValue); + pickerMetadata = multiple ? albums : albums[0]; + } + } else { + const currentIds = (Array.isArray(value) ? value : []) as string[]; + const excludedIds = multiple ? currentIds : []; + const people = await modalManager.show(PeoplePickerModal, { multiple, excludedIds }); + if (people && people.length > 0) { + const newValue = multiple ? people.map((p) => p.id) : people[0].id; + onchange(newValue); + pickerMetadata = multiple ? people : people[0]; + } + } + }; + + const removeSelection = () => { + onchange(multiple ? [] : ''); + pickerMetadata = undefined; + }; + + const removeItemFromSelection = (itemId: string) => { + if (!Array.isArray(value)) { + return; + } + + const newValue = value.filter((id) => id !== itemId); + onchange(newValue); + + if (Array.isArray(pickerMetadata)) { + pickerMetadata = pickerMetadata.filter((item) => item.id !== itemId) as AlbumResponseDto[] | PersonResponseDto[]; + } + }; + + const getButtonText = () => { + if (isAlbum) { + return multiple ? $t('select_albums') : $t('select_album'); + } + return multiple ? $t('select_people') : $t('select_person'); + }; + + +{#snippet pickerItemCard(item: AlbumResponseDto | PersonResponseDto, onRemove: () => void)} + + +
+ {#if isAlbum && 'albumThumbnailAssetId' in item} + {#if item.albumThumbnailAssetId} + {item.albumName} + {:else} +
+ {/if} + {:else if !isAlbum && 'name' in item} + {item.name} + {/if} +
+
+ + {isAlbum && 'albumName' in item ? item.albumName : 'name' in item ? item.name : ''} + + {#if isAlbum && 'assetCount' in item} + + {$t('items_count', { values: { count: item.assetCount } })} + + {/if} +
+ + +
+
+{/snippet} + + +
+ {#if pickerMetadata && !Array.isArray(pickerMetadata)} + {@render pickerItemCard(pickerMetadata, removeSelection)} + {:else if pickerMetadata && Array.isArray(pickerMetadata) && pickerMetadata.length > 0} +
+ {#each pickerMetadata as item (item.id)} + {@render pickerItemCard(item, () => removeItemFromSelection(item.id))} + {/each} +
+ {/if} + +
+
diff --git a/web/src/routes/(user)/utilities/workflows/[workflowId]/+page.svelte b/web/src/routes/(user)/utilities/workflows/[workflowId]/+page.svelte index 2d3d88fccb..03113c1c39 100644 --- a/web/src/routes/(user)/utilities/workflows/[workflowId]/+page.svelte +++ b/web/src/routes/(user)/utilities/workflows/[workflowId]/+page.svelte @@ -440,7 +440,7 @@ isDragging: draggedFilterIndex === index, isDragOver: dragOverFilterIndex === index, }} - class="mb-4 cursor-move rounded-lg border-2 p-4 transition-all bg-light-50 border-dashed border-transparent hover:border-light-300" + class="mb-4 cursor-move rounded-2xl border-2 p-4 transition-all bg-light-50 border-dashed hover:border-light-300" >
{@render cardOrder(index)} @@ -472,7 +472,7 @@ leadingIcon={mdiPlus} onclick={() => handleAddStep('filter')} > - Add more + {$t('add_filter')} {/if} @@ -509,7 +509,7 @@ isDragging: draggedActionIndex === index, isDragOver: dragOverActionIndex === index, }} - class="mb-4 cursor-move rounded-lg border-2 p-4 transition-all bg-light-50 border-dashed border-transparent hover:border-light-300" + class="mb-4 cursor-move rounded-2xl border-2 p-4 transition-all bg-light-50 border-dashed hover:border-light-300" >
{@render cardOrder(index)} @@ -540,7 +540,7 @@ leadingIcon={mdiPlus} onclick={() => handleAddStep('action')} > - Add more + {$t('add_action')} {/if} @@ -567,7 +567,7 @@ leadingIcon={mdiViewDashboard} onclick={() => (viewMode = 'visual')} > - Visual + {$t('visual')}