mirror of
https://github.com/immich-app/immich.git
synced 2026-01-18 15:55:56 -08:00
feat(web): immich/ui select component (#25268)
Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -104,6 +104,7 @@
|
||||
</Table>
|
||||
{:else}
|
||||
<EmptyPlaceholder
|
||||
fullWidth
|
||||
text={$t('no_libraries_message')}
|
||||
onClick={() => goto(AppRoute.ADMIN_LIBRARIES_NEW)}
|
||||
class="mt-10 mx-auto"
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user