refactor: modals (#25163)

This commit is contained in:
Jason Rasmussen
2026-01-09 15:05:20 -05:00
committed by GitHub
parent 88327fb872
commit 1e4af9731d
18 changed files with 258 additions and 357 deletions

View File

@@ -56,7 +56,7 @@ test.describe('User Administration', () => {
await expect(page.getByLabel('Admin User')).not.toBeChecked();
await page.getByLabel('Admin User').click();
await expect(page.getByLabel('Admin User')).toBeChecked();
await page.getByRole('button', { name: 'Confirm' }).click();
await page.getByRole('button', { name: 'Save' }).click();
await expect
.poll(async () => {
@@ -85,7 +85,7 @@ test.describe('User Administration', () => {
await expect(page.getByLabel('Admin User')).toBeChecked();
await page.getByLabel('Admin User').click();
await expect(page.getByLabel('Admin User')).not.toBeChecked();
await page.getByRole('button', { name: 'Confirm' }).click();
await page.getByRole('button', { name: 'Save' }).click();
await expect
.poll(async () => {

10
pnpm-lock.yaml generated
View File

@@ -735,8 +735,8 @@ importers:
specifier: file:../open-api/typescript-sdk
version: link:../open-api/typescript-sdk
'@immich/ui':
specifier: ^0.54.0
version: 0.54.0(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(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)(vite@7.3.0(@types/node@25.0.3)(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.56.1
version: 0.56.1(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(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)(vite@7.3.0(@types/node@25.0.3)(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)
@@ -3084,8 +3084,8 @@ packages:
peerDependencies:
svelte: ^5.0.0
'@immich/ui@0.54.0':
resolution: {integrity: sha512-6jvkvKhgsZ7LvspaJkbht/f8W5IRm+vjYkcZecShFAPaxaowbm7io9sO15MpJdIQfPdXg7vwLI527PV3vlBc6A==}
'@immich/ui@0.56.1':
resolution: {integrity: sha512-W4uEQn9pxVKRvIV7sl9p6dU2r7xlVsMFxBeClxtXzSsiJEoE10uZwBIm0L9q17c4TQ/+lk9e/w1e4jNSvFqFwQ==}
peerDependencies:
svelte: ^5.0.0
@@ -15098,7 +15098,7 @@ snapshots:
dependencies:
svelte: 5.46.1
'@immich/ui@0.54.0(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(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)(vite@7.3.0(@types/node@25.0.3)(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.56.1(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(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)(vite@7.3.0(@types/node@25.0.3)(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.54.0",
"@immich/ui": "^0.56.1",
"@mapbox/mapbox-gl-rtl-text": "0.2.3",
"@mdi/js": "^7.4.47",
"@photo-sphere-viewer/core": "^5.14.0",

View File

@@ -32,8 +32,8 @@
const allMethodsDisabled = !configToEdit.oauth.enabled && !configToEdit.passwordLogin.enabled;
if (allMethodsDisabled) {
const isConfirmed = await modalManager.show(AuthDisableLoginConfirmModal);
if (!isConfirmed) {
const confirmed = await modalManager.show(AuthDisableLoginConfirmModal);
if (!confirmed) {
return false;
}
}

View File

@@ -146,7 +146,7 @@
size="medium"
onClose={handleConfirm}
>
{#snippet promptSnippet()}
{#snippet prompt()}
<div class="flex flex-col w-full h-full gap-2">
<div class="relative w-64 sm:w-96 z-1">
{#if suggestionContainer}

View File

@@ -4,10 +4,10 @@
import { mdiKeyVariant } from '@mdi/js';
import { t } from 'svelte-i18n';
interface Props {
type Props = {
secret?: string;
onClose: () => void;
}
};
let { secret = '', onClose }: Props = $props();
</script>

View File

@@ -29,7 +29,7 @@
icon={mdiDeleteForeverOutline}
{onClose}
>
{#snippet promptSnippet()}
{#snippet prompt()}
<p>
<FormatMessage key="permanently_delete_assets_prompt" values={{ count: size }}>
{#snippet children({ message })}

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { ConfirmModal, Field, Textarea } from '@immich/ui';
import { Field, FormModal, Textarea } from '@immich/ui';
import { mdiText } from '@mdi/js';
import { t } from 'svelte-i18n';
@@ -11,16 +11,8 @@
let description = $state('');
</script>
<ConfirmModal
confirmColor="primary"
title={$t('edit_description')}
icon={mdiText}
prompt={$t('edit_description_prompt')}
onClose={(confirmed) => (confirmed ? onClose(description) : onClose())}
>
{#snippet promptSnippet()}
<Field label={$t('description')}>
<Textarea bind:value={description} grow />
</Field>
{/snippet}
</ConfirmModal>
<FormModal title={$t('edit_description')} icon={mdiText} {onClose} onSubmit={() => onClose(description)}>
<Field label={$t('description')}>
<Textarea bind:value={description} grow />
</Field>
</FormModal>

View File

@@ -1,18 +1,18 @@
<script lang="ts">
import FormatMessage from '$lib/elements/FormatMessage.svelte';
import { Button, HStack, Modal, ModalBody, ModalFooter } from '@immich/ui';
import { ConfirmModal } from '@immich/ui';
import { mdiCancel } from '@mdi/js';
import { t } from 'svelte-i18n';
interface Props {
type Props = {
onClose: (confirmed?: boolean) => void;
}
};
let { onClose }: Props = $props();
</script>
<Modal title={$t('admin.disable_login')} icon={mdiCancel} size="small" {onClose}>
<ModalBody>
<ConfirmModal title={$t('admin.disable_login')} icon={mdiCancel} size="small" {onClose}>
{#snippet prompt()}
<div class="flex flex-col gap-4 text-center">
<p>{$t('admin.authentication_settings_disable_all')}</p>
<p>
@@ -30,15 +30,5 @@
</FormatMessage>
</p>
</div>
</ModalBody>
<ModalFooter>
<HStack fullWidth>
<Button shape="round" color="secondary" fullWidth onclick={() => onClose(false)}>
{$t('cancel')}
</Button>
<Button shape="round" color="danger" fullWidth onclick={() => onClose(true)}>
{$t('confirm')}
</Button>
</HStack>
</ModalFooter>
</Modal>
{/snippet}
</ConfirmModal>

View File

@@ -1,33 +1,20 @@
<script lang="ts">
import { Button, HStack, Modal, ModalBody, ModalFooter } from '@immich/ui';
import { ConfirmModal } from '@immich/ui';
import { t } from 'svelte-i18n';
interface Props {
location: { latitude: number | undefined; longitude: number | undefined };
assetCount: number;
onClose: (confirm?: true) => void;
onClose: (confirm: boolean) => void;
}
let { location, assetCount, onClose }: Props = $props();
</script>
<Modal title={$t('confirm')} size="small" {onClose}>
<ModalBody>
<p>
{$t('update_location_action_prompt', {
values: {
count: assetCount,
},
})}
</p>
<ConfirmModal title={$t('confirm')} size="small" confirmColor="primary" {onClose}>
{#snippet prompt()}
<p>{$t('update_location_action_prompt', { values: { count: assetCount } })}</p>
<p>- {$t('latitude')}: {location.latitude}</p>
<p>- {$t('longitude')}: {location.longitude}</p>
</ModalBody>
<ModalFooter>
<HStack fullWidth>
<Button shape="round" color="secondary" fullWidth onclick={() => onClose()}>{$t('cancel')}</Button>
<Button shape="round" type="submit" fullWidth onclick={() => onClose(true)}>{$t('confirm')}</Button>
</HStack>
</ModalFooter>
</Modal>
{/snippet}
</ConfirmModal>

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { copyToClipboard } from '$lib/utils';
import { Button, Code, HStack, IconButton, Modal, ModalBody, ModalFooter, Text } from '@immich/ui';
import { BasicModal, Code, IconButton, Text } from '@immich/ui';
import { mdiCheck, mdiContentCopy } from '@mdi/js';
import { t } from 'svelte-i18n';
@@ -12,33 +12,23 @@
const { onClose, newPassword }: Props = $props();
</script>
<Modal title={$t('password_reset_success')} icon={mdiCheck} onClose={() => onClose()} size="small">
<ModalBody>
<div class="flex flex-col gap-4">
<Text>{$t('admin.user_password_has_been_reset')}</Text>
<BasicModal title={$t('password_reset_success')} icon={mdiCheck} {onClose} size="small" closeText={$t('done')}>
<div class="flex flex-col gap-4">
<Text>{$t('admin.user_password_has_been_reset')}</Text>
<div class="flex justify-center gap-2 items-center">
<Code color="primary">{newPassword}</Code>
<IconButton
icon={mdiContentCopy}
shape="round"
color="secondary"
variant="ghost"
onclick={() => copyToClipboard(newPassword)}
title={$t('copy_password')}
aria-label={$t('copy_password')}
/>
</div>
<Text>{$t('admin.user_password_reset_description')}</Text>
<div class="flex justify-center gap-2 items-center">
<Code color="primary">{newPassword}</Code>
<IconButton
icon={mdiContentCopy}
shape="round"
color="secondary"
variant="ghost"
onclick={() => copyToClipboard(newPassword)}
title={$t('copy_password')}
aria-label={$t('copy_password')}
/>
</div>
</ModalBody>
<ModalFooter>
<HStack fullWidth>
<Button shape="round" color="primary" fullWidth onclick={() => onClose()}>
{$t('done')}
</Button>
</HStack>
</ModalFooter>
</Modal>
<Text>{$t('admin.user_password_reset_description')}</Text>
</div>
</BasicModal>

View File

@@ -2,18 +2,18 @@
import { getPeopleThumbnailUrl } from '$lib/utils';
import { handleError } from '$lib/utils/handle-error';
import { mergePerson, type PersonResponseDto } from '@immich/sdk';
import { Button, HStack, Icon, IconButton, Modal, ModalBody, ModalFooter, toastManager } from '@immich/ui';
import { FormModal, Icon, IconButton, toastManager } from '@immich/ui';
import { mdiArrowLeft, mdiCallMerge, mdiSwapHorizontal } from '@mdi/js';
import { onMount, tick } from 'svelte';
import { t } from 'svelte-i18n';
import ImageThumbnail from '../components/assets/thumbnail/image-thumbnail.svelte';
interface Props {
type Props = {
personToMerge: PersonResponseDto;
personToBeMergedInto: PersonResponseDto;
potentialMergePeople: PersonResponseDto[];
onClose: (people?: [PersonResponseDto, PersonResponseDto]) => void;
}
};
let {
personToMerge = $bindable(),
@@ -32,7 +32,7 @@
choosePersonToMerge = false;
};
const handleMergePerson = async () => {
const onSubmit = async () => {
try {
await mergePerson({
id: personToBeMergedInto.id,
@@ -51,99 +51,95 @@
});
</script>
<Modal title="{$t('merge_people')} - {title}" {onClose}>
<ModalBody>
<div class="flex items-center justify-center gap-2 py-4 md:h-36">
{#if !choosePersonToMerge}
<div class="flex h-20 w-20 items-center px-1 md:h-24 md:w-24 md:px-2">
<ImageThumbnail
circle
shadow
url={getPeopleThumbnailUrl(personToMerge)}
altText={personToMerge.name}
widthStyle="100%"
<FormModal
title="{$t('merge_people')} - {title}"
submitColor="primary"
submitText={$t('yes')}
cancelText={$t('no')}
{onClose}
{onSubmit}
>
<div class="flex items-center justify-center gap-2 py-4 md:h-36">
{#if !choosePersonToMerge}
<div class="flex h-20 w-20 items-center px-1 md:h-24 md:w-24 md:px-2">
<ImageThumbnail
circle
shadow
url={getPeopleThumbnailUrl(personToMerge)}
altText={personToMerge.name}
widthStyle="100%"
/>
</div>
<div class="grid grid-rows-3">
<div></div>
<div class="flex flex-col h-full items-center justify-center">
<div class="flex h-full items-center justify-center">
<Icon icon={mdiCallMerge} size="48" class="rotate-90 dark:text-white" />
</div>
</div>
<div>
<IconButton
shape="round"
color="secondary"
variant="ghost"
aria-label={$t('swap_merge_direction')}
icon={mdiSwapHorizontal}
onclick={() => ([personToMerge, personToBeMergedInto] = [personToBeMergedInto, personToMerge])}
/>
</div>
</div>
<div class="grid grid-rows-3">
<div></div>
<div class="flex flex-col h-full items-center justify-center">
<div class="flex h-full items-center justify-center">
<Icon icon={mdiCallMerge} size="48" class="rotate-90 dark:text-white" />
</div>
</div>
<div>
<IconButton
shape="round"
color="secondary"
variant="ghost"
aria-label={$t('swap_merge_direction')}
icon={mdiSwapHorizontal}
onclick={() => ([personToMerge, personToBeMergedInto] = [personToBeMergedInto, personToMerge])}
/>
<button
type="button"
disabled={potentialMergePeople.length === 0}
class="flex h-28 w-28 items-center rounded-full border-2 border-immich-primary px-1 dark:border-immich-dark-primary md:h-32 md:w-32 md:px-2"
onclick={() => {
if (potentialMergePeople.length > 0) {
choosePersonToMerge = !choosePersonToMerge;
}
}}
>
<ImageThumbnail
border={potentialMergePeople.length > 0}
circle
shadow
url={getPeopleThumbnailUrl(personToBeMergedInto)}
altText={personToBeMergedInto.name}
widthStyle="100%"
/>
</button>
{:else}
<div class="grid w-full grid-cols-1 gap-2">
<div class="px-2">
<button type="button" onclick={() => (choosePersonToMerge = false)}> <Icon icon={mdiArrowLeft} /></button>
</div>
<div class="flex items-center justify-center">
<div class="flex flex-wrap justify-center md:grid md:grid-cols-{potentialMergePeople.length}">
{#each potentialMergePeople as person (person.id)}
<div class="h-24 w-24 md:h-28 md:w-28">
<button type="button" class="p-2 w-full" onclick={() => changePersonToMerge(person)}>
<ImageThumbnail
border={true}
circle
shadow
url={getPeopleThumbnailUrl(person)}
altText={person.name}
widthStyle="100%"
/>
</button>
</div>
{/each}
</div>
</div>
</div>
{/if}
</div>
<button
type="button"
disabled={potentialMergePeople.length === 0}
class="flex h-28 w-28 items-center rounded-full border-2 border-immich-primary px-1 dark:border-immich-dark-primary md:h-32 md:w-32 md:px-2"
onclick={() => {
if (potentialMergePeople.length > 0) {
choosePersonToMerge = !choosePersonToMerge;
}
}}
>
<ImageThumbnail
border={potentialMergePeople.length > 0}
circle
shadow
url={getPeopleThumbnailUrl(personToBeMergedInto)}
altText={personToBeMergedInto.name}
widthStyle="100%"
/>
</button>
{:else}
<div class="grid w-full grid-cols-1 gap-2">
<div class="px-2">
<button type="button" onclick={() => (choosePersonToMerge = false)}> <Icon icon={mdiArrowLeft} /></button>
</div>
<div class="flex items-center justify-center">
<div class="flex flex-wrap justify-center md:grid md:grid-cols-{potentialMergePeople.length}">
{#each potentialMergePeople as person (person.id)}
<div class="h-24 w-24 md:h-28 md:w-28">
<button type="button" class="p-2 w-full" onclick={() => changePersonToMerge(person)}>
<ImageThumbnail
border={true}
circle
shadow
url={getPeopleThumbnailUrl(person)}
altText={person.name}
widthStyle="100%"
/>
</button>
</div>
{/each}
</div>
</div>
</div>
{/if}
</div>
<div class="flex px-4 md:pt-4">
<h1 class="text-xl text-gray-500 dark:text-gray-300">{$t('are_these_the_same_person')}</h1>
</div>
<div class="flex px-4 pt-2">
<p class="text-sm text-gray-500 dark:text-gray-300">{$t('they_will_be_merged_together')}</p>
</div>
</ModalBody>
<ModalFooter>
<HStack fullWidth>
<Button fullWidth shape="round" color="secondary" onclick={() => onClose()}>{$t('no')}</Button>
<Button id="merge-confirm-button" fullWidth shape="round" onclick={handleMergePerson}>
{$t('yes')}
</Button>
</HStack>
</ModalFooter>
</Modal>
<div class="flex px-4 md:pt-4">
<h1 class="text-xl text-gray-500 dark:text-gray-300">{$t('are_these_the_same_person')}</h1>
</div>
<div class="flex px-4 pt-2">
<p class="text-sm text-gray-500 dark:text-gray-300">{$t('they_will_be_merged_together')}</p>
</div>
</FormModal>

View File

@@ -2,7 +2,7 @@
import { user } from '$lib/stores/user.store';
import { handleError } from '$lib/utils/handle-error';
import { createProfileImage, type AssetResponseDto } from '@immich/sdk';
import { Button, Modal, ModalBody, ModalFooter, toastManager } from '@immich/ui';
import { FormModal, toastManager } from '@immich/ui';
import domtoimage from 'dom-to-image';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
@@ -50,7 +50,7 @@
return false;
};
const handleSetProfilePicture = async () => {
const onSubmit = async () => {
if (!imgElement) {
return;
}
@@ -72,24 +72,20 @@
toastManager.success($t('profile_picture_set'));
$user.profileImagePath = profileImagePath;
$user.profileChangedAt = profileChangedAt;
onClose();
} catch (error) {
handleError(error, $t('errors.unable_to_set_profile_picture'));
}
onClose();
};
</script>
<Modal size="small" title={$t('set_profile_picture')} {onClose}>
<ModalBody>
<div class="flex place-items-center items-center justify-center">
<div
class="relative flex aspect-square w-62.5 overflow-hidden rounded-full border-4 border-immich-primary bg-immich-dark-primary dark:border-immich-dark-primary dark:bg-immich-primary"
>
<PhotoViewer bind:element={imgElement} cursor={{ current: asset }} />
</div>
<FormModal size="small" title={$t('set_profile_picture')} {onClose} {onSubmit}>
<div class="flex place-items-center items-center justify-center">
<div
class="relative flex aspect-square w-62.5 overflow-hidden rounded-full border-4 border-immich-primary bg-immich-dark-primary dark:border-immich-dark-primary dark:bg-immich-primary"
>
<PhotoViewer bind:element={imgElement} cursor={{ current: asset }} />
</div>
</ModalBody>
<ModalFooter>
<Button fullWidth shape="round" onclick={handleSetProfilePicture}>{$t('set_as_profile_picture')}</Button>
</ModalFooter>
</Modal>
</div>
</FormModal>

View File

@@ -38,7 +38,7 @@
onClose={handleClose}
{disabled}
>
{#snippet promptSnippet()}
{#snippet prompt()}
<div class="flex flex-col gap-4">
<Text>
{#if force}

View File

@@ -33,7 +33,7 @@
size="small"
onClose={handleClose}
>
{#snippet promptSnippet()}
{#snippet prompt()}
<p>
<FormatMessage key="admin.user_restore_description" values={{ user: user.name }}>
{#snippet children({ message })}

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import FormatMessage from '$lib/elements/FormatMessage.svelte';
import { Button, Modal, ModalBody, ModalFooter } from '@immich/ui';
import { BasicModal } from '@immich/ui';
import { t } from 'svelte-i18n';
type Props = {
@@ -12,33 +12,33 @@
const { serverVersion, releaseVersion, onClose }: Props = $props();
</script>
<Modal size="small" title="🎉 {$t('new_version_available')}" {onClose} icon={false}>
<ModalBody>
<div>
<FormatMessage key="version_announcement_message">
{#snippet children({ tag, message })}
{#if tag === 'link'}
<span class="font-medium underline">
<a href="https://github.com/immich-app/immich/releases/latest" target="_blank" rel="noopener noreferrer">
{message}
</a>
</span>
{:else if tag === 'code'}
<code>{message}</code>
{/if}
{/snippet}
</FormatMessage>
</div>
<BasicModal
size="small"
title="🎉 {$t('new_version_available')}"
closeText={$t('acknowledge')}
closeColor="primary"
{onClose}
icon={false}
>
<FormatMessage key="version_announcement_message">
{#snippet children({ tag, message })}
{#if tag === 'link'}
<span class="font-medium underline">
<a href="https://github.com/immich-app/immich/releases/latest" target="_blank" rel="noopener noreferrer">
{message}
</a>
</span>
{:else if tag === 'code'}
<code>{message}</code>
{/if}
{/snippet}
</FormatMessage>
<div class="mt-4 font-medium">{$t('version_announcement_closing')}</div>
<div class="mt-4 font-medium">{$t('version_announcement_closing')}</div>
<div class="font-sm mt-8">
<code>{$t('server_version')}: {serverVersion}</code>
<br />
<code>{$t('latest_version')}: {releaseVersion}</code>
</div>
</ModalBody>
<ModalFooter>
<Button fullWidth shape="round" onclick={onClose}>{$t('acknowledge')}</Button>
</ModalFooter>
</Modal>
<div class="font-sm mt-8">
<code>{$t('server_version')}: {serverVersion}</code>
<br />
<code>{$t('latest_version')}: {releaseVersion}</code>
</div>
</BasicModal>

View File

@@ -5,19 +5,7 @@
import { handleCreateUserAdmin } from '$lib/services/user-admin.service';
import { userInteraction } from '$lib/stores/user.svelte';
import { ByteUnit, convertToBytes } from '$lib/utils/byte-units';
import {
Button,
Field,
HelperText,
HStack,
Input,
Modal,
ModalBody,
ModalFooter,
PasswordInput,
Stack,
Switch,
} from '@immich/ui';
import { Field, FormModal, HelperText, Input, PasswordInput, Stack, Switch } from '@immich/ui';
import { t } from 'svelte-i18n';
let success = $state(false);
@@ -73,61 +61,48 @@
};
</script>
<Modal title={$t('create_new_user')} {onClose} size="small">
<ModalBody>
<form onsubmit={onSubmit} autocomplete="off" id="create-new-user-form">
{#if success}
<p class="text-sm text-immich-primary">{$t('new_user_created')}</p>
<FormModal title={$t('create_new_user')} size="small" disabled={!valid} submitText={$t('create')} {onClose} {onSubmit}>
{#if success}
<p class="text-sm text-immich-primary">{$t('new_user_created')}</p>
{/if}
<Stack gap={4}>
<Field label={$t('email')} required>
<Input bind:value={email} type="email" />
</Field>
{#if featureFlagsManager.value.email}
<Field label={$t('admin.send_welcome_email')}>
<Switch id="send-welcome-email" bind:checked={notify} class="text-sm" />
</Field>
{/if}
<Field label={$t('password')} required={!featureFlagsManager.value.oauth}>
<PasswordInput id="password" bind:value={password} autocomplete="new-password" />
</Field>
<Field label={$t('confirm_password')} required={!featureFlagsManager.value.oauth}>
<PasswordInput id="confirmPassword" bind:value={passwordConfirm} autocomplete="new-password" />
<HelperText color="danger">{passwordMismatchMessage}</HelperText>
</Field>
<Field label={$t('admin.require_password_change_on_login')}>
<Switch id="require-password-change" bind:checked={shouldChangePassword} class="text-sm text-start" />
</Field>
<Field label={$t('name')} required>
<Input bind:value={name} />
</Field>
<Field label={$t('admin.quota_size_gib')}>
<Input bind:value={quotaSize} type="number" placeholder={$t('unlimited')} min="0" step="1" />
{#if quotaSizeWarning}
<HelperText color="danger">{$t('errors.quota_higher_than_disk_size')}</HelperText>
{/if}
</Field>
<Stack gap={4}>
<Field label={$t('email')} required>
<Input bind:value={email} type="email" />
</Field>
{#if featureFlagsManager.value.email}
<Field label={$t('admin.send_welcome_email')}>
<Switch id="send-welcome-email" bind:checked={notify} class="text-sm" />
</Field>
{/if}
<Field label={$t('password')} required={!featureFlagsManager.value.oauth}>
<PasswordInput id="password" bind:value={password} autocomplete="new-password" />
</Field>
<Field label={$t('confirm_password')} required={!featureFlagsManager.value.oauth}>
<PasswordInput id="confirmPassword" bind:value={passwordConfirm} autocomplete="new-password" />
<HelperText color="danger">{passwordMismatchMessage}</HelperText>
</Field>
<Field label={$t('admin.require_password_change_on_login')}>
<Switch id="require-password-change" bind:checked={shouldChangePassword} class="text-sm text-start" />
</Field>
<Field label={$t('name')} required>
<Input bind:value={name} />
</Field>
<Field label={$t('admin.quota_size_gib')}>
<Input bind:value={quotaSize} type="number" placeholder={$t('unlimited')} min="0" step="1" />
{#if quotaSizeWarning}
<HelperText color="danger">{$t('errors.quota_higher_than_disk_size')}</HelperText>
{/if}
</Field>
<Field label={$t('admin.admin_user')}>
<Switch bind:checked={isAdmin} />
</Field>
</Stack>
</form>
</ModalBody>
<ModalFooter>
<HStack fullWidth>
<Button color="secondary" fullWidth onclick={() => onClose()} shape="round">{$t('cancel')}</Button>
<Button type="submit" disabled={!valid} fullWidth shape="round" form="create-new-user-form"
>{$t('create')}
</Button>
</HStack>
</ModalFooter>
</Modal>
<Field label={$t('admin.admin_user')}>
<Switch bind:checked={isAdmin} />
</Field>
</Stack>
</FormModal>

View File

@@ -5,19 +5,7 @@
import { user as authUser } from '$lib/stores/user.store';
import { userInteraction } from '$lib/stores/user.svelte';
import { ByteUnit, convertFromBytes, convertToBytes } from '$lib/utils/byte-units';
import {
Button,
Field,
HStack,
Input,
Link,
Modal,
ModalBody,
ModalFooter,
NumberInput,
Switch,
Text,
} from '@immich/ui';
import { Field, FormModal, Input, Link, NumberInput, Switch, Text } from '@immich/ui';
import { mdiAccountEditOutline } from '@mdi/js';
import { t } from 'svelte-i18n';
import type { PageData } from './$types';
@@ -69,49 +57,36 @@
};
</script>
<Modal title={$t('edit_user')} size="small" icon={mdiAccountEditOutline} {onClose}>
<ModalBody>
<form onsubmit={onSubmit} autocomplete="off" id="edit-user-form">
<Field label={$t('email')} required>
<Input type="email" bind:value={email} />
</Field>
<FormModal title={$t('edit_user')} size="small" icon={mdiAccountEditOutline} {onClose} {onSubmit}>
<Field label={$t('email')} required>
<Input type="email" bind:value={email} />
</Field>
<Field label={$t('name')} required class="mt-4">
<Input bind:value={name} />
</Field>
<Field label={$t('name')} required class="mt-4">
<Input bind:value={name} />
</Field>
<Field label={$t('admin.quota_size_gib')} class="mt-4">
<NumberInput bind:value={quotaSize} min="0" step="1" placeholder={$t('unlimited')} />
{#if quotaSizeWarning}
<Text size="small" color="danger">{$t('errors.quota_higher_than_disk_size')}</Text>
{/if}
</Field>
<Field label={$t('admin.quota_size_gib')} class="mt-4">
<NumberInput bind:value={quotaSize} min="0" step="1" placeholder={$t('unlimited')} />
{#if quotaSizeWarning}
<Text size="small" color="danger">{$t('errors.quota_higher_than_disk_size')}</Text>
{/if}
</Field>
<Field label={$t('storage_label')} class="mt-4">
<Input bind:value={storageLabel} />
</Field>
<Field label={$t('storage_label')} class="mt-4">
<Input bind:value={storageLabel} />
</Field>
<Text size="small" class="mt-2" color="muted">
{$t('admin.note_apply_storage_label_previous_assets')}
<Link href={AppRoute.ADMIN_QUEUES}>
{$t('admin.storage_template_migration_job')}
</Link>
</Text>
<Text size="small" class="mt-2" color="muted">
{$t('admin.note_apply_storage_label_previous_assets')}
<Link href={AppRoute.ADMIN_QUEUES}>
{$t('admin.storage_template_migration_job')}
</Link>
</Text>
{#if user.id !== $authUser.id}
<Field label={$t('admin.admin_user')}>
<Switch bind:checked={isAdmin} class="mt-4" />
</Field>
{/if}
</form>
</ModalBody>
<ModalFooter>
<HStack fullWidth>
<Button shape="round" color="secondary" fullWidth form="edit-user-form" onclick={() => onClose()}
>{$t('cancel')}</Button
>
<Button type="submit" shape="round" fullWidth form="edit-user-form">{$t('confirm')}</Button>
</HStack>
</ModalFooter>
</Modal>
{#if user.id !== $authUser.id}
<Field label={$t('admin.admin_user')}>
<Switch bind:checked={isAdmin} class="mt-4" />
</Field>
{/if}
</FormModal>