From d776078ecf93d192b36f552719fca063c753f5d8 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Thu, 13 Aug 2026 20:49:02 +0200 Subject: [PATCH] fix(onboarding): skip unfinished integrations --- packages/onboarding/src/setup-studio.tsx | 38 +++++++++--------------- packages/translation/src/lang/en.json | 3 +- 2 files changed, 15 insertions(+), 26 deletions(-) diff --git a/packages/onboarding/src/setup-studio.tsx b/packages/onboarding/src/setup-studio.tsx index 103471688..a75b35d41 100644 --- a/packages/onboarding/src/setup-studio.tsx +++ b/packages/onboarding/src/setup-studio.tsx @@ -121,9 +121,11 @@ interface IntegrationDraft { source: "manual" | "docker"; secretOption: number; secrets: { kind: IntegrationSecretKind; value: string }[]; - error: string | null; } +const isIntegrationDraftComplete = (draft: IntegrationDraft) => + isHttpUrl(draft.url) && draft.secrets.every((secret) => secret.value.trim().length > 0); + type DockerDiscoveryData = RouterOutputs["onboard"]["discoverDockerServices"]; type RuntimeCapabilitiesQuery = Omit< ReturnType, @@ -410,16 +412,7 @@ export const SetupStudio = ({ environment, assistantConfiguration }: OnboardingS setActiveSection("board"); throw new Error(t("board.nameDescription")); } - const draftsToApply = drafts; - const invalidDraft = draftsToApply.find( - (draft) => !isHttpUrl(draft.url) || draft.secrets.some((secret) => secret.value.trim().length === 0), - ); - if (invalidDraft) { - const message = t("connect.validationError", { name: invalidDraft.name }); - updateDraft(invalidDraft.id, { error: message }); - setActiveSection("connect"); - throw new Error(message); - } + const draftsToApply = drafts.filter(isIntegrationDraftComplete); const appsToCreate = discoveredApps.filter((app) => selectedAppIds.includes(app.sourceId)); const appWithoutAddress = appsToCreate.find((app) => !isHttpUrl(discoveredAppUrls[app.sourceId] ?? "")); if (appWithoutAddress) { @@ -580,13 +573,13 @@ export const SetupStudio = ({ environment, assistantConfiguration }: OnboardingS appError, appErrorSourceId, }; - const hasIntegrationsWithoutUrl = drafts.some((draft) => draft.url.trim().length === 0); + const hasIncompleteIntegrations = drafts.some((draft) => !isIntegrationDraftComplete(draft)); const continueDisabled = isApplying || (activeSection === "essentials" && serverOrigin.trim().length === 0); const continueOnboarding = () => { if (continueDisabled) return; if (activeSection === "review") { void applySetupAsync(); - } else if (activeSection === "connect" && hasIntegrationsWithoutUrl) { + } else if (activeSection === "connect" && hasIncompleteIntegrations) { setIncompleteIntegrationConfirmationOpened(true); } else { move(1); @@ -683,7 +676,7 @@ export const SetupStudio = ({ environment, assistantConfiguration }: OnboardingS ) : ( - {activeSection === "connect" && hasIntegrationsWithoutUrl ? ( + {activeSection === "connect" && hasIncompleteIntegrations ? ( setIncompleteIntegrationConfirmationOpened(false)} @@ -1228,8 +1221,7 @@ const IntegrationEditor = ({ const options = getAllSecretKindOptions(draft.kind); const apiKeyUrl = getIntegrationApiKeyUrl(draft.url, draft.kind); const documentationUrl = getIntegrationDocumentationUrl(draft.kind); - const needsConfiguration = - !isHttpUrl(draft.url) || draft.secrets.some((secret) => secret.value.trim().length === 0) || Boolean(draft.error); + const needsConfiguration = !isIntegrationDraftComplete(draft); return ( update({ name: event.currentTarget.value, error: null })} + onChange={(event) => update({ name: event.currentTarget.value })} /> update({ url: event.currentTarget.value, urlOverridden: true, error: null })} + onChange={(event) => update({ url: event.currentTarget.value, urlOverridden: true })} /> {options.length > 1 ? ( @@ -1289,7 +1281,6 @@ const IntegrationEditor = ({ value={secret.value} onChange={(event) => update({ - error: null, secrets: draft.secrets.map((item, itemIndex) => itemIndex === index ? { ...item, value: event.currentTarget.value } : item, ), @@ -1319,7 +1310,6 @@ const IntegrationEditor = ({ ) : null} - {draft.error ? {draft.error} : null} @@ -1751,7 +1741,7 @@ const Review = (props: StudioSectionProps) => { { icon: IconPlugConnected, label: t("integrations"), - value: t("integrationsValue", { count: props.drafts.length }), + value: t("integrationsValue", { count: props.drafts.filter(isIntegrationDraftComplete).length }), }, { icon: IconApps, @@ -1829,7 +1819,7 @@ const Review = (props: StudioSectionProps) => { const getPreviewWidgetKinds = (props: StudioSectionProps) => [ ...new Set([ ...generalWidgets, - ...props.drafts.flatMap((draft) => getWidgetKindsForIntegration(draft.kind)), + ...props.drafts.filter(isIntegrationDraftComplete).flatMap((draft) => getWidgetKindsForIntegration(draft.kind)), ...props.discoveredApps .filter((app) => props.selectedAppIds.includes(app.sourceId)) .flatMap((app) => (app.widgetKind ? [app.widgetKind] : [])), @@ -1837,7 +1827,8 @@ const getPreviewWidgetKinds = (props: StudioSectionProps) => [ ]; const getPreviewAppCount = (props: StudioSectionProps) => - props.drafts.length + props.discoveredApps.filter((app) => props.selectedAppIds.includes(app.sourceId)).length; + props.drafts.filter(isIntegrationDraftComplete).length + + props.discoveredApps.filter((app) => props.selectedAppIds.includes(app.sourceId)).length; const createDraft = ({ id, @@ -1858,5 +1849,4 @@ const createDraft = ({ source, secretOption: 0, secrets: (getAllSecretKindOptions(kind)[0] ?? []).map((secretKind) => ({ kind: secretKind, value: "" })), - error: null, }); diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json index bea32cb7f..2cbd0d235 100644 --- a/packages/translation/src/lang/en.json +++ b/packages/translation/src/lang/en.json @@ -129,8 +129,7 @@ "openGuide": "Open Homarr's setup guide for this service", "needsConfiguration": "Needs configuration", "configured": "Configured", - "validationError": "Complete the URL and required credentials for {name}, or remove it from the selection.", - "incompleteConfirmation": "Some integrations need to be configured, are you sure?" + "incompleteConfirmation": "Unfinished integrations will be skipped. Continue without them?" }, "board": { "title": "Shape the board before it is built",