Skip to content

Commit 5846d55

Browse files
committed
refactor: picker field
1 parent bd4355a commit 5846d55

File tree

2 files changed

+190
-220
lines changed

2 files changed

+190
-220
lines changed

web/src/lib/components/workflows/SchemaFormFields.svelte

Lines changed: 25 additions & 220 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
<script lang="ts">
2-
import AlbumPickerModal from '$lib/modals/AlbumPickerModal.svelte';
3-
import PeoplePickerModal from '$lib/modals/PeoplePickerModal.svelte';
4-
import { getAssetThumbnailUrl, getPeopleThumbnailUrl } from '$lib/utils';
5-
import { formatLabel, getComponentFromSchema, type ComponentConfig } from '$lib/utils/workflow';
6-
import { getAlbumInfo, getPerson, type AlbumResponseDto, type PersonResponseDto } from '@immich/sdk';
7-
import { Button, Field, Input, MultiSelect, Select, Switch, Text, modalManager, type SelectItem } from '@immich/ui';
8-
import { mdiPlus } from '@mdi/js';
9-
import { t } from 'svelte-i18n';
2+
import { formatLabel, getComponentFromSchema } from '$lib/utils/workflow';
3+
import { Field, Input, MultiSelect, Select, Switch, Text, type SelectItem } from '@immich/ui';
4+
import WorkflowPickerField from './WorkflowPickerField.svelte';
105
116
interface Props {
127
schema: object | null;
@@ -33,18 +28,6 @@
3328
let selectValue = $state<SelectItem>();
3429
let switchValue = $state<boolean>(false);
3530
let multiSelectValue = $state<SelectItem[]>([]);
36-
let pickerMetadata = $state<
37-
Record<string, AlbumResponseDto | PersonResponseDto | AlbumResponseDto[] | PersonResponseDto[]>
38-
>({});
39-
40-
// Fetch metadata for existing picker values (albums/people)
41-
$effect(() => {
42-
if (!components) {
43-
return;
44-
}
45-
46-
void fetchMetadata(components);
47-
});
4831
4932
$effect(() => {
5033
// Initialize config for actions/filters with empty schemas
@@ -99,202 +82,9 @@
9982
}
10083
});
10184
102-
const fetchMetadata = async (components: Record<string, ComponentConfig>) => {
103-
const metadataUpdates: Record<
104-
string,
105-
AlbumResponseDto | PersonResponseDto | AlbumResponseDto[] | PersonResponseDto[]
106-
> = {};
107-
108-
for (const [key, component] of Object.entries(components)) {
109-
const value = actualConfig[key];
110-
if (!value || pickerMetadata[key]) {
111-
continue; // Skip if no value or already loaded
112-
}
113-
114-
const isAlbumPicker = component.subType === 'album-picker';
115-
const isPeoplePicker = component.subType === 'people-picker';
116-
117-
if (!isAlbumPicker && !isPeoplePicker) {
118-
continue;
119-
}
120-
121-
try {
122-
if (Array.isArray(value) && value.length > 0) {
123-
// Multiple selection
124-
if (isAlbumPicker) {
125-
const albums = await Promise.all(value.map((id) => getAlbumInfo({ id })));
126-
metadataUpdates[key] = albums;
127-
} else if (isPeoplePicker) {
128-
const people = await Promise.all(value.map((id) => getPerson({ id })));
129-
metadataUpdates[key] = people;
130-
}
131-
} else if (typeof value === 'string' && value) {
132-
// Single selection
133-
if (isAlbumPicker) {
134-
const album = await getAlbumInfo({ id: value });
135-
metadataUpdates[key] = album;
136-
} else if (isPeoplePicker) {
137-
const person = await getPerson({ id: value });
138-
metadataUpdates[key] = person;
139-
}
140-
}
141-
} catch (error) {
142-
console.error(`Failed to fetch metadata for ${key}:`, error);
143-
}
144-
}
145-
146-
if (Object.keys(metadataUpdates).length > 0) {
147-
pickerMetadata = { ...pickerMetadata, ...metadataUpdates };
148-
}
149-
};
150-
151-
const handleAlbumPicker = async (key: string, multiple: boolean) => {
152-
const albums = await modalManager.show(AlbumPickerModal, { shared: false });
153-
if (albums && albums.length > 0) {
154-
const value = multiple ? albums.map((a) => a.id) : albums[0].id;
155-
updateConfig(key, value);
156-
pickerMetadata = {
157-
...pickerMetadata,
158-
[key]: multiple ? albums : albums[0],
159-
};
160-
}
161-
};
162-
163-
const handlePeoplePicker = async (key: string, multiple: boolean) => {
164-
const currentIds = (actualConfig[key] as string[] | undefined) ?? [];
165-
const excludedIds = multiple ? currentIds : [];
166-
const people = await modalManager.show(PeoplePickerModal, { multiple, excludedIds });
167-
if (people && people.length > 0) {
168-
const value = multiple ? people.map((p) => p.id) : people[0].id;
169-
updateConfig(key, value);
170-
pickerMetadata = {
171-
...pickerMetadata,
172-
[key]: multiple ? people : people[0],
173-
};
174-
}
175-
};
176-
177-
const removeSelection = (key: string) => {
178-
const { [key]: _, ...rest } = actualConfig;
179-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
180-
const { [key]: _removed, ...restMetadata } = pickerMetadata;
181-
182-
config = configKey ? { ...config, [configKey]: rest } : rest;
183-
pickerMetadata = restMetadata;
184-
};
185-
186-
const removeItemFromSelection = (key: string, itemId: string) => {
187-
const currentIds = actualConfig[key] as string[];
188-
const currentMetadata = pickerMetadata[key] as (AlbumResponseDto | PersonResponseDto)[];
189-
190-
updateConfig(
191-
key,
192-
currentIds.filter((id) => id !== itemId),
193-
);
194-
pickerMetadata = {
195-
...pickerMetadata,
196-
[key]: currentMetadata.filter((item) => item.id !== itemId) as AlbumResponseDto[] | PersonResponseDto[],
197-
};
198-
};
199-
200-
const renderPicker = (subType: 'album-picker' | 'people-picker', multiple: boolean) => {
201-
const isAlbum = subType === 'album-picker';
202-
const handler = isAlbum ? handleAlbumPicker : handlePeoplePicker;
203-
const selectSingleLabel = isAlbum ? 'select_album' : 'select_person';
204-
const selectMultiLabel = isAlbum ? 'select_albums' : 'select_people';
205-
206-
const buttonText = multiple ? $t(selectMultiLabel) : $t(selectSingleLabel);
207-
208-
return { handler, buttonText };
209-
};
85+
const isPickerField = (subType: string | undefined) => subType === 'album-picker' || subType === 'people-picker';
21086
</script>
21187

