diff --git a/server/src/bin/migrations.ts b/server/src/bin/migrations.ts index 7b850f6166..69070dc0cf 100644 --- a/server/src/bin/migrations.ts +++ b/server/src/bin/migrations.ts @@ -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(); diff --git a/server/test/medium.factory.ts b/server/test/medium.factory.ts index 388c4df96b..89b1921819 100644 --- a/server/test/medium.factory.ts +++ b/server/test/medium.factory.ts @@ -142,18 +142,15 @@ export const getRepository = (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': { diff --git a/server/test/medium/globalSetup.ts b/server/test/medium/globalSetup.ts index e63c9f5224..4398da5c0a 100644 --- a/server/test/medium/globalSetup.ts +++ b/server/test/medium/globalSetup.ts @@ -42,7 +42,7 @@ const globalSetup = async () => { const db = new Kysely(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(); 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/admin-page/delete-confirm-dialogue.svelte b/web/src/lib/components/admin-page/delete-confirm-dialogue.svelte index 6eb603263e..e2d3c86bf3 100644 --- a/web/src/lib/components/admin-page/delete-confirm-dialogue.svelte +++ b/web/src/lib/components/admin-page/delete-confirm-dialogue.svelte @@ -47,8 +47,7 @@ (confirmed ? handleDeleteUser() : onCancel())} disabled={deleteButtonDisabled} > {#snippet promptSnippet()} diff --git a/web/src/lib/components/admin-page/restore-dialogue.svelte b/web/src/lib/components/admin-page/restore-dialogue.svelte index 1386ae9fc4..7fd51aaf06 100644 --- a/web/src/lib/components/admin-page/restore-dialogue.svelte +++ b/web/src/lib/components/admin-page/restore-dialogue.svelte @@ -33,8 +33,7 @@ title={$t('restore_user')} confirmText={$t('continue')} confirmColor="success" - onConfirm={handleRestoreUser} - {onCancel} + onClose={(confirmed) => (confirmed ? handleRestoreUser() : onCancel())} > {#snippet promptSnippet()}

diff --git a/web/src/lib/components/admin-page/settings/auth/auth-settings.svelte b/web/src/lib/components/admin-page/settings/auth/auth-settings.svelte index b2454b06c3..2a270f7438 100644 --- a/web/src/lib/components/admin-page/settings/auth/auth-settings.svelte +++ b/web/src/lib/components/admin-page/settings/auth/auth-settings.svelte @@ -49,8 +49,7 @@ {#if isConfirmOpen} (isConfirmOpen = false)} - onConfirm={() => handleSave(true)} + onClose={(confirmed) => (confirmed ? handleSave(true) : (isConfirmOpen = false))} > {#snippet promptSnippet()}

diff --git a/web/src/lib/components/album-page/album-options.svelte b/web/src/lib/components/album-page/album-options.svelte index 884de8c2a2..4a8c018fbd 100644 --- a/web/src/lib/components/album-page/album-options.svelte +++ b/web/src/lib/components/album-page/album-options.svelte @@ -1,27 +1,27 @@ @@ -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} 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/photos-page/delete-asset-dialog.svelte b/web/src/lib/components/photos-page/delete-asset-dialog.svelte index 3053600a47..fbdec86244 100644 --- a/web/src/lib/components/photos-page/delete-asset-dialog.svelte +++ b/web/src/lib/components/photos-page/delete-asset-dialog.svelte @@ -1,9 +1,9 @@ - + (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 dad16d52ca..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)} {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/components/shared-components/dialog/dialog.ts b/web/src/lib/components/shared-components/dialog/dialog.ts index 8efff58da0..69a64aad21 100644 --- a/web/src/lib/components/shared-components/dialog/dialog.ts +++ b/web/src/lib/components/shared-components/dialog/dialog.ts @@ -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((resolve) => { const newDialog: Dialog = { ...options, - onConfirm: () => { + onClose: (confirmed) => { dialog.set(undefined); - resolve(true); - }, - onCancel: () => { - dialog.set(undefined); - resolve(false); + resolve(confirmed); }, }; 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/jobs-status/+page.svelte b/web/src/routes/admin/jobs-status/+page.svelte index 07757614e5..af7f3d11af 100644 --- a/web/src/routes/admin/jobs-status/+page.svelte +++ b/web/src/routes/admin/jobs-status/+page.svelte @@ -102,8 +102,7 @@ confirmColor="primary" title={$t('admin.create_job')} disabled={!selectedJob} - onConfirm={handleCreate} - onCancel={handleCancel} + onClose={(confirmed) => (confirmed ? handleCreate() : handleCancel())} > {#snippet promptSnippet()}
diff --git a/web/src/routes/admin/user-management/+page.svelte b/web/src/routes/admin/user-management/+page.svelte index a25799588a..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)} - onCancel={() => (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} @@ -257,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'),