fix: avoid nested board settings forms

This commit is contained in:
ajnart committed 2026-08-14 11:32:09 +02:00
1 parent 6a5162d615
commit 7adc18b1ce
1 file changed
+36 -27
@@ -36,6 +36,11 @@ const boardSettingsFormSchema = boardSavePartialSettingsSchema
export type FormValues = z.infer<typeof boardSettingsFormSchema>;
const normalizeMobileLayoutGutters = (layouts: readonly FormValues["layouts"][number][]) =>
layouts.map((layout) =>
layout.role === "mobile" ? { ...layout, leftGutterColumnCount: 0, rightGutterColumnCount: 0 } : layout,
);
const PARTIAL_FORM_KEYS = [
"pageTitle",
"metaTitle",
@@ -70,7 +75,7 @@ const buildInitialValues = (board: Board): FormValues => ({
itemRadius: board.itemRadius,
customCss: board.customCss ?? "",
disableStatus: board.disableStatus,
layouts: board.layouts,
layouts: normalizeMobileLayoutGutters(board.layouts),
});
interface BoardSettingsFormProps {
@@ -122,46 +127,45 @@ export const BoardSettingsForm = ({ board, permissions, hasFullAccess, hideVisib
return () => window.removeEventListener("beforeunload", handler);
}, []);
const handleSubmitAsync = async (values: FormValues) => {
const saveSettingsAsync = async (values: FormValues): Promise<FormValues | null> => {
const defaults = initialValuesRef.current;
const changed = <TKey extends keyof FormValues>(...fields: TKey[]) =>
fields.some((field) => values[field] !== defaults[field]);
const { layouts, ...partialSettings } = values;
const saveActions: { when: boolean; action: () => Promise<unknown> }[] = [
{
when: changed(...PARTIAL_FORM_KEYS),
action: () =>
savePartialSettings.mutateAsync({ id: board.id, ...partialSettings }).then(() => {
updateFavicon(values.faviconImageUrl ?? homarrLogoPath);
}),
},
{
when: changed("layouts"),
action: () => saveLayouts.mutateAsync({ id: board.id, layouts }),
},
];
const promises = saveActions.filter((saveAction) => saveAction.when).map((saveAction) => saveAction.action());
if (promises.length === 0) return;
const { layouts: submittedLayouts, ...partialSettings } = values;
const layouts = normalizeMobileLayoutGutters(submittedLayouts);
const partialSettingsChanged = changed(...PARTIAL_FORM_KEYS);
const layoutsChanged = changed("layouts");
if (!partialSettingsChanged && !layoutsChanged) return values;
try {
await Promise.all(promises);
lastSavedRef.current = { pageTitle: values.pageTitle, logoImageUrl: values.logoImageUrl };
initialValuesRef.current = values;
form.setInitialValues(values);
form.resetDirty();
const [, canonicalLayouts] = await Promise.all([
partialSettingsChanged
? savePartialSettings.mutateAsync({ id: board.id, ...partialSettings }).then(() => {
updateFavicon(values.faviconImageUrl ?? homarrLogoPath);
})
: Promise.resolve(),
layoutsChanged ? saveLayouts.mutateAsync({ id: board.id, layouts }) : Promise.resolve(null),
]);
const canonicalValues = canonicalLayouts ? { ...values, layouts: canonicalLayouts } : values;
lastSavedRef.current = { pageTitle: canonicalValues.pageTitle, logoImageUrl: canonicalValues.logoImageUrl };
initialValuesRef.current = canonicalValues;
form.setValues(canonicalValues);
form.setInitialValues(canonicalValues);
form.resetDirty(canonicalValues);
await revalidatePathActionAsync(`/boards/${board.name}/settings`);
showSuccessNotification({
title: t("common.notification.update.success"),
message: t("common.notification.update.success"),
});
return canonicalValues;
} catch {
showErrorNotification({
title: t("common.notification.update.error"),
message: t("common.notification.update.error"),
});
return null;
}
};
@@ -179,10 +183,15 @@ export const BoardSettingsForm = ({ board, permissions, hasFullAccess, hideVisib
return (
<Stack gap="xl">
<form onSubmit={form.onSubmit((values) => void handleSubmitAsync(values))}>
<form onSubmit={form.onSubmit((values) => void saveSettingsAsync(values))}>
<Stack gap="xl">
<GeneralSettingsContent board={board} form={form} />
<LayoutSettingsContent form={form} />
<LayoutSettingsContent
board={board}
form={form}
isSaving={isPending}
saveSettingsAsync={() => saveSettingsAsync(form.values)}
/>
<BackgroundSettingsContent form={form} />
<ColorSettingsContent form={form} />
<CustomCssSettingsContent form={form} />