212-
{#snippet pickerItemCard(
213-
item: AlbumResponseDto | PersonResponseDto,
214-
isAlbum: boolean,
215-
size: 'large' | 'small',
216-
onRemove: () => void,
217-
)}
218-
{@const sizeClass = size === 'large' ? 'h-16 w-16' : 'h-12 w-12'}
219-
{@const textSizeClass = size === 'large' ? 'font-medium' : 'font-medium text-sm'}
220-
{@const iconSizeClass = size === 'large' ? 'h-5 w-5' : 'h-4 w-4'}
221-
{@const countSizeClass = size === 'large' ? 'text-sm' : 'text-xs'}
222-
223-
<div
224-
class="flex items-center gap-3 rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 p-3 shadow-sm"
225-
>
226-
<div class="shrink-0">
227-
{#if isAlbum && 'albumThumbnailAssetId' in item}
228-
{#if item.albumThumbnailAssetId}
229-
<img
230-
src={getAssetThumbnailUrl(item.albumThumbnailAssetId)}
231-
alt={item.albumName}
232-
class="{sizeClass} rounded-lg object-cover"
233-
/>
234-
{:else}
235-
<div class="{sizeClass} rounded-lg bg-gray-200 dark:bg-gray-700"></div>
236-
{/if}
237-
{:else if !isAlbum && 'name' in item}
238-
<img src={getPeopleThumbnailUrl(item)} alt={item.name} class="{sizeClass} rounded-full object-cover" />
239-
{/if}
240-
</div>
241-
<div class="flex-1 min-w-0">
242-
<p class="{textSizeClass} text-gray-900 dark:text-gray-100 truncate">
243-
{isAlbum && 'albumName' in item ? item.albumName : 'name' in item ? item.name : ''}
244-
</p>
245-
{#if isAlbum && 'assetCount' in item}
246-
<p class="{countSizeClass} text-gray-500 dark:text-gray-400">
247-
{$t('items_count', { values: { count: item.assetCount } })}
248-
</p>
249-
{/if}
250-
</div>
251-
<button
252-
type="button"
253-
onclick={onRemove}
254-
class="shrink-0 rounded-full p-1.5 text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
255-
aria-label={$t('remove')}
256-
>
257-
<svg class={iconSizeClass} fill="none" viewBox="0 0 24 24" stroke="currentColor">
258-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
259-
</svg>
260-
</button>
261-
</div>
262-
{/snippet}
263-
264-
{#snippet pickerField(
265-
subType: string,
266-
key: string,
267-
label: string,
268-
component: { required?: boolean; description?: string },
269-
multiple: boolean,
270-
)}
271-
{@const picker = renderPicker(subType as 'album-picker' | 'people-picker', multiple)}
272-
{@const metadata = pickerMetadata[key]}
273-
{@const isAlbum = subType === 'album-picker'}
274-
275-
<Field
276-
{label}
277-
required={component.required}
278-
description={component.description}
279-
requiredIndicator={component.required}
280-
>
281-
<div class="flex flex-col gap-3">
282-
{#if metadata && !Array.isArray(metadata)}
283-
{@render pickerItemCard(metadata, isAlbum, 'large', () => removeSelection(key))}
284-
{:else if metadata && Array.isArray(metadata) && metadata.length > 0}
285-
<div class="flex flex-col gap-2">
286-
{#each metadata as item (item.id)}
287-
{@render pickerItemCard(item, isAlbum, 'small', () => removeItemFromSelection(key, item.id))}
288-
{/each}
289-
</div>
290-
{/if}
291-
<Button size="small" variant="outline" leadingIcon={mdiPlus} onclick={() => picker.handler(key, multiple)}>
292-
{picker.buttonText}
293-
</Button>
294-
</div>
295-
</Field>
296-
{/snippet}
297-
29888
{#if components}
29989
<div class="flex flex-col gap-2">
30090
{#each Object.entries(components) as [key, component] (key)}
@@ -303,8 +93,13 @@
30393
<div class="flex flex-col gap-1 bg-light-50 border p-4 rounded-xl">
30494
<!-- Select component -->
30595
{#if component.type === 'select'}
306-
{#if component.subType === 'album-picker' || component.subType === 'people-picker'}
307-
{@render pickerField(component.subType, key, label, component, false)}
96+
{#if isPickerField(component.subType)}
97+
<WorkflowPickerField
98+
{component}
99+
configKey={key}
100+
value={actualConfig[key] as string | string[]}
101+
onchange={(value) => updateConfig(key, value)}
102+
/>
308103
{:else}
309104
{@const options = component.options?.map((opt) => {
310105
return { label: opt.label, value: String(opt.value) };
@@ -322,8 +117,13 @@
322117

323118
<!-- MultiSelect component -->
324119
{:else if component.type === 'multiselect'}
325-
{#if component.subType === 'album-picker' || component.subType === 'people-picker'}
326-
{@render pickerField(component.subType, key, label, component, true)}
120+
{#if isPickerField(component.subType)}
121+
<WorkflowPickerField
122+
{component}
123+
configKey={key}
124+
value={actualConfig[key] as string | string[]}
125+
onchange={(value) => updateConfig(key, value)}
126+
/>
327127
{:else}
328128
{@const options = component.options?.map((opt) => {
329129
return { label: opt.label, value: String(opt.value) };
@@ -359,8 +159,13 @@
359159
</Field>
360160

361161
<!-- Text input -->
362-
{:else if component.subType === 'album-picker' || component.subType === 'people-picker'}
363-
{@render pickerField(component.subType, key, label, component, false)}
162+
{:else if isPickerField(component.subType)}
163+
<WorkflowPickerField
164+
{component}
165+
configKey={key}
166+
value={actualConfig[key] as string | string[]}
167+
onchange={(value) => updateConfig(key, value)}
168+
/>
364169
{:else}
365170
<Field
366171
{label}

0 commit comments

Comments
 (0)