Merge branch 'main' into lighter_buckets_web

This commit is contained in:
Min Idzelis
2025-05-02 19:19:46 -04:00
committed by GitHub
24 changed files with 333 additions and 319 deletions
@@ -47,8 +47,7 @@
<ConfirmDialog
title={$t('delete_user')}
confirmText={forceDelete ? $t('permanently_delete') : $t('delete')}
onConfirm={handleDeleteUser}
{onCancel}
onClose={(confirmed) => (confirmed ? handleDeleteUser() : onCancel())}
disabled={deleteButtonDisabled}
>
{#snippet promptSnippet()}
@@ -33,8 +33,7 @@
title={$t('restore_user')}
confirmText={$t('continue')}
confirmColor="success"
onConfirm={handleRestoreUser}
{onCancel}
onClose={(confirmed) => (confirmed ? handleRestoreUser() : onCancel())}
>
{#snippet promptSnippet()}
<p>
@@ -49,8 +49,7 @@
{#if isConfirmOpen}
<ConfirmDialog
title={$t('admin.disable_login')}
onCancel={() => (isConfirmOpen = false)}
onConfirm={() => handleSave(true)}
onClose={(confirmed) => (confirmed ? handleSave(true) : (isConfirmOpen = false))}
>
{#snippet promptSnippet()}
<div class="flex flex-col gap-4">
@@ -1,27 +1,27 @@
<script lang="ts">
import Icon from '$lib/components/elements/icon.svelte';
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
import ConfirmDialog from '$lib/components/shared-components/dialog/confirm-dialog.svelte';
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
import { handleError } from '$lib/utils/handle-error';
import {
updateAlbumInfo,
AlbumUserRole,
AssetOrder,
removeUserFromAlbum,
updateAlbumInfo,
updateAlbumUser,
type AlbumResponseDto,
type UserResponseDto,
AssetOrder,
AlbumUserRole,
updateAlbumUser,
} from '@immich/sdk';
import { mdiArrowDownThin, mdiArrowUpThin, mdiPlus, mdiDotsVertical } from '@mdi/js';
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import SettingDropdown from '../shared-components/settings/setting-dropdown.svelte';
import type { RenderedOption } from '../elements/dropdown.svelte';
import { handleError } from '$lib/utils/handle-error';
import { mdiArrowDownThin, mdiArrowUpThin, mdiDotsVertical, mdiPlus } from '@mdi/js';
import { findKey } from 'lodash-es';
import { t } from 'svelte-i18n';
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
import ConfirmDialog from '$lib/components/shared-components/dialog/confirm-dialog.svelte';
import type { RenderedOption } from '../elements/dropdown.svelte';
import { notificationController, NotificationType } from '../shared-components/notification/notification';
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
import SettingDropdown from '../shared-components/settings/setting-dropdown.svelte';
interface Props {
album: AlbumResponseDto;
@@ -195,7 +195,6 @@
title={$t('album_remove_user')}
prompt={$t('album_remove_user_confirmation', { values: { user: selectedRemoveUser.name } })}
confirmText={$t('remove_user')}
onConfirm={handleRemoveUser}
onCancel={() => (selectedRemoveUser = null)}
onClose={(confirmed) => (confirmed ? handleRemoveUser() : (selectedRemoveUser = null))}
/>
{/if}
@@ -1,22 +1,22 @@
<script lang="ts">
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
import {
AlbumUserRole,
getMyUser,
removeUserFromAlbum,
updateAlbumUser,
type AlbumResponseDto,
type UserResponseDto,
updateAlbumUser,
AlbumUserRole,
} from '@immich/sdk';
import { mdiDotsVertical } from '@mdi/js';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
import { handleError } from '../../utils/handle-error';
import ConfirmDialog from '../shared-components/dialog/confirm-dialog.svelte';
import MenuOption from '../shared-components/context-menu/menu-option.svelte';
import ConfirmDialog from '../shared-components/dialog/confirm-dialog.svelte';
import { NotificationType, notificationController } from '../shared-components/notification/notification';
import UserAvatar from '../shared-components/user-avatar.svelte';
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
import { t } from 'svelte-i18n';
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
interface Props {
album: AlbumResponseDto;
@@ -144,8 +144,7 @@
title={$t('album_leave')}
prompt={$t('album_leave_confirmation', { values: { album: album.albumName } })}
confirmText={$t('leave')}
onConfirm={handleRemoveUser}
onCancel={() => (selectedRemoveUser = null)}
onClose={(confirmed) => (confirmed ? handleRemoveUser() : (selectedRemoveUser = null))}
/>
{/if}
@@ -154,7 +153,6 @@
title={$t('album_remove_user')}
prompt={$t('album_remove_user_confirmation', { values: { user: selectedRemoveUser.name } })}
confirmText={$t('remove_user')}
onConfirm={handleRemoveUser}
onCancel={() => (selectedRemoveUser = null)}
onClose={(confirmed) => (confirmed ? handleRemoveUser() : (selectedRemoveUser = null))}
/>
{/if}
@@ -1,13 +1,13 @@
<script lang="ts">
import { shortcut } from '$lib/actions/shortcut';
import ConfirmDialog from '$lib/components/shared-components/dialog/confirm-dialog.svelte';
import { editTypes, showCancelConfirmDialog } from '$lib/stores/asset-editor.store';
import { websocketEvents } from '$lib/stores/websocket';
import { type AssetResponseDto } from '@immich/sdk';
import { mdiClose } from '@mdi/js';
import { onMount } from 'svelte';
import CircleIconButton from '../../elements/buttons/circle-icon-button.svelte';
import { t } from 'svelte-i18n';
import { editTypes, showCancelConfirmDialog } from '$lib/stores/asset-editor.store';
import ConfirmDialog from '$lib/components/shared-components/dialog/confirm-dialog.svelte';
import { shortcut } from '$lib/actions/shortcut';
import CircleIconButton from '../../elements/buttons/circle-icon-button.svelte';
onMount(() => {
return websocketEvents.on('on_asset_update', (assetUpdate) => {
@@ -31,10 +31,13 @@
setTimeout(() => {
onUpdateSelectedType(selectedType);
}, 1);
function selectType(name: string) {
selectedType = name;
onUpdateSelectedType(selectedType);
}
const onConfirm = () => (typeof $showCancelConfirmDialog === 'boolean' ? null : $showCancelConfirmDialog());
</script>
<svelte:window use:shortcut={{ shortcut: { key: 'Escape' }, onShortcut: onClose }} />
@@ -71,9 +74,6 @@
cancelColor="secondary"
confirmColor="danger"
confirmText={$t('close')}
onCancel={() => {
$showCancelConfirmDialog = false;
}}
onConfirm={() => (typeof $showCancelConfirmDialog === 'boolean' ? null : $showCancelConfirmDialog())}
onClose={(confirmed) => (confirmed ? onConfirm() : ($showCancelConfirmDialog = false))}
/>
{/if}
@@ -1,21 +1,29 @@
<script lang="ts">
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
import { featureFlags } from '$lib/stores/server-config.store';
import { userInteraction } from '$lib/stores/user.svelte';
import { ByteUnit, convertToBytes } from '$lib/utils/byte-units';
import { handleError } from '$lib/utils/handle-error';
import { createUserAdmin } from '@immich/sdk';
import { Alert, Button, Field, HelperText, Input, PasswordInput, Stack, Switch } from '@immich/ui';
import { createUserAdmin, type UserAdminResponseDto } from '@immich/sdk';
import {
Alert,
Button,
Field,
HelperText,
Input,
Modal,
ModalBody,
ModalFooter,
PasswordInput,
Stack,
Switch,
} from '@immich/ui';
import { t } from 'svelte-i18n';
interface Props {
onClose: () => void;
onSubmit: () => void;
onCancel: () => void;
oauthEnabled?: boolean;
onClose: (user?: UserAdminResponseDto) => void;
}
let { onClose, onSubmit: onDone, onCancel, oauthEnabled = false }: Props = $props();
let { onClose }: Props = $props();
let error = $state('');
let success = $state(false);
@@ -50,7 +58,7 @@
error = '';
try {
await createUserAdmin({
const user = await createUserAdmin({
userAdminCreateDto: {
email,
password,
@@ -63,8 +71,7 @@
success = true;
onDone();
onClose(user);
return;
} catch (error) {
handleError(error, $t('errors.unable_to_create_user'));
@@ -74,55 +81,60 @@
};
</script>
<form onsubmit={onSubmit} autocomplete="off" id="create-new-user-form">
<FullScreenModal title={$t('create_new_user')} showLogo {onClose}>
{#if error}
<Alert color="danger" size="small" title={error} closable />
{/if}
{#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 $featureFlags.email}
<Field label={$t('admin.send_welcome_email')}>
<Switch id="send-welcome-email" bind:checked={notify} class="text-sm" />
</Field>
<Modal title={$t('create_new_user')} {onClose} size="small" class="text-dark bg-light">
<ModalBody>
<form onsubmit={onSubmit} autocomplete="off" id="create-new-user-form">
{#if error}
<Alert color="danger" size="small" title={error} closable />
{/if}
<Field label={$t('password')} required={!oauthEnabled}>
<PasswordInput id="password" bind:value={password} autocomplete="new-password" />
</Field>
{#if success}
<p class="text-sm text-immich-primary">{$t('new_user_created')}</p>
{/if}
<Field label={$t('confirm_password')} required={!oauthEnabled}>
<PasswordInput id="confirmPassword" bind:value={passwordConfirm} autocomplete="new-password" />
<HelperText color="danger">{passwordMismatchMessage}</HelperText>
</Field>
<Stack gap={4}>
<Field label={$t('email')} required>
<Input bind:value={email} type="email" />
</Field>
<Field label={$t('admin.require_password_change_on_login')}>
<Switch id="require-password-change" bind:checked={shouldChangePassword} class="text-sm" />
</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" />
{#if quotaSizeWarning}
<HelperText color="danger">{$t('errors.quota_higher_than_disk_size')}</HelperText>
{#if $featureFlags.email}
<Field label={$t('admin.send_welcome_email')}>
<Switch id="send-welcome-email" bind:checked={notify} class="text-sm" />
</Field>
{/if}
</Field>
</Stack>
{#snippet stickyBottom()}
<Button color="secondary" fullWidth onclick={onCancel} shape="round">{$t('cancel')}</Button>
<Button type="submit" disabled={!valid} fullWidth shape="round">{$t('create')}</Button>
{/snippet}
</FullScreenModal>
</form>
<Field label={$t('password')} required={!$featureFlags.oauth}>
<PasswordInput id="password" bind:value={password} autocomplete="new-password" />
</Field>
<Field label={$t('confirm_password')} required={!$featureFlags.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" />
{#if quotaSizeWarning}
<HelperText color="danger">{$t('errors.quota_higher_than_disk_size')}</HelperText>
{/if}
</Field>
</Stack>
</form>
</ModalBody>
<ModalFooter>
<div class="flex gap-3 w-full">
<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
>
</div>
</ModalFooter>
</Modal>
@@ -1,34 +1,26 @@
<script lang="ts">
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
import { AppRoute } from '$lib/constants';
import { modalManager } from '$lib/managers/modal-manager.svelte';
import { userInteraction } from '$lib/stores/user.svelte';
import { ByteUnit, convertFromBytes, convertToBytes } from '$lib/utils/byte-units';
import { handleError } from '$lib/utils/handle-error';
import { updateUserAdmin, type UserAdminResponseDto } from '@immich/sdk';
import { Button } from '@immich/ui';
import { Button, Modal, ModalBody, ModalFooter } from '@immich/ui';
import { mdiAccountEditOutline } from '@mdi/js';
import { t } from 'svelte-i18n';
interface Props {
user: UserAdminResponseDto;
canResetPassword?: boolean;
newPassword: string;
onClose: () => void;
onResetPasswordSuccess: () => void;
onEditSuccess: () => void;
onClose: (
data?: { action: 'update'; data: UserAdminResponseDto } | { action: 'resetPassword'; data: string },
) => void;
}
let {
user,
canResetPassword = true,
newPassword = $bindable(),
onClose,
onResetPasswordSuccess,
onEditSuccess,
}: Props = $props();
let { user, canResetPassword = true, onClose }: Props = $props();
let quotaSize = $state(user.quotaSizeInBytes === null ? null : convertFromBytes(user.quotaSizeInBytes, ByteUnit.GiB));
let newPassword = $state<string>('');
const previousQutoa = user.quotaSizeInBytes;
@@ -42,7 +34,7 @@
const editUser = async () => {
try {
const { id, email, name, storageLabel } = user;
await updateUserAdmin({
const newUser = await updateUserAdmin({
id,
userAdminUpdateDto: {
email,
@@ -52,14 +44,14 @@
},
});
onEditSuccess();
onClose({ action: 'update', data: newUser });
} catch (error) {
handleError(error, $t('errors.unable_to_update_user'));
}
};
const resetPassword = async () => {
const isConfirmed = await dialogController.show({
const isConfirmed = await modalManager.openDialog({
prompt: $t('admin.confirm_user_password_reset', { values: { user: user.name } }),
});
@@ -78,7 +70,7 @@
},
});
onResetPasswordSuccess();
onClose({ action: 'resetPassword', data: newPassword });
} catch (error) {
handleError(error, $t('errors.unable_to_reset_password'));
}
@@ -107,61 +99,65 @@
};
</script>
<FullScreenModal title={$t('edit_user')} icon={mdiAccountEditOutline} {onClose}>
<form onsubmit={onSubmit} autocomplete="off" id="edit-user-form">
<div class="my-4 flex flex-col gap-2">
<label class="immich-form-label" for="email">{$t('email')}</label>
<input class="immich-form-input" id="email" name="email" type="email" bind:value={user.email} />
<Modal title={$t('edit_user')} size="small" icon={mdiAccountEditOutline} {onClose}>
<ModalBody>
<form onsubmit={onSubmit} autocomplete="off" id="edit-user-form">
<div class="my-4 flex flex-col gap-2">
<label class="immich-form-label" for="email">{$t('email')}</label>
<input class="immich-form-input" id="email" name="email" type="email" bind:value={user.email} />
</div>
<div class="my-4 flex flex-col gap-2">
<label class="immich-form-label" for="name">{$t('name')}</label>
<input class="immich-form-input" id="name" name="name" type="text" required bind:value={user.name} />
</div>
<div class="my-4 flex flex-col gap-2">
<label class="flex items-center gap-2 immich-form-label" for="quotaSize">
{$t('admin.quota_size_gib')}
{#if quotaSizeWarning}
<p class="text-red-400 text-sm">{$t('errors.quota_higher_than_disk_size')}</p>
{/if}</label
>
<input
class="immich-form-input"
id="quotaSize"
name="quotaSize"
placeholder={$t('unlimited')}
type="number"
min="0"
bind:value={quotaSize}
/>
</div>
<div class="my-4 flex flex-col gap-2">
<label class="immich-form-label" for="storage-label">{$t('storage_label')}</label>
<input
class="immich-form-input"
id="storage-label"
name="storage-label"
type="text"
bind:value={user.storageLabel}
/>
<p>
{$t('admin.note_apply_storage_label_previous_assets')}
<a href={AppRoute.ADMIN_JOBS} class="text-immich-primary dark:text-immich-dark-primary">
{$t('admin.storage_template_migration_job')}
</a>
</p>
</div>
</form>
</ModalBody>
<ModalFooter>
<div class="flex gap-3 w-full">
{#if canResetPassword}
<Button shape="round" color="warning" variant="filled" fullWidth onclick={resetPassword}
>{$t('reset_password')}</Button
>
{/if}
<Button type="submit" shape="round" fullWidth form="edit-user-form">{$t('confirm')}</Button>
</div>
<div class="my-4 flex flex-col gap-2">
<label class="immich-form-label" for="name">{$t('name')}</label>
<input class="immich-form-input" id="name" name="name" type="text" required bind:value={user.name} />
</div>
<div class="my-4 flex flex-col gap-2">
<label class="flex items-center gap-2 immich-form-label" for="quotaSize">
{$t('admin.quota_size_gib')}
{#if quotaSizeWarning}
<p class="text-red-400 text-sm">{$t('errors.quota_higher_than_disk_size')}</p>
{/if}</label
>
<input
class="immich-form-input"
id="quotaSize"
name="quotaSize"
placeholder={$t('unlimited')}
type="number"
min="0"
bind:value={quotaSize}
/>
</div>
<div class="my-4 flex flex-col gap-2">
<label class="immich-form-label" for="storage-label">{$t('storage_label')}</label>
<input
class="immich-form-input"
id="storage-label"
name="storage-label"
type="text"
bind:value={user.storageLabel}
/>
<p>
{$t('admin.note_apply_storage_label_previous_assets')}
<a href={AppRoute.ADMIN_JOBS} class="text-immich-primary dark:text-immich-dark-primary">
{$t('admin.storage_template_migration_job')}
</a>
</p>
</div>
</form>
{#snippet stickyBottom()}
{#if canResetPassword}
<Button shape="round" color="warning" variant="filled" fullWidth onclick={resetPassword}
>{$t('reset_password')}</Button
>
{/if}
<Button type="submit" shape="round" fullWidth form="edit-user-form">{$t('confirm')}</Button>
{/snippet}
</FullScreenModal>
</ModalFooter>
</Modal>
@@ -1,9 +1,9 @@
<script lang="ts">
import ConfirmDialog from '../shared-components/dialog/confirm-dialog.svelte';
import { showDeleteModal } from '$lib/stores/preferences.store';
import Checkbox from '$lib/components/elements/checkbox.svelte';
import { t } from 'svelte-i18n';
import FormatMessage from '$lib/components/i18n/format-message.svelte';
import { showDeleteModal } from '$lib/stores/preferences.store';
import { t } from 'svelte-i18n';
import ConfirmDialog from '../shared-components/dialog/confirm-dialog.svelte';
interface Props {
size: number;
@@ -26,8 +26,7 @@
<ConfirmDialog
title={$t('permanently_delete_assets_count', { values: { count: size } })}
confirmText={$t('delete')}
onConfirm={handleConfirm}
{onCancel}
onClose={(confirmed) => (confirmed ? handleConfirm() : onCancel())}
>
{#snippet promptSnippet()}
<p>
@@ -1,9 +1,9 @@
<script lang="ts">
import { DateTime } from 'luxon';
import ConfirmDialog from './dialog/confirm-dialog.svelte';
import Combobox, { type ComboBoxOption } from './combobox.svelte';
import DateInput from '../elements/date-input.svelte';
import { t } from 'svelte-i18n';
import DateInput from '../elements/date-input.svelte';
import Combobox, { type ComboBoxOption } from './combobox.svelte';
import ConfirmDialog from './dialog/confirm-dialog.svelte';
interface Props {
initialDate?: DateTime;
@@ -138,8 +138,7 @@
title={$t('edit_date_and_time')}
prompt="Please select a new date:"
disabled={!date.isValid}
onConfirm={handleConfirm}
{onCancel}
onClose={(confirmed) => (confirmed ? handleConfirm() : onCancel())}
>
<!-- @migration-task: migrate this slot by hand, `prompt` would shadow a prop on the parent component -->
<!-- @migration-task: migrate this slot by hand, `prompt` would shadow a prop on the parent component -->
@@ -1,20 +1,20 @@
<script lang="ts">
import ConfirmDialog from './dialog/confirm-dialog.svelte';
import { timeDebounceOnSearch } from '$lib/constants';
import { handleError } from '$lib/utils/handle-error';
import { lastChosenLocation } from '$lib/stores/asset-editor.store';
import { handleError } from '$lib/utils/handle-error';
import ConfirmDialog from './dialog/confirm-dialog.svelte';
import { clickOutside } from '$lib/actions/click-outside';
import LoadingSpinner from './loading-spinner.svelte';
import { delay } from '$lib/utils/asset-utils';
import { timeToLoadTheMap } from '$lib/constants';
import { searchPlaces, type AssetResponseDto, type PlacesResponseDto } from '@immich/sdk';
import SearchBar from '../elements/search-bar.svelte';
import { listNavigation } from '$lib/actions/list-navigation';
import { t } from 'svelte-i18n';
import CoordinatesInput from '$lib/components/shared-components/coordinates-input.svelte';
import type Map from '$lib/components/shared-components/map/map.svelte';
import { timeToLoadTheMap } from '$lib/constants';
import { delay } from '$lib/utils/asset-utils';
import { searchPlaces, type AssetResponseDto, type PlacesResponseDto } from '@immich/sdk';
import { t } from 'svelte-i18n';
import { get } from 'svelte/store';
import SearchBar from '../elements/search-bar.svelte';
import LoadingSpinner from './loading-spinner.svelte';
interface Point {
lng: number;
lat: number;
@@ -112,7 +112,12 @@
};
</script>
<ConfirmDialog confirmColor="primary" title={$t('change_location')} width="wide" onConfirm={handleConfirm} {onCancel}>
<ConfirmDialog
confirmColor="primary"
title={$t('change_location')}
size="medium"
onClose={(confirmed) => (confirmed ? handleConfirm() : onCancel())}
>
{#snippet promptSnippet()}
<div class="flex flex-col w-full h-full gap-2">
<div class="relative w-64 sm:w-96">
@@ -1,8 +1,7 @@
<script lang="ts">
import FullScreenModal from '../full-screen-modal.svelte';
import { t } from 'svelte-i18n';
import { Button, Modal, ModalBody, ModalFooter, type Color } from '@immich/ui';
import type { Snippet } from 'svelte';
import { Button, type Color } from '@immich/ui';
import { t } from 'svelte-i18n';
interface Props {
title?: string;
@@ -13,9 +12,8 @@
cancelColor?: Color;
hideCancelButton?: boolean;
disabled?: boolean;
width?: 'wide' | 'narrow';
onCancel: () => void;
onConfirm: () => void;
size?: 'small' | 'medium';
onClose: (confirmed: boolean) => void;
promptSnippet?: Snippet;
}
@@ -28,32 +26,33 @@
cancelColor = 'secondary',
hideCancelButton = false,
disabled = false,
width = 'narrow',
onCancel,
onConfirm,
size = 'small',
onClose,
promptSnippet,
}: Props = $props();
const handleConfirm = () => {
onConfirm();
onClose(true);
};
</script>
<FullScreenModal {title} onClose={onCancel} {width}>
<div class="text-md py-5 text-center">
<Modal {title} onClose={() => onClose(false)} {size} class="bg-light text-dark">
<ModalBody>
{#if promptSnippet}{@render promptSnippet()}{:else}
<p>{prompt}</p>
{/if}
</div>
</ModalBody>
{#snippet stickyBottom()}
{#if !hideCancelButton}
<Button shape="round" color={cancelColor} fullWidth onclick={onCancel}>
{cancelText}
<ModalFooter>
<div class="flex gap-3 w-full">
{#if !hideCancelButton}
<Button shape="round" color={cancelColor} fullWidth onclick={() => onClose(false)}>
{cancelText}
</Button>
{/if}
<Button shape="round" color={confirmColor} fullWidth onclick={handleConfirm} {disabled}>
{confirmText}
</Button>
{/if}
<Button shape="round" color={confirmColor} fullWidth onclick={handleConfirm} {disabled}>
{confirmText}
</Button>
{/snippet}
</FullScreenModal>
</div>
</ModalFooter>
</Modal>
@@ -1,8 +1,7 @@
import { writable } from 'svelte/store';
type DialogActions = {
onConfirm: () => void;
onCancel: () => void;
onClose: (confirmed: boolean) => void;
};
type DialogOptions = {
@@ -24,13 +23,9 @@ function createDialogWrapper() {
return new Promise<boolean>((resolve) => {
const newDialog: Dialog = {
...options,
onConfirm: () => {
onClose: (confirmed) => {
dialog.set(undefined);
resolve(true);
},
onCancel: () => {
dialog.set(undefined);
resolve(false);
resolve(confirmed);
},
};
@@ -0,0 +1,43 @@
<script lang="ts">
import ConfirmDialog from '$lib/components/shared-components/dialog/confirm-dialog.svelte';
import { copyToClipboard } from '$lib/utils';
import { Code, IconButton, Text } from '@immich/ui';
import { mdiContentCopy } from '@mdi/js';
import { t } from 'svelte-i18n';
type Props = {
onClose: () => void;
newPassword: string;
};
const { onClose, newPassword }: Props = $props();
</script>
<ConfirmDialog
title={$t('password_reset_success')}
confirmText={$t('done')}
{onClose}
hideCancelButton={true}
confirmColor="success"
>
{#snippet promptSnippet()}
<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>
{/snippet}
</ConfirmDialog>
@@ -0,0 +1,33 @@
import ConfirmDialog from '$lib/components/shared-components/dialog/confirm-dialog.svelte';
import { mount, unmount, type Component, type ComponentProps } from 'svelte';
type OnCloseData<T> = T extends { onClose: (data: infer R) => void } ? R : never;
// TODO make `props` optional if component only has `onClose`
// type OptionalIfEmpty<T extends object> = keyof T extends never ? undefined : T;
class ModalManager {
open<T extends object, K = OnCloseData<T>>(Component: Component<T>, props: Omit<T, 'onClose'>) {
return new Promise<K>((resolve) => {
let modal: object = {};
const onClose = async (data: K) => {
await unmount(modal);
resolve(data);
};
modal = mount(Component, {
target: document.body,
props: {
...(props as T),
onClose,
},
});
});
}
openDialog(options: Omit<ComponentProps<typeof ConfirmDialog>, 'onClose'>) {
return this.open(ConfirmDialog, options);
}
}
export const modalManager = new ModalManager();