refactor(forms): update form submission methods

This commit is contained in:
Thomas Camlong committed 2026-05-29 11:46:03 +02:00
1 parent fec0eb51a3
commit ecbed76a65
3 files changed
+69 -26

No files matched your search

@@ -18,7 +18,7 @@ import { findBestIconMatch } from "./icon-matcher";
type FormType = z.infer<typeof appManageSchema>;
export interface AppFormHandle {
submit: () => void;
submit: () => Promise<boolean>;
isDirty: () => boolean;
}
@@ -29,7 +29,7 @@ interface AppFormProps {
submitAndCreateAnother?: string;
};
initialValues?: FormType;
handleSubmit: (values: FormType, redirect: boolean, afterSuccess?: () => void) => void;
handleSubmit: (values: FormType, redirect: boolean, afterSuccess?: () => void) => void | Promise<void>;
isPending: boolean;
hideButtons?: boolean;
formRef?: React.Ref<AppFormHandle>;
@@ -74,7 +74,20 @@ export const AppForm = ({
useImperativeHandle(
formRef,
() => ({
submit: () => form.onSubmit(handleSubmit)(),
submit: () =>
new Promise<boolean>((resolve) => {
form.onSubmit(
async (values) => {
try {
await Promise.resolve(handleSubmit(values));
resolve(true);
} catch {
resolve(false);
}
},
() => resolve(false),
)();
}),
isDirty: () => form.isDirty(),
}),
[form, handleSubmit],
@@ -1,7 +1,7 @@
"use client";
import { useImperativeHandle, useRef } from "react";
import { Alert, Center, Loader, Stack } from "@mantine/core";
import { Alert, Button, Center, Loader, Stack, Text } from "@mantine/core";
import { IconInfoCircle } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
@@ -11,7 +11,7 @@ import { showErrorNotification, showSuccessNotification } from "@homarr/notifica
import { useI18n } from "@homarr/translation/client";
export interface EmbeddedAppEditFormHandle {
submitIfDirty: () => void;
submitIfDirty: () => Promise<boolean>;
}
interface EmbeddedAppEditFormProps {
@@ -22,9 +22,9 @@ interface EmbeddedAppEditFormProps {
export const EmbeddedAppEditForm = ({ appId, handleRef }: EmbeddedAppEditFormProps) => {
const t = useI18n();
const appFormRef = useRef<AppFormHandle>(null);
const { data: app, isPending: isLoadingApp } = clientApi.app.byId.useQuery({ id: appId });
const { data: app, isPending: isLoadingApp, isError, refetch } = clientApi.app.byId.useQuery({ id: appId });
const { mutate, isPending: isMutating } = clientApi.app.update.useMutation({
const { mutateAsync, isPending: isMutating } = clientApi.app.update.useMutation({
onSuccess: () => {
showSuccessNotification({
title: t("app.page.edit.notification.success.title"),
@@ -42,16 +42,18 @@ export const EmbeddedAppEditForm = ({ appId, handleRef }: EmbeddedAppEditFormPro
useImperativeHandle(
handleRef,
() => ({
submitIfDirty: () => {
if (appFormRef.current?.isDirty()) {
appFormRef.current.submit();
submitIfDirty: async () => {
if (!appFormRef.current?.isDirty()) {
return true;
}
return appFormRef.current.submit();
},
}),
[],
);
if (isLoadingApp || !app) {
if (isLoadingApp) {
return (
<Center py="xl">
<Loader />
@@ -59,6 +61,17 @@ export const EmbeddedAppEditForm = ({ appId, handleRef }: EmbeddedAppEditFormPro
);
}
if (isError || !app) {
return (
<Stack align="center" gap="md" py="xl">
<Text c="dimmed">{t("app.action.select.notFound")}</Text>
<Button variant="light" onClick={() => void refetch()}>
{t("common.action.tryAgain")}
</Button>
</Stack>
);
}
return (
<Stack>
<Alert icon={<IconInfoCircle size={16} />} color="blue" variant="light">
@@ -70,8 +83,8 @@ export const EmbeddedAppEditForm = ({ appId, handleRef }: EmbeddedAppEditFormPro
showBackToOverview={false}
buttonLabels={{ submit: "" }}
initialValues={app}
handleSubmit={(values) => {
mutate({ id: appId, ...values });
handleSubmit={async (values) => {
await mutateAsync({ id: appId, ...values });
}}
isPending={isMutating}
/>
@@ -1,6 +1,6 @@
"use client";
import { useRef, useState } from "react";
import { useRef, useState, type FormEvent } from "react";
import { Button, Group, Stack, Tabs } from "@mantine/core";
import { schemaResolver } from "@mantine/form";
import { z } from "zod/v4";
@@ -45,6 +45,7 @@ export const WidgetEditModal = createModal<ModalProps<WidgetKind>>(({ actions, i
const { data: session } = useSession();
const [advancedOptions, setAdvancedOptions] = useState<BoardItemAdvancedOptions>(innerProps.value.advancedOptions);
const appEditRef = useRef<EmbeddedAppEditFormHandle>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
z.config({
customError: zodErrorMap(t),
@@ -86,15 +87,29 @@ export const WidgetEditModal = createModal<ModalProps<WidgetKind>>(({ actions, i
const appId = innerProps.appId;
const showAppTab = innerProps.kind === "app" && canModifyApps && Boolean(appId);
const handleSubmit = form.onSubmit((values) => {
innerProps.onSuccessfulEdit({
...values,
advancedOptions,
});
appEditRef.current?.submitIfDirty();
actions.closeModal();
const handleSubmit = form.onSubmit(async (values) => {
setIsSubmitting(true);
try {
const appSaved = showAppTab ? await (appEditRef.current?.submitIfDirty() ?? true) : true;
if (!appSaved) {
return;
}
innerProps.onSuccessfulEdit({
...values,
advancedOptions,
});
actions.closeModal();
} finally {
setIsSubmitting(false);
}
});
const onFormSubmit = (event: FormEvent<HTMLFormElement>) => {
handleSubmit(event);
};
const widgetFormContent = (
<Stack>
{innerProps.integrationSupport && (
@@ -108,9 +123,7 @@ export const WidgetEditModal = createModal<ModalProps<WidgetKind>>(({ actions, i
const Input = getInputForType(value.type);
if (
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
!Input ||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
value.shouldHide?.(
form.values.options as never,
innerProps.integrationData
@@ -154,7 +167,9 @@ export const WidgetEditModal = createModal<ModalProps<WidgetKind>>(({ actions, i
<Button onClick={actions.closeModal} variant="subtle" color="gray">
{t("common.action.cancel")}
</Button>
<Button type="submit">{t("common.action.saveChanges")}</Button>
<Button type="submit" loading={isSubmitting}>
{t("common.action.saveChanges")}
</Button>
</Group>
)}
</Group>
@@ -162,7 +177,7 @@ export const WidgetEditModal = createModal<ModalProps<WidgetKind>>(({ actions, i
);
return (
<form onSubmit={handleSubmit}>
<form onSubmit={onFormSubmit}>
<FormProvider form={form}>
{showAppTab ? (
<Stack>
@@ -182,7 +197,9 @@ export const WidgetEditModal = createModal<ModalProps<WidgetKind>>(({ actions, i
<Button onClick={actions.closeModal} variant="subtle" color="gray">
{t("common.action.cancel")}
</Button>
<Button type="submit">{t("common.action.saveChanges")}</Button>
<Button type="submit" loading={isSubmitting}>
{t("common.action.saveChanges")}
</Button>
</Group>
</Stack>
) : (