From 7985ff953a03e4ba5c6e674a72bdecd69042c5aa Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Fri, 1 May 2026 09:58:54 +0200 Subject: [PATCH] feat(onboarding): add integration configuration stepper Add 3-phase integration setup to onboarding: multi-select grid, per-integration configuration with NewIntegrationForm stepper, and finishing phase that populates the dashboard and shows final actions. Merge finish step actions into the integrations step. Add translation keys for configure phase. --- .../init/_steps/finish/init-finish.tsx | 24 +-- .../_steps/integrations/init-integrations.tsx | 185 ++++++++++++++++++ apps/nextjs/src/app/[locale]/init/page.tsx | 2 + packages/translation/src/lang/en-gb.json | 14 ++ packages/translation/src/lang/en.json | 14 ++ 5 files changed, 223 insertions(+), 16 deletions(-) create mode 100644 apps/nextjs/src/app/[locale]/init/_steps/integrations/init-integrations.tsx diff --git a/apps/nextjs/src/app/[locale]/init/_steps/finish/init-finish.tsx b/apps/nextjs/src/app/[locale]/init/_steps/finish/init-finish.tsx index 288b9adae..7e426bcfb 100644 --- a/apps/nextjs/src/app/[locale]/init/_steps/finish/init-finish.tsx +++ b/apps/nextjs/src/app/[locale]/init/_steps/finish/init-finish.tsx @@ -1,6 +1,6 @@ import type { MantineColor } from "@mantine/core"; import { Button, Card, Stack, Text } from "@mantine/core"; -import { IconBook2, IconCategoryPlus, IconLayoutDashboard, IconMailForward } from "@tabler/icons-react"; +import { IconBook2, IconLayoutDashboard, IconMailForward } from "@tabler/icons-react"; import { isProviderEnabled } from "@homarr/auth/server"; import { getMantineColor } from "@homarr/common"; @@ -13,27 +13,19 @@ import type { TablerIcon } from "@homarr/ui"; export const InitFinish = async () => { const firstBoard = await db.query.boards.findFirst({ columns: { name: true } }); const tFinish = await getScopedI18n("init.step.finish"); + const boardName = firstBoard?.name ?? "dashboard"; return ( {tFinish("description")} - {firstBoard ? ( - - {tFinish("action.goToBoard", { name: firstBoard.name })} - - ) : ( - - {tFinish("action.createBoard")} - - )} + + {tFinish("action.goToBoard", { name: boardName })} + {isProviderEnabled("credentials") && ( { + const [phase, setPhase] = useState("select"); + const [selectedKinds, setSelectedKinds] = useState([]); + const [currentIndex, setCurrentIndex] = useState(0); + const t = useScopedI18n("init.step.integrations"); + + const { mutateAsync: setupIntegrations } = clientApi.onboard.setupIntegrations.useMutation(); + + const handleSelectionChange = useCallback((kinds: IntegrationKind[]) => { + setSelectedKinds(kinds); + }, []); + + const handleContinueToConfig = () => { + setCurrentIndex(0); + setPhase("configure"); + }; + + const handleSkipAll = () => { + setPhase("done"); + }; + + const advanceOrFinish = () => { + if (currentIndex + 1 >= selectedKinds.length) { + setPhase("done"); + } else { + setCurrentIndex((i) => i + 1); + } + }; + + const backToSelect = () => { + setPhase("select"); + setCurrentIndex(0); + }; + + if (phase === "done") { + return ; + } + + if (phase === "configure") { + const currentKind = selectedKinds[currentIndex]; + if (currentKind === undefined) { + return null; + } + + return ( + + + + {selectedKinds.map((kind, index) => ( + } + completedIcon={} + label={index === currentIndex ? getIntegrationName(kind) : undefined} + /> + ))} + + + + {t("configure.progress", { current: String(currentIndex + 1), total: String(selectedKinds.length) })} + + + + + + ); + } + + return ( + + + {t("description")} + + + + + + + + + + ); +}; + +const FinishingPhase = ({ setupIntegrations }: { setupIntegrations: () => Promise }) => { + const t = useScopedI18n("init.step"); + const [isReady, setIsReady] = useState(false); + const calledRef = useRef(false); + + useEffect(() => { + if (calledRef.current) return; + calledRef.current = true; + + void setupIntegrations().then(() => { + setIsReady(true); + }); + }, [setupIntegrations]); + + if (!isReady) { + return ( + +
+ + + {t("integrations.configure.finishing")} + +
+
+ ); + } + + return ( + + + + + + {t("finish.title")} + + + {t("finish.description")} + + + + + + + + + + + ); +}; diff --git a/apps/nextjs/src/app/[locale]/init/page.tsx b/apps/nextjs/src/app/[locale]/init/page.tsx index 5684e5709..61dd1025f 100644 --- a/apps/nextjs/src/app/[locale]/init/page.tsx +++ b/apps/nextjs/src/app/[locale]/init/page.tsx @@ -13,6 +13,7 @@ import { BackToStart } from "./_steps/back"; import { InitFinish } from "./_steps/finish/init-finish"; import { InitGroup } from "./_steps/group/init-group"; import { InitImport } from "./_steps/import/init-import"; +import { InitIntegrations } from "./_steps/integrations/init-integrations"; import { InitSettings } from "./_steps/settings/init-settings"; import { InitStart } from "./_steps/start/init-start"; import { InitUser } from "./_steps/user/init-user"; @@ -23,6 +24,7 @@ const stepComponents: Record MaybePromise