feat(web): immich/ui select component (#25268)

Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
Jason Rasmussen
2026-01-14 20:38:13 -05:00
committed by GitHub
parent 7b3a298c6a
commit d59ee7d2ae
9 changed files with 90 additions and 109 deletions

10
pnpm-lock.yaml generated
View File

@@ -741,8 +741,8 @@ importers:
specifier: file:../open-api/typescript-sdk
version: link:../open-api/typescript-sdk
'@immich/ui':
specifier: ^0.58.2
version: 0.58.2(@sveltejs/kit@2.49.3(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.3(svelte@5.46.1)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.1)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.1)
specifier: ^0.58.4
version: 0.58.4(@sveltejs/kit@2.49.3(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.3(svelte@5.46.1)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.1)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.1)
'@mapbox/mapbox-gl-rtl-text':
specifier: 0.2.3
version: 0.2.3(mapbox-gl@1.13.3)
@@ -3128,8 +3128,8 @@ packages:
peerDependencies:
svelte: ^5.0.0
'@immich/ui@0.58.2':
resolution: {integrity: sha512-6+lg5v2EKlYivjl4TAz+LNLQfbalXn/01Vzur5XqrZNgZvo0GBHHCW82gbT/tEixnaF0WiUzUsXPoLlSfF62jQ==}
'@immich/ui@0.58.4':
resolution: {integrity: sha512-/Y+TRA9E8VQ+yH0aqrkEnQTQi4j02dNgahil9NbJe3hSnakfDHZUgJR5xevGZbKqlnBV4O3mjbwmzr6j9wlP7w==}
peerDependencies:
svelte: ^5.0.0
@@ -15604,7 +15604,7 @@ snapshots:
dependencies:
svelte: 5.46.1
'@immich/ui@0.58.2(@sveltejs/kit@2.49.3(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.3(svelte@5.46.1)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.1)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.1)':
'@immich/ui@0.58.4(@sveltejs/kit@2.49.3(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.3(svelte@5.46.1)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.1)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.1)':
dependencies:
'@immich/svelte-markdown-preprocess': 0.1.0(svelte@5.46.1)
'@internationalized/date': 3.10.0

View File

@@ -27,7 +27,7 @@
"@formatjs/icu-messageformat-parser": "^3.0.0",
"@immich/justified-layout-wasm": "^0.4.3",
"@immich/sdk": "file:../open-api/typescript-sdk",
"@immich/ui": "^0.58.2",
"@immich/ui": "^0.58.4",
"@mapbox/mapbox-gl-rtl-text": "0.2.3",
"@mdi/js": "^7.4.47",
"@photo-sphere-viewer/core": "^5.14.0",

View File

@@ -1,10 +1,9 @@
<script lang="ts">
import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.svelte';
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
import { preferences } from '$lib/stores/user.store';
import { handleError } from '$lib/utils/handle-error';
import { AssetOrder, updateMyPreferences } from '@immich/sdk';
import { Button, Field, NumberInput, Switch, toastManager } from '@immich/ui';
import { Button, Field, NumberInput, Select, Switch, toastManager } from '@immich/ui';
import { t } from 'svelte-i18n';
import { fade } from 'svelte/transition';
@@ -71,15 +70,15 @@
<div class="ms-4 mt-4 flex flex-col">
<SettingAccordion key="albums" title={$t('albums')} subtitle={$t('albums_feature_description')}>
<div class="ms-4 mt-6 flex flex-col gap-4">
<SettingSelect
label={$t('albums_default_sort_order')}
desc={$t('albums_default_sort_order_description')}
options={[
{ value: AssetOrder.Asc, text: $t('oldest_first') },
{ value: AssetOrder.Desc, text: $t('newest_first') },
]}
bind:value={defaultAssetOrder}
/>
<Field label={$t('albums_default_sort_order')} description={$t('albums_default_sort_order_description')}>
<Select
options={[
{ label: $t('oldest_first'), value: AssetOrder.Asc },
{ label: $t('newest_first'), value: AssetOrder.Desc },
]}
bind:value={defaultAssetOrder}
/>
</Field>
</div>
</SettingAccordion>

View File

@@ -81,16 +81,13 @@
{@const options = component.options?.map((opt) => {
return { label: opt.label, value: String(opt.value) };
}) || [{ label: 'N/A', value: '' }]}
{@const currentValue = actualConfig[key]}
{@const selectedItem = options.find((opt) => opt.value === String(currentValue)) ?? options[0]}
<Field
{label}
required={component.required}
description={component.description}
requiredIndicator={component.required}
>
<Select data={options} onChange={(opt) => updateConfig(key, opt.value)} value={selectedItem} />
<Select {options} onChange={(value) => updateConfig(key, value)} value={actualConfig[key] as string} />
</Field>
{/if}
@@ -107,9 +104,6 @@
{@const options = component.options?.map((opt) => {
return { label: opt.label, value: String(opt.value) };
}) || [{ label: 'N/A', value: '' }]}
{@const currentValues = (actualConfig[key] as string[]) ?? []}
{@const selectedItems = options.filter((opt) => currentValues.includes(opt.value))}
<Field
{label}
required={component.required}
@@ -117,13 +111,9 @@
requiredIndicator={component.required}
>
<MultiSelect
data={options}
onChange={(opt) =>
updateConfig(
key,
opt.map((o) => o.value),
)}
values={selectedItems}
{options}
values={(actualConfig[key] as string[]) ?? []}
onChange={(values) => updateConfig(key, values)}
/>
</Field>
{/if}

View File

@@ -19,7 +19,7 @@
type SharedLinkResponseDto,
type UserResponseDto,
} from '@immich/sdk';
import { Field, HStack, Modal, ModalBody, Select, Stack, Switch, Text } from '@immich/ui';
import { Field, HStack, Modal, ModalBody, Select, Stack, Switch, Text, type SelectOption } from '@immich/ui';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
@@ -30,21 +30,6 @@
let { album, onClose }: Props = $props();
const orderOptions = [
{ label: $t('newest_first'), value: AssetOrder.Desc },
{ label: $t('oldest_first'), value: AssetOrder.Asc },
];
const roleOptions: Array<{ label: string; value: AlbumUserRole | 'none'; icon?: string }> = [
{ label: $t('role_editor'), value: AlbumUserRole.Editor },
{ label: $t('role_viewer'), value: AlbumUserRole.Viewer },
{ label: $t('remove_user'), value: 'none' },
];
const selectedOrderOption = $derived(
album.order ? orderOptions.find(({ value }) => value === album.order) : orderOptions[0],
);
const handleRoleSelect = async (user: UserResponseDto, role: AlbumUserRole | 'none') => {
if (role === 'none') {
await handleRemoveUserFromAlbum(album, user);
@@ -97,9 +82,12 @@
{#if album.order}
<Field label={$t('display_order')}>
<Select
data={orderOptions}
value={selectedOrderOption}
onChange={({ value }) => handleUpdateAlbum(album, { order: value })}
value={album.order}
options={[
{ label: $t('newest_first'), value: AssetOrder.Desc },
{ label: $t('oldest_first'), value: AssetOrder.Asc },
]}
onChange={(value) => handleUpdateAlbum(album, { order: value })}
/>
</Field>
{/if}
@@ -124,7 +112,7 @@
</div>
<Text class="w-full" size="small">{$user.name}</Text>
<Field disabled class="w-32 shrink-0">
<Select data={[{ label: $t('owner'), value: 'owner' }]} value={{ label: $t('owner'), value: 'owner' }} />
<Select options={[{ label: $t('owner'), value: 'owner' }]} value="owner" />
</Field>
</div>
@@ -138,9 +126,13 @@
</div>
<Field class="w-32">
<Select
data={roleOptions}
value={roleOptions.find(({ value }) => value === role)}
onChange={({ value }) => handleRoleSelect(user, value)}
value={role}
options={[
{ label: $t('role_editor'), value: AlbumUserRole.Editor },
{ label: $t('role_viewer'), value: AlbumUserRole.Viewer },
{ label: $t('remove_user'), value: 'none' },
] as SelectOption<AlbumUserRole | 'none'>[]}
onChange={(value) => handleRoleSelect(user, value)}
/>
</Field>
</div>

View File

@@ -1,16 +1,15 @@
<script lang="ts">
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
import DateInput from '$lib/elements/DateInput.svelte';
import type { MapSettings } from '$lib/stores/preferences.store';
import { Button, Field, FormModal, Stack, Switch } from '@immich/ui';
import { Button, Field, FormModal, Select, Stack, Switch } from '@immich/ui';
import { Duration } from 'luxon';
import { t } from 'svelte-i18n';
import { fly } from 'svelte/transition';
interface Props {
type Props = {
settings: MapSettings;
onClose: (settings?: MapSettings) => void;
}
};
let { settings: initialValues, onClose }: Props = $props();
let settings = $state(initialValues);
@@ -73,37 +72,37 @@
</div>
{:else}
<div in:fly={{ y: -10, duration: 200 }} class="flex flex-col gap-1">
<SettingSelect
label={$t('date_range')}
name="date-range"
bind:value={settings.relativeDate}
options={[
{
value: '',
text: $t('all'),
},
{
value: Duration.fromObject({ hours: 24 }).toISO() || '',
text: $t('past_durations.hours', { values: { hours: 24 } }),
},
{
value: Duration.fromObject({ days: 7 }).toISO() || '',
text: $t('past_durations.days', { values: { days: 7 } }),
},
{
value: Duration.fromObject({ days: 30 }).toISO() || '',
text: $t('past_durations.days', { values: { days: 30 } }),
},
{
value: Duration.fromObject({ years: 1 }).toISO() || '',
text: $t('past_durations.years', { values: { years: 1 } }),
},
{
value: Duration.fromObject({ years: 3 }).toISO() || '',
text: $t('past_durations.years', { values: { years: 3 } }),
},
]}
/>
<Field label={$t('date_range')}>
<Select
bind:value={settings.relativeDate}
options={[
{
label: $t('all'),
value: '',
},
{
label: $t('past_durations.hours', { values: { hours: 24 } }),
value: Duration.fromObject({ hours: 24 }).toISO() || '',
},
{
label: $t('past_durations.days', { values: { days: 7 } }),
value: Duration.fromObject({ days: 7 }).toISO() || '',
},
{
label: $t('past_durations.days', { values: { days: 30 } }),
value: Duration.fromObject({ days: 30 }).toISO() || '',
},
{
label: $t('past_durations.years', { values: { years: 1 } }),
value: Duration.fromObject({ years: 1 }).toISO() || '',
},
{
label: $t('past_durations.years', { values: { years: 3 } }),
value: Duration.fromObject({ years: 3 }).toISO() || '',
},
]}
/>
</Field>
<div class="text-xs">
<Button
color="primary"

View File

@@ -1,8 +1,7 @@
<script lang="ts">
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
import { handleCreateApiKey } from '$lib/services/api-key.service';
import { Permission } from '@immich/sdk';
import { Button, Field, Input, Modal, ModalBody, obtainiumBadge, Text } from '@immich/ui';
import { Button, Field, Input, Modal, ModalBody, obtainiumBadge, Select, Text } from '@immich/ui';
import { t } from 'svelte-i18n';
let inputUrl = $state(location.origin);
let inputApiKey = $state('');
@@ -29,6 +28,18 @@
<ModalBody>
<Text color="muted" size="small">{$t('obtainium_configurator_instructions')}</Text>
<Field label={$t('app_architecture_variant')} class="mt-4">
<Select
bind:value={archVariant}
options={[
{ label: 'arm64-v8a', value: 'arm64-v8a-release' },
{ label: 'armeabi-v7a', value: 'armeabi-v7a-release' },
{ label: 'universal', value: 'release' },
{ label: 'x86_64', value: 'x86_64-release' },
]}
/>
</Field>
<Field label={$t('url')} class="mt-4">
<Input bind:value={inputUrl} />
</Field>
@@ -41,17 +52,6 @@
<Button size="small" onclick={handleCreate}>{$t('create_api_key')}</Button>
</div>
<SettingSelect
label={$t('app_architecture_variant')}
bind:value={archVariant}
options={[
{ value: 'arm64-v8a-release', text: 'arm64-v8a' },
{ value: 'armeabi-v7a-release', text: 'armeabi-v7a' },
{ value: 'release', text: 'universal' },
{ value: 'x86_64-release', text: 'x86_64' },
]}
/>
{#if inputUrl && inputApiKey && archVariant}
<div class="content-center">
<hr />

View File

@@ -104,6 +104,7 @@
</Table>
{:else}
<EmptyPlaceholder
fullWidth
text={$t('no_libraries_message')}
onClick={() => goto(AppRoute.ADMIN_LIBRARIES_NEW)}
class="mt-10 mx-auto"

View File

@@ -1,10 +1,9 @@
<script lang="ts">
import { goto } from '$app/navigation';
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
import { AppRoute } from '$lib/constants';
import { handleCreateLibrary } from '$lib/services/library.service';
import { user } from '$lib/stores/user.store';
import { FormModal, Text } from '@immich/ui';
import { Field, FormModal, HelperText, Select } from '@immich/ui';
import { mdiFolderSync } from '@mdi/js';
import { t } from 'svelte-i18n';
import { type PageData } from './$types';
@@ -17,7 +16,6 @@
let ownerId: string = $state($user.id);
const users = $state(data.allUsers);
const userOptions = $derived(users.map((user) => ({ value: user.id, text: user.name })));
const onClose = async () => {
await goto(AppRoute.ADMIN_LIBRARIES);
@@ -34,11 +32,13 @@
<FormModal
title={$t('create_library')}
icon={mdiFolderSync}
{onClose}
size="small"
{onSubmit}
submitText={$t('create')}
{onClose}
{onSubmit}
>
<SettingSelect label={$t('owner')} bind:value={ownerId} options={userOptions} name="user" />
<Text color="warning" size="small">{$t('admin.note_cannot_be_changed_later')}</Text>
<Field label={$t('owner')}>
<Select bind:value={ownerId} options={users.map((user) => ({ label: user.name, value: user.id }))} />
<HelperText color="warning">{$t('admin.note_cannot_be_changed_later')}</HelperText>
</Field>
</FormModal>