From 62fc5b3c7db526856832a778fe877305a51e34d0 Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Sat, 3 May 2025 00:41:42 +0200 Subject: [PATCH] refactor: introduce modal manager (#18039) --- web/package-lock.json | 8 +- web/package.json | 2 +- web/src/app.css | 3 +- .../components/forms/create-user-form.svelte | 126 ++++++++------- .../components/forms/edit-user-form.svelte | 148 +++++++++--------- .../shared-components/change-location.svelte | 2 +- .../dialog/confirm-dialog.svelte | 35 +++-- .../lib/forms/password-reset-success.svelte | 43 +++++ web/src/lib/managers/modal-manager.svelte.ts | 33 ++++ .../routes/admin/user-management/+page.svelte | 99 +++--------- web/tailwind.config.js | 2 +- 11 files changed, 265 insertions(+), 236 deletions(-) create mode 100644 web/src/lib/forms/password-reset-success.svelte create mode 100644 web/src/lib/managers/modal-manager.svelte.ts diff --git a/web/package-lock.json b/web/package-lock.json index c76dd64840..75c55aa779 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -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", diff --git a/web/package.json b/web/package.json index 9aa9bee6bc..7c5a0147bb 100644 --- a/web/package.json +++ b/web/package.json @@ -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", diff --git a/web/src/app.css b/web/src/app.css index 2c8d150b4f..61759eb1b0 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -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; } } diff --git a/web/src/lib/components/forms/create-user-form.svelte b/web/src/lib/components/forms/create-user-form.svelte index 83b3154d4b..34e498ce1c 100644 --- a/web/src/lib/components/forms/create-user-form.svelte +++ b/web/src/lib/components/forms/create-user-form.svelte @@ -1,21 +1,29 @@ -
- - {#if error} - - {/if} - - {#if success} -

{$t('new_user_created')}

- {/if} - - - - - - - {#if $featureFlags.email} - - - + + + + {#if error} + {/if} - - - + {#if success} +

{$t('new_user_created')}

+ {/if} - - - {passwordMismatchMessage} - + + + + - - - - - - - - - - - {#if quotaSizeWarning} - {$t('errors.quota_higher_than_disk_size')} + {#if $featureFlags.email} + + + {/if} - - - {#snippet stickyBottom()} - - - {/snippet} -
-
+ + + + + + + {passwordMismatchMessage} + + + + + + + + + + + + + {#if quotaSizeWarning} + {$t('errors.quota_higher_than_disk_size')} + {/if} + + + + + + +
+ + +
+
+ diff --git a/web/src/lib/components/forms/edit-user-form.svelte b/web/src/lib/components/forms/edit-user-form.svelte index ab914e6430..d2f56a974a 100644 --- a/web/src/lib/components/forms/edit-user-form.svelte +++ b/web/src/lib/components/forms/edit-user-form.svelte @@ -1,34 +1,26 @@ - -
-
- - + + + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + + +

+ {$t('admin.note_apply_storage_label_previous_assets')} + + {$t('admin.storage_template_migration_job')} + +

+
+ +
+ + +
+ {#if canResetPassword} + + {/if} +
- -
- - -
- -
- - -
- -
- - - -

- {$t('admin.note_apply_storage_label_previous_assets')} - - {$t('admin.storage_template_migration_job')} - -

-
- - - {#snippet stickyBottom()} - {#if canResetPassword} - - {/if} - - {/snippet} - +
+
diff --git a/web/src/lib/components/shared-components/change-location.svelte b/web/src/lib/components/shared-components/change-location.svelte index 1987596026..3539945911 100644 --- a/web/src/lib/components/shared-components/change-location.svelte +++ b/web/src/lib/components/shared-components/change-location.svelte @@ -115,7 +115,7 @@ (confirmed ? handleConfirm() : onCancel())} > {#snippet promptSnippet()} diff --git a/web/src/lib/components/shared-components/dialog/confirm-dialog.svelte b/web/src/lib/components/shared-components/dialog/confirm-dialog.svelte index 32f4b6a8f4..75c07aebc6 100644 --- a/web/src/lib/components/shared-components/dialog/confirm-dialog.svelte +++ b/web/src/lib/components/shared-components/dialog/confirm-dialog.svelte @@ -1,8 +1,7 @@ - onClose(false)} {width}> -
+ onClose(false)} {size} class="bg-light text-dark"> + {#if promptSnippet}{@render promptSnippet()}{:else}

{prompt}

{/if} -
+ - {#snippet stickyBottom()} - {#if !hideCancelButton} - + {/if} + - {/if} - - {/snippet} -
+
+ + diff --git a/web/src/lib/forms/password-reset-success.svelte b/web/src/lib/forms/password-reset-success.svelte new file mode 100644 index 0000000000..7091047eb8 --- /dev/null +++ b/web/src/lib/forms/password-reset-success.svelte @@ -0,0 +1,43 @@ + + + + {#snippet promptSnippet()} +
+ {$t('admin.user_password_has_been_reset')} + +
+ {newPassword} + copyToClipboard(newPassword)} + title={$t('copy_password')} + aria-label={$t('copy_password')} + /> +
+ + {$t('admin.user_password_reset_description')} +
+ {/snippet} +
diff --git a/web/src/lib/managers/modal-manager.svelte.ts b/web/src/lib/managers/modal-manager.svelte.ts new file mode 100644 index 0000000000..c8cefe8a58 --- /dev/null +++ b/web/src/lib/managers/modal-manager.svelte.ts @@ -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 extends { onClose: (data: infer R) => void } ? R : never; +// TODO make `props` optional if component only has `onClose` +// type OptionalIfEmpty = keyof T extends never ? undefined : T; + +class ModalManager { + open>(Component: Component, props: Omit) { + return new Promise((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, 'onClose'>) { + return this.open(ConfirmDialog, options); + } +} + +export const modalManager = new ModalManager(); diff --git a/web/src/routes/admin/user-management/+page.svelte b/web/src/routes/admin/user-management/+page.svelte index 8b96c6c922..42d1404177 100644 --- a/web/src/routes/admin/user-management/+page.svelte +++ b/web/src/routes/admin/user-management/+page.svelte @@ -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(); - 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 @@
- {#if shouldShowCreateUserForm} - (shouldShowCreateUserForm = false)} - onClose={() => (shouldShowCreateUserForm = false)} - oauthEnabled={$featureFlags.oauth} - /> - {/if} - - {#if shouldShowEditUserForm && selectedUser} - (shouldShowEditUserForm = false)} - /> - {/if} - {#if shouldShowDeleteConfirmDialog && selectedUser} {/if} - {#if shouldShowPasswordResetSuccess} - (shouldShowPasswordResetSuccess = false)} - hideCancelButton={true} - confirmColor="success" - > - {#snippet promptSnippet()} -
- {$t('admin.user_password_has_been_reset')} - -
- {newPassword} - copyToClipboard(newPassword)} - title={$t('copy_password')} - aria-label={$t('copy_password')} - /> -
- - {$t('admin.user_password_reset_description')} -
- {/snippet} -
- {/if} - editUserHandler(immichUser)} + onclick={() => handleEdit(immichUser)} aria-label={$t('edit_user')} /> {#if immichUser.id !== $user.id} @@ -256,7 +199,7 @@ {/if}
- +
diff --git a/web/tailwind.config.js b/web/tailwind.config.js index 95611d486d..2e13e5997d 100644 --- a/web/tailwind.config.js +++ b/web/tailwind.config.js @@ -36,7 +36,7 @@ export default { danger: 'rgb(var(--immich-ui-danger) / )', warning: 'rgb(var(--immich-ui-warning) / )', info: 'rgb(var(--immich-ui-info) / )', - subtle: 'rgb(var(--immich-gray) / )', + subtle: 'rgb(var(--immich-ui-gray) / )', }, borderColor: ({ theme }) => ({ ...theme('colors'),