refactor: job create modal ()

* refactor: job create modal

* chore: better modal manager types ()
This commit is contained in:
Daniel Dietzler 2025-05-06 14:44:44 +02:00 committed by GitHub
parent 33f3751b72
commit 1138f6dcce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 83 additions and 68 deletions
web/src
lib
routes/admin
jobs-status
user-management

View file

@ -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,
},
});

View file

@ -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>

View file

@ -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}

View file

@ -59,7 +59,7 @@
};
const handleCreate = async () => {
await modalManager.open(UserCreateModal, {});
await modalManager.open(UserCreateModal);
await refresh();
};