From ddf8ba9948f34aaaa225d579a58143cde49b779e Mon Sep 17 00:00:00 2001 From: Thomas Date: Thu, 20 Aug 2026 12:33:18 +0200 Subject: [PATCH] feat: rework management settings layout (#6634) --- apps/docs/docs/management/settings/index.mdx | 2 +- .../_components/appearance-settings-form.tsx | 1 + .../settings/_components/board-settings-form.tsx | 8 +++++--- .../settings/_components/culture-settings-form.tsx | 1 + .../settings/_components/search-settings-form.tsx | 6 +++--- .../manage/settings/_components/settings-form.tsx | 13 ++++++++----- .../src/app/[locale]/manage/settings/page.tsx | 14 +++++++++++--- 7 files changed, 30 insertions(+), 15 deletions(-) diff --git a/apps/docs/docs/management/settings/index.mdx b/apps/docs/docs/management/settings/index.mdx index 8507d2b4f..215ea1bd5 100644 --- a/apps/docs/docs/management/settings/index.mdx +++ b/apps/docs/docs/management/settings/index.mdx @@ -13,7 +13,7 @@ tags: The settings page applies options to the whole Homarr application. Some changes need a full page reload before they take effect. -All changes are collected in a single form. Click the **Save changes** button in the save bar at the bottom of the page to apply them. The save bar only appears when you have unsaved changes, and the **Discard** button reverts your changes. +All settings sections remain visible in a single form. Click the **Save changes** button in the save bar at the bottom of the page to apply them. The save bar only appears when you have unsaved changes, and the **Discard** button reverts your changes. ## Analytics diff --git a/apps/nextjs/src/app/[locale]/manage/settings/_components/appearance-settings-form.tsx b/apps/nextjs/src/app/[locale]/manage/settings/_components/appearance-settings-form.tsx index e33afe1b6..bd09e35df 100644 --- a/apps/nextjs/src/app/[locale]/manage/settings/_components/appearance-settings-form.tsx +++ b/apps/nextjs/src/app/[locale]/manage/settings/_components/appearance-settings-form.tsx @@ -29,6 +29,7 @@ export const AppearanceSettingsForm = ({ form }: AppearanceSettingsFormProps) => }))} {...form.getInputProps("defaultColorScheme")} SelectOption={AppearanceCustomOption} + withinPortal /> ); diff --git a/apps/nextjs/src/app/[locale]/manage/settings/_components/board-settings-form.tsx b/apps/nextjs/src/app/[locale]/manage/settings/_components/board-settings-form.tsx index 801e0df9f..d520c1f02 100644 --- a/apps/nextjs/src/app/[locale]/manage/settings/_components/board-settings-form.tsx +++ b/apps/nextjs/src/app/[locale]/manage/settings/_components/board-settings-form.tsx @@ -2,7 +2,7 @@ import { Switch, Text } from "@mantine/core"; -import { clientApi } from "@homarr/api/client"; +import type { RouterOutputs } from "@homarr/api"; import type { UseFormReturnType } from "@homarr/form"; import { useScopedI18n } from "@homarr/translation/client"; @@ -12,11 +12,11 @@ import type { FormValues } from "./settings-form"; interface BoardSettingsFormProps { form: UseFormReturnType; + selectableBoards: RouterOutputs["board"]["getPublicBoards"]; } -export const BoardSettingsForm = ({ form }: BoardSettingsFormProps) => { +export const BoardSettingsForm = ({ form, selectableBoards }: BoardSettingsFormProps) => { const tBoard = useScopedI18n("management.page.settings.section.board"); - const [selectableBoards] = clientApi.board.getPublicBoards.useSuspenseQuery(); return ( @@ -26,6 +26,7 @@ export const BoardSettingsForm = ({ form }: BoardSettingsFormProps) => { clearable boards={selectableBoards} {...form.getInputProps("homeBoardId")} + withinPortal /> { clearable boards={selectableBoards} {...form.getInputProps("mobileHomeBoardId")} + withinPortal /> {tBoard("status.title")} diff --git a/apps/nextjs/src/app/[locale]/manage/settings/_components/culture-settings-form.tsx b/apps/nextjs/src/app/[locale]/manage/settings/_components/culture-settings-form.tsx index da34c7019..aa8b8ae10 100644 --- a/apps/nextjs/src/app/[locale]/manage/settings/_components/culture-settings-form.tsx +++ b/apps/nextjs/src/app/[locale]/manage/settings/_components/culture-settings-form.tsx @@ -22,6 +22,7 @@ export const CultureSettingsForm = ({ form }: CultureSettingsFormProps) => { label={tCulture("defaultLocale.label")} {...localeInputProps} value={localeInputProps.value as SupportedLanguage} + withinPortal /> ); diff --git a/apps/nextjs/src/app/[locale]/manage/settings/_components/search-settings-form.tsx b/apps/nextjs/src/app/[locale]/manage/settings/_components/search-settings-form.tsx index 904dd4b08..ad48a97cc 100644 --- a/apps/nextjs/src/app/[locale]/manage/settings/_components/search-settings-form.tsx +++ b/apps/nextjs/src/app/[locale]/manage/settings/_components/search-settings-form.tsx @@ -2,7 +2,7 @@ import { Select } from "@mantine/core"; -import { clientApi } from "@homarr/api/client"; +import type { RouterOutputs } from "@homarr/api"; import type { UseFormReturnType } from "@homarr/form"; import { useScopedI18n } from "@homarr/translation/client"; @@ -11,11 +11,11 @@ import type { FormValues } from "./settings-form"; interface SearchSettingsFormProps { form: UseFormReturnType; + selectableSearchEngines: RouterOutputs["searchEngine"]["getSelectable"]; } -export const SearchSettingsForm = ({ form }: SearchSettingsFormProps) => { +export const SearchSettingsForm = ({ form, selectableSearchEngines }: SearchSettingsFormProps) => { const tSearch = useScopedI18n("management.page.settings.section.search"); - const [selectableSearchEngines] = clientApi.searchEngine.getSelectable.useSuspenseQuery({ withIntegrations: false }); return ( diff --git a/apps/nextjs/src/app/[locale]/manage/settings/_components/settings-form.tsx b/apps/nextjs/src/app/[locale]/manage/settings/_components/settings-form.tsx index ce64a3404..b99a5aa9e 100644 --- a/apps/nextjs/src/app/[locale]/manage/settings/_components/settings-form.tsx +++ b/apps/nextjs/src/app/[locale]/manage/settings/_components/settings-form.tsx @@ -4,6 +4,7 @@ import { useEffect, useRef, useState } from "react"; import { Button, Stack } from "@mantine/core"; import { z } from "zod/v4"; +import type { RouterOutputs } from "@homarr/api"; import { clientApi } from "@homarr/api/client"; import { revalidatePathActionAsync } from "@homarr/common/client"; import { env } from "@homarr/common/env"; @@ -58,9 +59,11 @@ const buildInitialValues = (initialData: ServerSettings): FormValues => ({ interface SettingsFormProps { initialData: ServerSettings; + selectableBoards: RouterOutputs["board"]["getPublicBoards"]; + selectableSearchEngines: RouterOutputs["searchEngine"]["getSelectable"]; } -export const SettingsForm = ({ initialData }: SettingsFormProps) => { +export const SettingsForm = ({ initialData, selectableBoards, selectableSearchEngines }: SettingsFormProps) => { const t = useI18n(); const tSettings = useScopedI18n("management.page.settings"); @@ -169,13 +172,13 @@ export const SettingsForm = ({ initialData }: SettingsFormProps) => { return (
void handleSubmitAsync(values))}> - - - + - + + + {form.isDirty() && ( diff --git a/apps/nextjs/src/app/[locale]/manage/settings/page.tsx b/apps/nextjs/src/app/[locale]/manage/settings/page.tsx index 242cd44c1..ddfa33a24 100644 --- a/apps/nextjs/src/app/[locale]/manage/settings/page.tsx +++ b/apps/nextjs/src/app/[locale]/manage/settings/page.tsx @@ -23,12 +23,20 @@ export default async function SettingsPage() { notFound(); } - const serverSettings = await api.serverSettings.getAll(); - const tSettings = await getScopedI18n("management.page.settings"); + const [serverSettings, selectableBoards, selectableSearchEngines, tSettings] = await Promise.all([ + api.serverSettings.getAll(), + api.board.getPublicBoards(), + api.searchEngine.getSelectable({ withIntegrations: false }), + getScopedI18n("management.page.settings"), + ]); return ( - + ); }