mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-04 03:27:09 -05:00 
			
		
		
		
	Merge branch 'main' into lighter_buckets_web
This commit is contained in:
		
						commit
						9d527b37f0
					
				@ -67,7 +67,7 @@ const runQuery = async (query: string) => {
 | 
			
		||||
 | 
			
		||||
const runMigrations = async () => {
 | 
			
		||||
  const configRepository = new ConfigRepository();
 | 
			
		||||
  const logger = new LoggingRepository(undefined, configRepository);
 | 
			
		||||
  const logger = LoggingRepository.create();
 | 
			
		||||
  const db = getDatabaseClient();
 | 
			
		||||
  const databaseRepository = new DatabaseRepository(db, logger, configRepository);
 | 
			
		||||
  await databaseRepository.runMigrations();
 | 
			
		||||
 | 
			
		||||
@ -142,18 +142,15 @@ export const getRepository = <K extends keyof RepositoriesTypes>(key: K, db: Kys
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    case 'database': {
 | 
			
		||||
      const configRepo = new ConfigRepository();
 | 
			
		||||
      return new DatabaseRepository(db, new LoggingRepository(undefined, configRepo), configRepo);
 | 
			
		||||
      return new DatabaseRepository(db, LoggingRepository.create(), new ConfigRepository());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    case 'email': {
 | 
			
		||||
      const logger = new LoggingRepository(undefined, new ConfigRepository());
 | 
			
		||||
      return new EmailRepository(logger);
 | 
			
		||||
      return new EmailRepository(LoggingRepository.create());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    case 'logger': {
 | 
			
		||||
      const configMock = { getEnv: () => ({ noColor: false }) };
 | 
			
		||||
      return new LoggingRepository(undefined, configMock as ConfigRepository);
 | 
			
		||||
      return LoggingRepository.create();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    case 'memory': {
 | 
			
		||||
 | 
			
		||||
@ -42,7 +42,7 @@ const globalSetup = async () => {
 | 
			
		||||
  const db = new Kysely<DB>(getKyselyConfig({ connectionType: 'url', url: postgresUrl }));
 | 
			
		||||
 | 
			
		||||
  const configRepository = new ConfigRepository();
 | 
			
		||||
  const logger = new LoggingRepository(undefined, configRepository);
 | 
			
		||||
  const logger = LoggingRepository.create();
 | 
			
		||||
  await new DatabaseRepository(db, logger, configRepository).runMigrations();
 | 
			
		||||
 | 
			
		||||
  await db.destroy();
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										8
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							@ -11,7 +11,7 @@
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@formatjs/icu-messageformat-parser": "^2.9.8",
 | 
			
		||||
        "@immich/sdk": "file:../open-api/typescript-sdk",
 | 
			
		||||
        "@immich/ui": "^0.18.1",
 | 
			
		||||
        "@immich/ui": "^0.19.0",
 | 
			
		||||
        "@mapbox/mapbox-gl-rtl-text": "0.2.3",
 | 
			
		||||
        "@mdi/js": "^7.4.47",
 | 
			
		||||
        "@photo-sphere-viewer/core": "^5.11.5",
 | 
			
		||||
@ -1320,9 +1320,9 @@
 | 
			
		||||
      "link": true
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@immich/ui": {
 | 
			
		||||
      "version": "0.18.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@immich/ui/-/ui-0.18.1.tgz",
 | 
			
		||||
      "integrity": "sha512-XWWO6OTfH3MektyxCn0hWefZyOGyWwwx/2zHinuShpxTHSyfveJ4mOkFP8DkyMz0dnvJ1EfdkPBMkld3y5R/Hw==",
 | 
			
		||||
      "version": "0.19.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@immich/ui/-/ui-0.19.0.tgz",
 | 
			
		||||
      "integrity": "sha512-XVjSUoQVIoe83pxM4q8kmlejb2xep/TZEfoGbasI7takEGKNiWEyXr5eZaXZCSVgq78fcNRr4jyWz290ZAXh7A==",
 | 
			
		||||
      "license": "GNU Affero General Public License version 3",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@mdi/js": "^7.4.47",
 | 
			
		||||
 | 
			
		||||
@ -27,7 +27,7 @@
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@formatjs/icu-messageformat-parser": "^2.9.8",
 | 
			
		||||
    "@immich/sdk": "file:../open-api/typescript-sdk",
 | 
			
		||||
    "@immich/ui": "^0.18.1",
 | 
			
		||||
    "@immich/ui": "^0.19.0",
 | 
			
		||||
    "@mapbox/mapbox-gl-rtl-text": "0.2.3",
 | 
			
		||||
    "@mdi/js": "^7.4.47",
 | 
			
		||||
    "@photo-sphere-viewer/core": "^5.11.5",
 | 
			
		||||
 | 
			
		||||
@ -8,7 +8,6 @@
 | 
			
		||||
    --immich-primary: 66 80 175;
 | 
			
		||||
    --immich-bg: 255 255 255;
 | 
			
		||||
    --immich-fg: 0 0 0;
 | 
			
		||||
    --immich-gray: 246 246 244;
 | 
			
		||||
    --immich-error: 229 115 115;
 | 
			
		||||
    --immich-success: 129 199 132;
 | 
			
		||||
    --immich-warning: 255 183 77;
 | 
			
		||||
@ -33,6 +32,7 @@
 | 
			
		||||
    --immich-ui-warning: 255 170 0;
 | 
			
		||||
    --immich-ui-info: 14 165 233;
 | 
			
		||||
    --immich-ui-default-border: 209 213 219;
 | 
			
		||||
    --immich-ui-gray: 246 246 246;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .dark {
 | 
			
		||||
@ -45,6 +45,7 @@
 | 
			
		||||
    --immich-ui-warning: 255 170 0;
 | 
			
		||||
    --immich-ui-info: 14 165 233;
 | 
			
		||||
    --immich-ui-default-border: 55 65 81;
 | 
			
		||||
    --immich-ui-gray: 33 33 33;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
        },
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										43
									
								
								web/src/lib/forms/password-reset-success.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								web/src/lib/forms/password-reset-success.svelte
									
									
									
									
									
										Normal file
									
								
							@ -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>
 | 
			
		||||
							
								
								
									
										33
									
								
								web/src/lib/managers/modal-manager.svelte.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								web/src/lib/managers/modal-manager.svelte.ts
									
									
									
									
									
										Normal file
									
								
							@ -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();
 | 
			
		||||
@ -102,8 +102,7 @@
 | 
			
		||||
    confirmColor="primary"
 | 
			
		||||
    title={$t('admin.create_job')}
 | 
			
		||||
    disabled={!selectedJob}
 | 
			
		||||
    onConfirm={handleCreate}
 | 
			
		||||
    onCancel={handleCancel}
 | 
			
		||||
    onClose={(confirmed) => (confirmed ? handleCreate() : handleCancel())}
 | 
			
		||||
  >
 | 
			
		||||
    {#snippet promptSnippet()}
 | 
			
		||||
      <form {onsubmit} autocomplete="off" id="create-tag-form" class="w-full">
 | 
			
		||||
 | 
			
		||||
@ -6,20 +6,20 @@
 | 
			
		||||
  import CreateUserForm from '$lib/components/forms/create-user-form.svelte';
 | 
			
		||||
  import EditUserForm from '$lib/components/forms/edit-user-form.svelte';
 | 
			
		||||
  import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
 | 
			
		||||
  import ConfirmDialog from '$lib/components/shared-components/dialog/confirm-dialog.svelte';
 | 
			
		||||
  import {
 | 
			
		||||
    NotificationType,
 | 
			
		||||
    notificationController,
 | 
			
		||||
  } from '$lib/components/shared-components/notification/notification';
 | 
			
		||||
  import PasswordResetSuccess from '$lib/forms/password-reset-success.svelte';
 | 
			
		||||
  import { modalManager } from '$lib/managers/modal-manager.svelte';
 | 
			
		||||
  import { locale } from '$lib/stores/preferences.store';
 | 
			
		||||
  import { featureFlags, serverConfig } from '$lib/stores/server-config.store';
 | 
			
		||||
  import { serverConfig } from '$lib/stores/server-config.store';
 | 
			
		||||
  import { user } from '$lib/stores/user.store';
 | 
			
		||||
  import { websocketEvents } from '$lib/stores/websocket';
 | 
			
		||||
  import { copyToClipboard } from '$lib/utils';
 | 
			
		||||
  import { getByteUnitString } from '$lib/utils/byte-units';
 | 
			
		||||
  import { UserStatus, searchUsersAdmin, type UserAdminResponseDto } from '@immich/sdk';
 | 
			
		||||
  import { Button, Code, IconButton, Text } from '@immich/ui';
 | 
			
		||||
  import { mdiContentCopy, mdiDeleteRestore, mdiInfinity, mdiPencilOutline, mdiTrashCanOutline } from '@mdi/js';
 | 
			
		||||
  import { Button, IconButton } from '@immich/ui';
 | 
			
		||||
  import { mdiDeleteRestore, mdiInfinity, mdiPencilOutline, mdiTrashCanOutline } from '@mdi/js';
 | 
			
		||||
  import { DateTime } from 'luxon';
 | 
			
		||||
  import { onMount } from 'svelte';
 | 
			
		||||
  import { t } from 'svelte-i18n';
 | 
			
		||||
@ -32,13 +32,9 @@
 | 
			
		||||
  let { data }: Props = $props();
 | 
			
		||||
 | 
			
		||||
  let allUsers: UserAdminResponseDto[] = $state([]);
 | 
			
		||||
  let shouldShowEditUserForm = $state(false);
 | 
			
		||||
  let shouldShowCreateUserForm = $state(false);
 | 
			
		||||
  let shouldShowPasswordResetSuccess = $state(false);
 | 
			
		||||
  let shouldShowDeleteConfirmDialog = $state(false);
 | 
			
		||||
  let shouldShowRestoreDialog = $state(false);
 | 
			
		||||
  let selectedUser = $state<UserAdminResponseDto>();
 | 
			
		||||
  let newPassword = $state('');
 | 
			
		||||
 | 
			
		||||
  const refresh = async () => {
 | 
			
		||||
    allUsers = await searchUsersAdmin({ withDeleted: true });
 | 
			
		||||
@ -65,25 +61,23 @@
 | 
			
		||||
    return DateTime.fromISO(deletedAt).plus({ days: $serverConfig.userDeleteDelay }).toJSDate();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const onUserCreated = async () => {
 | 
			
		||||
  const handleCreate = async () => {
 | 
			
		||||
    await modalManager.open(CreateUserForm, {});
 | 
			
		||||
    await refresh();
 | 
			
		||||
    shouldShowCreateUserForm = false;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const editUserHandler = (user: UserAdminResponseDto) => {
 | 
			
		||||
    selectedUser = user;
 | 
			
		||||
    shouldShowEditUserForm = true;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const onEditUserSuccess = async () => {
 | 
			
		||||
    await refresh();
 | 
			
		||||
    shouldShowEditUserForm = false;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const onEditPasswordSuccess = async () => {
 | 
			
		||||
    await refresh();
 | 
			
		||||
    shouldShowEditUserForm = false;
 | 
			
		||||
    shouldShowPasswordResetSuccess = true;
 | 
			
		||||
  const handleEdit = async (dto: UserAdminResponseDto) => {
 | 
			
		||||
    const result = await modalManager.open(EditUserForm, { user: dto, canResetPassword: dto.id !== $user.id });
 | 
			
		||||
    switch (result?.action) {
 | 
			
		||||
      case 'resetPassword': {
 | 
			
		||||
        await modalManager.open(PasswordResetSuccess, { newPassword: result.data });
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      case 'update': {
 | 
			
		||||
        await refresh();
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const deleteUserHandler = (user: UserAdminResponseDto) => {
 | 
			
		||||
@ -110,26 +104,6 @@
 | 
			
		||||
<UserPageLayout title={data.meta.title} admin>
 | 
			
		||||
  <section id="setting-content" class="flex place-content-center sm:mx-4">
 | 
			
		||||
    <section class="w-full pb-28 lg:w-[850px]">
 | 
			
		||||
      {#if shouldShowCreateUserForm}
 | 
			
		||||
        <CreateUserForm
 | 
			
		||||
          onSubmit={onUserCreated}
 | 
			
		||||
          onCancel={() => (shouldShowCreateUserForm = false)}
 | 
			
		||||
          onClose={() => (shouldShowCreateUserForm = false)}
 | 
			
		||||
          oauthEnabled={$featureFlags.oauth}
 | 
			
		||||
        />
 | 
			
		||||
      {/if}
 | 
			
		||||
 | 
			
		||||
      {#if shouldShowEditUserForm && selectedUser}
 | 
			
		||||
        <EditUserForm
 | 
			
		||||
          user={selectedUser}
 | 
			
		||||
          bind:newPassword
 | 
			
		||||
          canResetPassword={selectedUser?.id !== $user.id}
 | 
			
		||||
          onEditSuccess={onEditUserSuccess}
 | 
			
		||||
          onResetPasswordSuccess={onEditPasswordSuccess}
 | 
			
		||||
          onClose={() => (shouldShowEditUserForm = false)}
 | 
			
		||||
        />
 | 
			
		||||
      {/if}
 | 
			
		||||
 | 
			
		||||
      {#if shouldShowDeleteConfirmDialog && selectedUser}
 | 
			
		||||
        <DeleteConfirmDialog
 | 
			
		||||
          user={selectedUser}
 | 
			
		||||
@ -148,38 +122,6 @@
 | 
			
		||||
        />
 | 
			
		||||
      {/if}
 | 
			
		||||
 | 
			
		||||
      {#if shouldShowPasswordResetSuccess}
 | 
			
		||||
        <ConfirmDialog
 | 
			
		||||
          title={$t('password_reset_success')}
 | 
			
		||||
          confirmText={$t('done')}
 | 
			
		||||
          onConfirm={() => (shouldShowPasswordResetSuccess = false)}
 | 
			
		||||
          onCancel={() => (shouldShowPasswordResetSuccess = false)}
 | 
			
		||||
          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>
 | 
			
		||||
      {/if}
 | 
			
		||||
 | 
			
		||||
      <table class="my-5 w-full text-start">
 | 
			
		||||
        <thead
 | 
			
		||||
          class="mb-4 flex h-12 w-full rounded-md border bg-gray-50 text-immich-primary dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-primary"
 | 
			
		||||
@ -225,7 +167,7 @@
 | 
			
		||||
                      size="small"
 | 
			
		||||
                      icon={mdiPencilOutline}
 | 
			
		||||
                      title={$t('edit_user')}
 | 
			
		||||
                      onclick={() => editUserHandler(immichUser)}
 | 
			
		||||
                      onclick={() => handleEdit(immichUser)}
 | 
			
		||||
                      aria-label={$t('edit_user')}
 | 
			
		||||
                    />
 | 
			
		||||
                    {#if immichUser.id !== $user.id}
 | 
			
		||||
@ -257,7 +199,7 @@
 | 
			
		||||
          {/if}
 | 
			
		||||
        </tbody>
 | 
			
		||||
      </table>
 | 
			
		||||
      <Button shape="round" size="small" onclick={() => (shouldShowCreateUserForm = true)}>{$t('create_user')}</Button>
 | 
			
		||||
      <Button shape="round" size="small" onclick={handleCreate}>{$t('create_user')}</Button>
 | 
			
		||||
    </section>
 | 
			
		||||
  </section>
 | 
			
		||||
</UserPageLayout>
 | 
			
		||||
 | 
			
		||||
@ -36,7 +36,7 @@ export default {
 | 
			
		||||
        danger: 'rgb(var(--immich-ui-danger) / <alpha-value>)',
 | 
			
		||||
        warning: 'rgb(var(--immich-ui-warning) / <alpha-value>)',
 | 
			
		||||
        info: 'rgb(var(--immich-ui-info) / <alpha-value>)',
 | 
			
		||||
        subtle: 'rgb(var(--immich-gray) / <alpha-value>)',
 | 
			
		||||
        subtle: 'rgb(var(--immich-ui-gray) / <alpha-value>)',
 | 
			
		||||
      },
 | 
			
		||||
      borderColor: ({ theme }) => ({
 | 
			
		||||
        ...theme('colors'),
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user