From 1138f6dcceab6d113253d89e659e64fe468ffadb Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Tue, 6 May 2025 14:44:44 +0200 Subject: [PATCH] refactor: job create modal (#18106) * refactor: job create modal * chore: better modal manager types (#18107) --- web/src/lib/managers/modal-manager.svelte.ts | 11 ++- web/src/lib/modals/JobCreateModal.svelte | 65 +++++++++++++++++ web/src/routes/admin/jobs-status/+page.svelte | 73 +++---------------- .../routes/admin/user-management/+page.svelte | 2 +- 4 files changed, 83 insertions(+), 68 deletions(-) create mode 100644 web/src/lib/modals/JobCreateModal.svelte diff --git a/web/src/lib/managers/modal-manager.svelte.ts b/web/src/lib/managers/modal-manager.svelte.ts index c8cefe8a58..73a3351a7e 100644 --- a/web/src/lib/managers/modal-manager.svelte.ts +++ b/web/src/lib/managers/modal-manager.svelte.ts @@ -2,11 +2,14 @@ import ConfirmDialog from '$lib/components/shared-components/dialog/confirm-dial 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; +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'>) { + open<T extends object, K = OnCloseData<T>>( + Component: Component<T>, + props?: OptionalIfEmpty<Omit<T, 'onClose'>> | Record<string, never>, + ): Promise<K>; + open<T extends object, K = OnCloseData<T>>(Component: Component<T>, props: OptionalIfEmpty<Omit<T, 'onClose'>>) { return new Promise<K>((resolve) => { let modal: object = {}; @@ -18,7 +21,7 @@ class ModalManager { modal = mount(Component, { target: document.body, props: { - ...(props as T), + ...((props ?? {}) as T), onClose, }, }); diff --git a/web/src/lib/modals/JobCreateModal.svelte b/web/src/lib/modals/JobCreateModal.svelte new file mode 100644 index 0000000000..6c173f918c --- /dev/null +++ b/web/src/lib/modals/JobCreateModal.svelte @@ -0,0 +1,65 @@ +<script lang="ts"> + import Combobox, { type ComboBoxOption } from '$lib/components/shared-components/combobox.svelte'; + import ConfirmDialog from '$lib/components/shared-components/dialog/confirm-dialog.svelte'; + import { + notificationController, + NotificationType, + } from '$lib/components/shared-components/notification/notification'; + import { handleError } from '$lib/utils/handle-error'; + import { createJob, ManualJobName } from '@immich/sdk'; + import { t } from 'svelte-i18n'; + + type Props = { onClose: (confirmed: boolean) => void }; + + let { onClose }: Props = $props(); + + const options = [ + { title: $t('admin.person_cleanup_job'), value: ManualJobName.PersonCleanup }, + { title: $t('admin.tag_cleanup_job'), value: ManualJobName.TagCleanup }, + { title: $t('admin.user_cleanup_job'), value: ManualJobName.UserCleanup }, + { title: $t('admin.memory_cleanup_job'), value: ManualJobName.MemoryCleanup }, + { title: $t('admin.memory_generate_job'), value: ManualJobName.MemoryCreate }, + { title: $t('admin.backup_database'), value: ManualJobName.BackupDatabase }, + ].map(({ value, title }) => ({ id: value, label: title, value })); + + let selectedJob: ComboBoxOption | undefined = $state(undefined); + + const onsubmit = async (event: Event) => { + event.preventDefault(); + await handleCreate(); + }; + + const handleCreate = async () => { + if (!selectedJob) { + return; + } + + try { + await createJob({ jobCreateDto: { name: selectedJob.value as ManualJobName } }); + notificationController.show({ message: $t('admin.job_created'), type: NotificationType.Info }); + onClose(true); + } catch (error) { + handleError(error, $t('errors.unable_to_submit_job')); + } + }; +</script> + +<ConfirmDialog + confirmColor="primary" + title={$t('admin.create_job')} + disabled={!selectedJob} + onClose={(confirmed) => (confirmed ? handleCreate() : onClose(false))} +> + {#snippet promptSnippet()} + <form {onsubmit} autocomplete="off" id="create-tag-form" class="w-full"> + <div class="flex flex-col gap-1 text-start"> + <Combobox + bind:selectedOption={selectedJob} + label={$t('jobs')} + {options} + placeholder={$t('admin.search_jobs')} + /> + </div> + </form> + {/snippet} +</ConfirmDialog> diff --git a/web/src/routes/admin/jobs-status/+page.svelte b/web/src/routes/admin/jobs-status/+page.svelte index af7f3d11af..c5327a4271 100644 --- a/web/src/routes/admin/jobs-status/+page.svelte +++ b/web/src/routes/admin/jobs-status/+page.svelte @@ -1,16 +1,11 @@ <script lang="ts"> import JobsPanel from '$lib/components/admin-page/jobs/jobs-panel.svelte'; import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte'; - import Combobox, { type ComboBoxOption } from '$lib/components/shared-components/combobox.svelte'; - import ConfirmDialog from '$lib/components/shared-components/dialog/confirm-dialog.svelte'; - import { - notificationController, - NotificationType, - } from '$lib/components/shared-components/notification/notification'; import { AppRoute } from '$lib/constants'; + import { modalManager } from '$lib/managers/modal-manager.svelte'; + import JobCreateModal from '$lib/modals/JobCreateModal.svelte'; import { asyncTimeout } from '$lib/utils'; - import { handleError } from '$lib/utils/handle-error'; - import { createJob, getAllJobsStatus, ManualJobName, type AllJobStatusResponseDto } from '@immich/sdk'; + import { getAllJobsStatus, type AllJobStatusResponseDto } from '@immich/sdk'; import { Button, HStack, Text } from '@immich/ui'; import { mdiCog, mdiPlus } from '@mdi/js'; import { onDestroy, onMount } from 'svelte'; @@ -26,8 +21,6 @@ let jobs: AllJobStatusResponseDto | undefined = $state(); let running = true; - let isOpen = $state(false); - let selectedJob: ComboBoxOption | undefined = $state(undefined); onMount(async () => { while (running) { @@ -39,42 +32,18 @@ onDestroy(() => { running = false; }); - - const options = [ - { title: $t('admin.person_cleanup_job'), value: ManualJobName.PersonCleanup }, - { title: $t('admin.tag_cleanup_job'), value: ManualJobName.TagCleanup }, - { title: $t('admin.user_cleanup_job'), value: ManualJobName.UserCleanup }, - { title: $t('admin.memory_cleanup_job'), value: ManualJobName.MemoryCleanup }, - { title: $t('admin.memory_generate_job'), value: ManualJobName.MemoryCreate }, - { title: $t('admin.backup_database'), value: ManualJobName.BackupDatabase }, - ].map(({ value, title }) => ({ id: value, label: title, value })); - - const handleCancel = () => (isOpen = false); - - const handleCreate = async () => { - if (!selectedJob) { - return; - } - - try { - await createJob({ jobCreateDto: { name: selectedJob.value as ManualJobName } }); - notificationController.show({ message: $t('admin.job_created'), type: NotificationType.Info }); - handleCancel(); - } catch (error) { - handleError(error, $t('errors.unable_to_submit_job')); - } - }; - - const onsubmit = async (event: Event) => { - event.preventDefault(); - await handleCreate(); - }; </script> <UserPageLayout title={data.meta.title} admin> {#snippet buttons()} <HStack gap={0}> - <Button leadingIcon={mdiPlus} onclick={() => (isOpen = true)} size="small" variant="ghost" color="secondary"> + <Button + leadingIcon={mdiPlus} + onclick={() => modalManager.open(JobCreateModal)} + size="small" + variant="ghost" + color="secondary" + > <Text class="hidden md:block">{$t('admin.create_job')}</Text> </Button> <Button @@ -96,25 +65,3 @@ </section> </section> </UserPageLayout> - -{#if isOpen} - <ConfirmDialog - confirmColor="primary" - title={$t('admin.create_job')} - disabled={!selectedJob} - onClose={(confirmed) => (confirmed ? handleCreate() : handleCancel())} - > - {#snippet promptSnippet()} - <form {onsubmit} autocomplete="off" id="create-tag-form" class="w-full"> - <div class="flex flex-col gap-1 text-start"> - <Combobox - bind:selectedOption={selectedJob} - label={$t('jobs')} - {options} - placeholder={$t('admin.search_jobs')} - /> - </div> - </form> - {/snippet} - </ConfirmDialog> -{/if} diff --git a/web/src/routes/admin/user-management/+page.svelte b/web/src/routes/admin/user-management/+page.svelte index f6e7025ce2..8ac3793046 100644 --- a/web/src/routes/admin/user-management/+page.svelte +++ b/web/src/routes/admin/user-management/+page.svelte @@ -59,7 +59,7 @@ }; const handleCreate = async () => { - await modalManager.open(UserCreateModal, {}); + await modalManager.open(UserCreateModal); await refresh(); };