refactor(integrations): make onSuccess required and simplify new page
- Make onSuccess a required prop on NewIntegrationForm so the consumer always controls post-creation behavior. Remove internal router.push. - Add IntegrationNewFormWrapper client component to handle navigation for the server-rendered manage page. - Replace IntegrationPickerPage with a simple "back to overview" button when an invalid integration kind is provided. Delete unused integration-picker-page.tsx.
This commit is contained in:
4 files changed
+101
-83
No files matched your search
+26
@@ -0,0 +1,26 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import type { IntegrationKind } from "@homarr/definitions";
|
||||
|
||||
import { NewIntegrationForm } from "./_integration-new-form";
|
||||
|
||||
interface IntegrationNewFormWrapperProps {
|
||||
kind: IntegrationKind;
|
||||
initialUrl?: string;
|
||||
initialName?: string;
|
||||
}
|
||||
|
||||
export const IntegrationNewFormWrapper = ({ kind, initialUrl, initialName }: IntegrationNewFormWrapperProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<NewIntegrationForm
|
||||
kind={kind}
|
||||
initialUrl={initialUrl}
|
||||
initialName={initialName}
|
||||
onSuccess={() => router.push("/manage/integrations")}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { startTransition, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
@@ -48,8 +47,9 @@ interface NewIntegrationFormProps {
|
||||
kind: IntegrationKind;
|
||||
initialUrl?: string;
|
||||
initialName?: string;
|
||||
onSuccess?: () => void;
|
||||
onSuccess: () => void;
|
||||
onCancel?: () => void;
|
||||
isOnboarding?: boolean;
|
||||
}
|
||||
|
||||
const formSchema = integrationCreateSchema.omit({ kind: true, app: true }).and(
|
||||
@@ -60,13 +60,19 @@ const formSchema = integrationCreateSchema.omit({ kind: true, app: true }).and(
|
||||
}),
|
||||
);
|
||||
|
||||
export const NewIntegrationForm = ({ kind, initialUrl, initialName, onSuccess, onCancel }: NewIntegrationFormProps) => {
|
||||
export const NewIntegrationForm = ({
|
||||
kind,
|
||||
initialUrl,
|
||||
initialName,
|
||||
onSuccess,
|
||||
onCancel,
|
||||
isOnboarding = false,
|
||||
}: NewIntegrationFormProps) => {
|
||||
const t = useI18n();
|
||||
const secretKinds = getAllSecretKindOptions(kind);
|
||||
const hasUrlSecret = secretKinds.some((kinds) => kinds.includes("url"));
|
||||
const router = useRouter();
|
||||
const { data: session } = useSession();
|
||||
const canCreateApps = session?.user.permissions.includes("app-create") ?? false;
|
||||
const canCreateApps = !isOnboarding && (session?.user.permissions.includes("app-create") ?? false);
|
||||
|
||||
let url = initialUrl ?? getIntegrationDefaultUrl(kind) ?? "";
|
||||
if (hasUrlSecret) {
|
||||
@@ -80,18 +86,21 @@ export const NewIntegrationForm = ({ kind, initialUrl, initialName, onSuccess, o
|
||||
kind,
|
||||
value: "",
|
||||
})),
|
||||
attemptSearchEngineCreation: true,
|
||||
hasApp: true,
|
||||
appHref: url,
|
||||
attemptSearchEngineCreation: !isOnboarding,
|
||||
hasApp: !isOnboarding,
|
||||
appHref: isOnboarding ? null : url,
|
||||
appId: null,
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: createIntegrationAsync, isPending } = clientApi.integration.create.useMutation({
|
||||
const { mutateAsync: createIntegrationAsync, isPending: isCreatePending } = clientApi.integration.create.useMutation({
|
||||
async onSuccess() {
|
||||
await revalidatePathActionAsync("/manage/integrations");
|
||||
},
|
||||
});
|
||||
const { mutateAsync: createOnboardingIntegrationAsync, isPending: isOnboardingCreatePending } =
|
||||
clientApi.onboard.createIntegration.useMutation();
|
||||
const isPending = isCreatePending || isOnboardingCreatePending;
|
||||
const [error, setError] = useState<null | AnyMappedTestConnectionError>(null);
|
||||
|
||||
const handleSubmitAsync = async ({ appId, appHref, hasApp, ...values }: FormType) => {
|
||||
@@ -99,6 +108,39 @@ export const NewIntegrationForm = ({ kind, initialUrl, initialName, onSuccess, o
|
||||
? new URL(values.secrets.find((secret) => secret.kind === "url")?.value ?? values.url).origin
|
||||
: values.url;
|
||||
|
||||
const onMutationSuccess = (data: { error?: AnyMappedTestConnectionError } | undefined | void) => {
|
||||
if (data && "error" in data && data.error) {
|
||||
setError(data.error);
|
||||
showErrorNotification({
|
||||
title: t("integration.page.create.notification.error.title"),
|
||||
message: t("integration.page.create.notification.error.message"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
showSuccessNotification({
|
||||
title: t("integration.page.create.notification.success.title"),
|
||||
message: t("integration.page.create.notification.success.message"),
|
||||
});
|
||||
|
||||
onSuccess();
|
||||
};
|
||||
|
||||
const onMutationError = () => {
|
||||
showErrorNotification({
|
||||
title: t("integration.page.create.notification.error.title"),
|
||||
message: t("integration.page.create.notification.error.message"),
|
||||
});
|
||||
};
|
||||
|
||||
if (isOnboarding) {
|
||||
await createOnboardingIntegrationAsync(
|
||||
{ kind, name: values.name, url, secrets: values.secrets },
|
||||
{ onSuccess: onMutationSuccess, onError: onMutationError },
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const hasCustomHref = appHref !== null && appHref.trim().length >= 1;
|
||||
|
||||
const app = hasApp
|
||||
@@ -114,42 +156,8 @@ export const NewIntegrationForm = ({ kind, initialUrl, initialName, onSuccess, o
|
||||
: undefined;
|
||||
|
||||
await createIntegrationAsync(
|
||||
{
|
||||
kind,
|
||||
...values,
|
||||
url,
|
||||
app,
|
||||
},
|
||||
{
|
||||
onSuccess(data) {
|
||||
// We do it this way as we are unable to send a typesafe error through onError
|
||||
if (data?.error) {
|
||||
setError(data.error);
|
||||
showErrorNotification({
|
||||
title: t("integration.page.create.notification.error.title"),
|
||||
message: t("integration.page.create.notification.error.message"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
showSuccessNotification({
|
||||
title: t("integration.page.create.notification.success.title"),
|
||||
message: t("integration.page.create.notification.success.message"),
|
||||
});
|
||||
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
} else {
|
||||
router.push("/manage/integrations");
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
showErrorNotification({
|
||||
title: t("integration.page.create.notification.error.title"),
|
||||
message: t("integration.page.create.notification.error.message"),
|
||||
});
|
||||
},
|
||||
},
|
||||
{ kind, ...values, url, app },
|
||||
{ onSuccess: onMutationSuccess, onError: onMutationError },
|
||||
);
|
||||
};
|
||||
|
||||
@@ -185,7 +193,7 @@ export const NewIntegrationForm = ({ kind, initialUrl, initialName, onSuccess, o
|
||||
|
||||
{error !== null && <IntegrationTestConnectionError error={error} url={form.values.url} />}
|
||||
|
||||
{supportsSearchEngine && (
|
||||
{!isOnboarding && supportsSearchEngine && (
|
||||
<Checkbox
|
||||
label={t("integration.field.attemptSearchEngineCreation.label")}
|
||||
description={t("integration.field.attemptSearchEngineCreation.description", {
|
||||
@@ -195,7 +203,7 @@ export const NewIntegrationForm = ({ kind, initialUrl, initialName, onSuccess, o
|
||||
/>
|
||||
)}
|
||||
|
||||
<AppForm form={form} canCreateApps={canCreateApps} />
|
||||
{!isOnboarding && <AppForm form={form} canCreateApps={canCreateApps} />}
|
||||
|
||||
<Group justify="end" align="center">
|
||||
{onCancel ? (
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { Container, Group, Stack, Title } from "@mantine/core";
|
||||
import { Button, Center, Container, Group, Stack, Text, Title } from "@mantine/core";
|
||||
import { IconArrowLeft } from "@tabler/icons-react";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { auth } from "@homarr/auth/next";
|
||||
import { getIntegrationName, integrationKinds } from "@homarr/definitions";
|
||||
import { getScopedI18n } from "@homarr/translation/server";
|
||||
import { IntegrationAvatar } from "@homarr/ui";
|
||||
import { IntegrationAvatar, Link } from "@homarr/ui";
|
||||
|
||||
import { IntegrationPickerPage } from "~/components/integration/integration-picker-page";
|
||||
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
|
||||
import { env } from "~/env";
|
||||
import { NewIntegrationForm } from "./_integration-new-form";
|
||||
import { IntegrationNewFormWrapper } from "./_integration-new-form-wrapper";
|
||||
|
||||
interface NewIntegrationPageProps {
|
||||
searchParams: Promise<{
|
||||
@@ -34,11 +33,21 @@ export default async function IntegrationsNewPage(props: NewIntegrationPageProps
|
||||
return (
|
||||
<>
|
||||
<DynamicBreadcrumb />
|
||||
<Container size="xl">
|
||||
<Stack>
|
||||
<Title>{t("action.create")}</Title>
|
||||
<IntegrationPickerPage enableMockIntegration={env.UNSAFE_ENABLE_MOCK_INTEGRATION} />
|
||||
</Stack>
|
||||
<Container>
|
||||
<Center py="xl">
|
||||
<Stack align="center" gap="md">
|
||||
<Title order={3}>{t("action.create")}</Title>
|
||||
<Text c="dimmed">{t("page.list.search")}</Text>
|
||||
<Button
|
||||
variant="default"
|
||||
component={Link}
|
||||
href="/manage/integrations"
|
||||
leftSection={<IconArrowLeft size={16} />}
|
||||
>
|
||||
{t("page.list.title")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Center>
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
@@ -57,7 +66,7 @@ export default async function IntegrationsNewPage(props: NewIntegrationPageProps
|
||||
<IntegrationAvatar kind={currentKind} size="md" />
|
||||
<Title>{tCreate("title", { name: getIntegrationName(currentKind) })}</Title>
|
||||
</Group>
|
||||
<NewIntegrationForm kind={currentKind} initialUrl={searchParams.url} initialName={searchParams.name} />
|
||||
<IntegrationNewFormWrapper kind={currentKind} initialUrl={searchParams.url} initialName={searchParams.name} />
|
||||
</Stack>
|
||||
</Container>
|
||||
</>
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import type { IntegrationKind } from "@homarr/definitions";
|
||||
|
||||
import { IntegrationSelectGrid } from "./integration-select-grid";
|
||||
|
||||
interface IntegrationPickerPageProps {
|
||||
enableMockIntegration: boolean;
|
||||
}
|
||||
|
||||
export const IntegrationPickerPage = ({ enableMockIntegration }: IntegrationPickerPageProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
const handleSelect = useCallback(
|
||||
(kind: IntegrationKind) => {
|
||||
router.push(`/manage/integrations/new?kind=${kind}`);
|
||||
},
|
||||
[router],
|
||||
);
|
||||
|
||||
return <IntegrationSelectGrid onSelect={handleSelect} enableMockIntegration={enableMockIntegration} />;
|
||||
};
|
||||
Reference in new issue
Block a user