mirror of
https://github.com/immich-app/immich.git
synced 2025-12-17 09:13:17 +03:00
refactor: picker field
This commit is contained in:
@@ -1,5 +1,4 @@
|
|||||||
{
|
{
|
||||||
"get_people_error": "Error getting people",
|
|
||||||
"about": "About",
|
"about": "About",
|
||||||
"account": "Account",
|
"account": "Account",
|
||||||
"account_settings": "Account Settings",
|
"account_settings": "Account Settings",
|
||||||
@@ -1160,6 +1159,7 @@
|
|||||||
"general": "General",
|
"general": "General",
|
||||||
"geolocation_instruction_location": "Click on an asset with GPS coordinates to use its location, or select a location directly from the map",
|
"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_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",
|
"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",
|
"getting_started": "Getting Started",
|
||||||
"go_back": "Go back",
|
"go_back": "Go back",
|
||||||
@@ -2256,6 +2256,7 @@
|
|||||||
"viewer_stack_use_as_main_asset": "Use as Main Asset",
|
"viewer_stack_use_as_main_asset": "Use as Main Asset",
|
||||||
"viewer_unstack": "Un-Stack",
|
"viewer_unstack": "Un-Stack",
|
||||||
"visibility_changed": "Visibility changed for {count, plural, one {# person} other {# people}}",
|
"visibility_changed": "Visibility changed for {count, plural, one {# person} other {# people}}",
|
||||||
|
"visual": "Visual",
|
||||||
"visual_builder": "Visual builder",
|
"visual_builder": "Visual builder",
|
||||||
"waiting": "Waiting",
|
"waiting": "Waiting",
|
||||||
"warning": "Warning",
|
"warning": "Warning",
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import AlbumPickerModal from '$lib/modals/AlbumPickerModal.svelte';
|
import { formatLabel, getComponentFromSchema } from '$lib/utils/workflow';
|
||||||
import PeoplePickerModal from '$lib/modals/PeoplePickerModal.svelte';
|
import { Field, Input, MultiSelect, Select, Switch, Text, type SelectItem } from '@immich/ui';
|
||||||
import { getAssetThumbnailUrl, getPeopleThumbnailUrl } from '$lib/utils';
|
import WorkflowPickerField from './WorkflowPickerField.svelte';
|
||||||
import { formatLabel, getComponentFromSchema, type ComponentConfig } from '$lib/utils/workflow';
|
|
||||||
import { getAlbumInfo, getPerson, type AlbumResponseDto, type PersonResponseDto } from '@immich/sdk';
|
|
||||||
import { Button, Field, Input, MultiSelect, Select, Switch, Text, modalManager, type SelectItem } from '@immich/ui';
|
|
||||||
import { mdiPlus } from '@mdi/js';
|
|
||||||
import { t } from 'svelte-i18n';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
schema: object | null;
|
schema: object | null;
|
||||||
@@ -33,18 +28,6 @@
|
|||||||
let selectValue = $state<SelectItem>();
|
let selectValue = $state<SelectItem>();
|
||||||
let switchValue = $state<boolean>(false);
|
let switchValue = $state<boolean>(false);
|
||||||
let multiSelectValue = $state<SelectItem[]>([]);
|
let multiSelectValue = $state<SelectItem[]>([]);
|
||||||
let pickerMetadata = $state<
|
|
||||||
Record<string, AlbumResponseDto | PersonResponseDto | AlbumResponseDto[] | PersonResponseDto[]>
|
|
||||||
>({});
|
|
||||||
|
|
||||||
// Fetch metadata for existing picker values (albums/people)
|
|
||||||
$effect(() => {
|
|
||||||
if (!components) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
void fetchMetadata(components);
|
|
||||||
});
|
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
// Initialize config for actions/filters with empty schemas
|
// Initialize config for actions/filters with empty schemas
|
||||||
@@ -99,212 +82,24 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const fetchMetadata = async (components: Record<string, ComponentConfig>) => {
|
const isPickerField = (subType: string | undefined) => subType === 'album-picker' || subType === 'people-picker';
|
||||||
const metadataUpdates: Record<
|
|
||||||
string,
|
|
||||||
AlbumResponseDto | PersonResponseDto | AlbumResponseDto[] | PersonResponseDto[]
|
|
||||||
> = {};
|
|
||||||
|
|
||||||
for (const [key, component] of Object.entries(components)) {
|
|
||||||
const value = actualConfig[key];
|
|
||||||
if (!value || pickerMetadata[key]) {
|
|
||||||
continue; // Skip if no value or already loaded
|
|
||||||
}
|
|
||||||
|
|
||||||
const isAlbumPicker = component.subType === 'album-picker';
|
|
||||||
const isPeoplePicker = component.subType === 'people-picker';
|
|
||||||
|
|
||||||
if (!isAlbumPicker && !isPeoplePicker) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (Array.isArray(value) && value.length > 0) {
|
|
||||||
// Multiple selection
|
|
||||||
if (isAlbumPicker) {
|
|
||||||
const albums = await Promise.all(value.map((id) => getAlbumInfo({ id })));
|
|
||||||
metadataUpdates[key] = albums;
|
|
||||||
} else if (isPeoplePicker) {
|
|
||||||
const people = await Promise.all(value.map((id) => getPerson({ id })));
|
|
||||||
metadataUpdates[key] = people;
|
|
||||||
}
|
|
||||||
} else if (typeof value === 'string' && value) {
|
|
||||||
// Single selection
|
|
||||||
if (isAlbumPicker) {
|
|
||||||
const album = await getAlbumInfo({ id: value });
|
|
||||||
metadataUpdates[key] = album;
|
|
||||||
} else if (isPeoplePicker) {
|
|
||||||
const person = await getPerson({ id: value });
|
|
||||||
metadataUpdates[key] = person;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to fetch metadata for ${key}:`, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.keys(metadataUpdates).length > 0) {
|
|
||||||
pickerMetadata = { ...pickerMetadata, ...metadataUpdates };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAlbumPicker = async (key: string, multiple: boolean) => {
|
|
||||||
const albums = await modalManager.show(AlbumPickerModal, { shared: false });
|
|
||||||
if (albums && albums.length > 0) {
|
|
||||||
const value = multiple ? albums.map((a) => a.id) : albums[0].id;
|
|
||||||
updateConfig(key, value);
|
|
||||||
pickerMetadata = {
|
|
||||||
...pickerMetadata,
|
|
||||||
[key]: multiple ? albums : albums[0],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePeoplePicker = async (key: string, multiple: boolean) => {
|
|
||||||
const currentIds = (actualConfig[key] as string[] | undefined) ?? [];
|
|
||||||
const excludedIds = multiple ? currentIds : [];
|
|
||||||
const people = await modalManager.show(PeoplePickerModal, { multiple, excludedIds });
|
|
||||||
if (people && people.length > 0) {
|
|
||||||
const value = multiple ? people.map((p) => p.id) : people[0].id;
|
|
||||||
updateConfig(key, value);
|
|
||||||
pickerMetadata = {
|
|
||||||
...pickerMetadata,
|
|
||||||
[key]: multiple ? people : people[0],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeSelection = (key: string) => {
|
|
||||||
const { [key]: _, ...rest } = actualConfig;
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const { [key]: _removed, ...restMetadata } = pickerMetadata;
|
|
||||||
|
|
||||||
config = configKey ? { ...config, [configKey]: rest } : rest;
|
|
||||||
pickerMetadata = restMetadata;
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeItemFromSelection = (key: string, itemId: string) => {
|
|
||||||
const currentIds = actualConfig[key] as string[];
|
|
||||||
const currentMetadata = pickerMetadata[key] as (AlbumResponseDto | PersonResponseDto)[];
|
|
||||||
|
|
||||||
updateConfig(
|
|
||||||
key,
|
|
||||||
currentIds.filter((id) => id !== itemId),
|
|
||||||
);
|
|
||||||
pickerMetadata = {
|
|
||||||
...pickerMetadata,
|
|
||||||
[key]: currentMetadata.filter((item) => item.id !== itemId) as AlbumResponseDto[] | PersonResponseDto[],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderPicker = (subType: 'album-picker' | 'people-picker', multiple: boolean) => {
|
|
||||||
const isAlbum = subType === 'album-picker';
|
|
||||||
const handler = isAlbum ? handleAlbumPicker : handlePeoplePicker;
|
|
||||||
const selectSingleLabel = isAlbum ? 'select_album' : 'select_person';
|
|
||||||
const selectMultiLabel = isAlbum ? 'select_albums' : 'select_people';
|
|
||||||
|
|
||||||
const buttonText = multiple ? $t(selectMultiLabel) : $t(selectSingleLabel);
|
|
||||||
|
|
||||||
return { handler, buttonText };
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#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'}
|
|
||||||
|
|
||||||
<div
|
|
||||||
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"
|
|
||||||
>
|
|
||||||
<div class="shrink-0">
|
|
||||||
{#if isAlbum && 'albumThumbnailAssetId' in item}
|
|
||||||
{#if item.albumThumbnailAssetId}
|
|
||||||
<img
|
|
||||||
src={getAssetThumbnailUrl(item.albumThumbnailAssetId)}
|
|
||||||
alt={item.albumName}
|
|
||||||
class="{sizeClass} rounded-lg object-cover"
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
<div class="{sizeClass} rounded-lg bg-gray-200 dark:bg-gray-700"></div>
|
|
||||||
{/if}
|
|
||||||
{:else if !isAlbum && 'name' in item}
|
|
||||||
<img src={getPeopleThumbnailUrl(item)} alt={item.name} class="{sizeClass} rounded-full object-cover" />
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
<div class="flex-1 min-w-0">
|
|
||||||
<p class="{textSizeClass} text-gray-900 dark:text-gray-100 truncate">
|
|
||||||
{isAlbum && 'albumName' in item ? item.albumName : 'name' in item ? item.name : ''}
|
|
||||||
</p>
|
|
||||||
{#if isAlbum && 'assetCount' in item}
|
|
||||||
<p class="{countSizeClass} text-gray-500 dark:text-gray-400">
|
|
||||||
{$t('items_count', { values: { count: item.assetCount } })}
|
|
||||||
</p>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onclick={onRemove}
|
|
||||||
class="shrink-0 rounded-full p-1.5 text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
|
||||||
aria-label={$t('remove')}
|
|
||||||
>
|
|
||||||
<svg class={iconSizeClass} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{/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'}
|
|
||||||
|
|
||||||
<Field
|
|
||||||
{label}
|
|
||||||
required={component.required}
|
|
||||||
description={component.description}
|
|
||||||
requiredIndicator={component.required}
|
|
||||||
>
|
|
||||||
<div class="flex flex-col gap-3">
|
|
||||||
{#if metadata && !Array.isArray(metadata)}
|
|
||||||
{@render pickerItemCard(metadata, isAlbum, 'large', () => removeSelection(key))}
|
|
||||||
{:else if metadata && Array.isArray(metadata) && metadata.length > 0}
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
{#each metadata as item (item.id)}
|
|
||||||
{@render pickerItemCard(item, isAlbum, 'small', () => removeItemFromSelection(key, item.id))}
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
<Button size="small" variant="outline" leadingIcon={mdiPlus} onclick={() => picker.handler(key, multiple)}>
|
|
||||||
{picker.buttonText}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Field>
|
|
||||||
{/snippet}
|
|
||||||
|
|
||||||
{#if components}
|
{#if components}
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
{#each Object.entries(components) as [key, component] (key)}
|
{#each Object.entries(components) as [key, component] (key)}
|
||||||
{@const label = component.title || component.label || key}
|
{@const label = component.title || component.label || key}
|
||||||
|
|
||||||
<div class="flex flex-col gap-1 bg-light-50 border p-4 rounded-xl">
|
<div class="flex flex-col gap-1 border bg-light p-4 rounded-xl">
|
||||||
<!-- Select component -->
|
<!-- Select component -->
|
||||||
{#if component.type === 'select'}
|
{#if component.type === 'select'}
|
||||||
{#if component.subType === 'album-picker' || component.subType === 'people-picker'}
|
{#if isPickerField(component.subType)}
|
||||||
{@render pickerField(component.subType, key, label, component, false)}
|
<WorkflowPickerField
|
||||||
|
{component}
|
||||||
|
configKey={key}
|
||||||
|
value={actualConfig[key] as string | string[]}
|
||||||
|
onchange={(value) => updateConfig(key, value)}
|
||||||
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
{@const options = component.options?.map((opt) => {
|
{@const options = component.options?.map((opt) => {
|
||||||
return { label: opt.label, value: String(opt.value) };
|
return { label: opt.label, value: String(opt.value) };
|
||||||
@@ -322,8 +117,13 @@
|
|||||||
|
|
||||||
<!-- MultiSelect component -->
|
<!-- MultiSelect component -->
|
||||||
{:else if component.type === 'multiselect'}
|
{:else if component.type === 'multiselect'}
|
||||||
{#if component.subType === 'album-picker' || component.subType === 'people-picker'}
|
{#if isPickerField(component.subType)}
|
||||||
{@render pickerField(component.subType, key, label, component, true)}
|
<WorkflowPickerField
|
||||||
|
{component}
|
||||||
|
configKey={key}
|
||||||
|
value={actualConfig[key] as string | string[]}
|
||||||
|
onchange={(value) => updateConfig(key, value)}
|
||||||
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
{@const options = component.options?.map((opt) => {
|
{@const options = component.options?.map((opt) => {
|
||||||
return { label: opt.label, value: String(opt.value) };
|
return { label: opt.label, value: String(opt.value) };
|
||||||
@@ -359,8 +159,13 @@
|
|||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<!-- Text input -->
|
<!-- Text input -->
|
||||||
{:else if component.subType === 'album-picker' || component.subType === 'people-picker'}
|
{:else if isPickerField(component.subType)}
|
||||||
{@render pickerField(component.subType, key, label, component, false)}
|
<WorkflowPickerField
|
||||||
|
{component}
|
||||||
|
configKey={key}
|
||||||
|
value={actualConfig[key] as string | string[]}
|
||||||
|
onchange={(value) => updateConfig(key, value)}
|
||||||
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<Field
|
<Field
|
||||||
{label}
|
{label}
|
||||||
|
|||||||
162
web/src/lib/components/workflows/WorkflowPickerField.svelte
Normal file
162
web/src/lib/components/workflows/WorkflowPickerField.svelte
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
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<AlbumResponseDto | PersonResponseDto | AlbumResponseDto[] | PersonResponseDto[]>();
|
||||||
|
|
||||||
|
// 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');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#snippet pickerItemCard(item: AlbumResponseDto | PersonResponseDto, onRemove: () => void)}
|
||||||
|
<Card color="secondary">
|
||||||
|
<CardBody class="flex items-center gap-3">
|
||||||
|
<div class="shrink-0">
|
||||||
|
{#if isAlbum && 'albumThumbnailAssetId' in item}
|
||||||
|
{#if item.albumThumbnailAssetId}
|
||||||
|
<img
|
||||||
|
src={getAssetThumbnailUrl(item.albumThumbnailAssetId)}
|
||||||
|
alt={item.albumName}
|
||||||
|
class="h-12 w-12 rounded-lg object-cover"
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<div class="h-12 w-12 rounded-lg"></div>
|
||||||
|
{/if}
|
||||||
|
{:else if !isAlbum && 'name' in item}
|
||||||
|
<img src={getPeopleThumbnailUrl(item)} alt={item.name} class="h-12 w-12 rounded-full object-cover" />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
<Text class="font-semibold truncate">
|
||||||
|
{isAlbum && 'albumName' in item ? item.albumName : 'name' in item ? item.name : ''}
|
||||||
|
</Text>
|
||||||
|
{#if isAlbum && 'assetCount' in item}
|
||||||
|
<Text size="small" color="muted">
|
||||||
|
{$t('items_count', { values: { count: item.assetCount } })}
|
||||||
|
</Text>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
type="button"
|
||||||
|
onclick={onRemove}
|
||||||
|
class="shrink-0"
|
||||||
|
shape="round"
|
||||||
|
aria-label={$t('remove')}
|
||||||
|
icon={mdiClose}
|
||||||
|
size="small"
|
||||||
|
variant="ghost"
|
||||||
|
color="secondary"
|
||||||
|
/>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
<Field {label} required={component.required} description={component.description} requiredIndicator={component.required}>
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
{#if pickerMetadata && !Array.isArray(pickerMetadata)}
|
||||||
|
{@render pickerItemCard(pickerMetadata, removeSelection)}
|
||||||
|
{:else if pickerMetadata && Array.isArray(pickerMetadata) && pickerMetadata.length > 0}
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
{#each pickerMetadata as item (item.id)}
|
||||||
|
{@render pickerItemCard(item, () => removeItemFromSelection(item.id))}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<Button size="small" variant="outline" leadingIcon={mdiPlus} onclick={handlePicker}>
|
||||||
|
{getButtonText()}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Field>
|
||||||
@@ -440,7 +440,7 @@
|
|||||||
isDragging: draggedFilterIndex === index,
|
isDragging: draggedFilterIndex === index,
|
||||||
isDragOver: dragOverFilterIndex === 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"
|
||||||
>
|
>
|
||||||
<div class="flex items-start gap-4">
|
<div class="flex items-start gap-4">
|
||||||
{@render cardOrder(index)}
|
{@render cardOrder(index)}
|
||||||
@@ -472,7 +472,7 @@
|
|||||||
leadingIcon={mdiPlus}
|
leadingIcon={mdiPlus}
|
||||||
onclick={() => handleAddStep('filter')}
|
onclick={() => handleAddStep('filter')}
|
||||||
>
|
>
|
||||||
Add more
|
{$t('add_filter')}
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
</CardBody>
|
</CardBody>
|
||||||
@@ -509,7 +509,7 @@
|
|||||||
isDragging: draggedActionIndex === index,
|
isDragging: draggedActionIndex === index,
|
||||||
isDragOver: dragOverActionIndex === 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"
|
||||||
>
|
>
|
||||||
<div class="flex items-start gap-4">
|
<div class="flex items-start gap-4">
|
||||||
{@render cardOrder(index)}
|
{@render cardOrder(index)}
|
||||||
@@ -540,7 +540,7 @@
|
|||||||
leadingIcon={mdiPlus}
|
leadingIcon={mdiPlus}
|
||||||
onclick={() => handleAddStep('action')}
|
onclick={() => handleAddStep('action')}
|
||||||
>
|
>
|
||||||
Add more
|
{$t('add_action')}
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
</CardBody>
|
</CardBody>
|
||||||
@@ -567,7 +567,7 @@
|
|||||||
leadingIcon={mdiViewDashboard}
|
leadingIcon={mdiViewDashboard}
|
||||||
onclick={() => (viewMode = 'visual')}
|
onclick={() => (viewMode = 'visual')}
|
||||||
>
|
>
|
||||||
Visual
|
{$t('visual')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
|
|||||||
Reference in New Issue
Block a user