Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(integrations): add checkbox to add app #2463

Open
wants to merge 1 commit into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
"use client";

import { useCallback } from "react";
import { useCallback, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { Alert, Button, Checkbox, Fieldset, Group, SegmentedControl, Stack, Text, TextInput } from "@mantine/core";
import {
Alert,
Button,
Checkbox,
Collapse,
Fieldset,
Group,
SegmentedControl,
Stack,
Text,
TextInput,
} from "@mantine/core";
import { IconInfoCircle } from "@tabler/icons-react";
import type { z } from "zod";
import { z } from "zod";

import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client";
import type { IntegrationKind, IntegrationSecretKind } from "@homarr/definitions";
import { getAllSecretKindOptions, getIntegrationName, integrationDefs } from "@homarr/definitions";
import { getAllSecretKindOptions, getIconUrl, getIntegrationName, integrationDefs } from "@homarr/definitions";
import type { UseFormReturnType } from "@homarr/form";
import { useZodForm } from "@homarr/form";
import { convertIntegrationTestConnectionError } from "@homarr/integrations/client";
Expand All @@ -26,11 +37,19 @@ interface NewIntegrationFormProps {
};
}

const formSchema = validation.integration.create.omit({ kind: true }).and(
z.object({
createApp: z.boolean(),
appHref: validation.app.manage.shape.href,
}),
);

export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) => {
const t = useI18n();
const secretKinds = getAllSecretKindOptions(searchParams.kind);
const router = useRouter();
const form = useZodForm(validation.integration.create.omit({ kind: true }), {
const [opened, setOpened] = useState(false);
const form = useZodForm(formSchema, {
initialValues: {
name: searchParams.name ?? getIntegrationName(searchParams.kind),
url: searchParams.url ?? "",
Expand All @@ -39,23 +58,60 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) =>
value: "",
})),
attemptSearchEngineCreation: true,
createApp: false,
appHref: "",
},
onValuesChange(values, previous) {
if (values.createApp !== previous.createApp) {
setOpened(values.createApp);
}
},
});
const { mutateAsync, isPending } = clientApi.integration.create.useMutation();

const { mutateAsync: createIntegrationAsync, isPending: isPendingIntegration } =
clientApi.integration.create.useMutation();
const { mutateAsync: createAppAsync, isPending: isPendingApp } = clientApi.app.create.useMutation();
const isPending = isPendingIntegration || isPendingApp;

const handleSubmitAsync = async (values: FormType) => {
await mutateAsync(
await createIntegrationAsync(
{
kind: searchParams.kind,
...values,
},
{
onSuccess: () => {
async onSuccess() {
showSuccessNotification({
title: t("integration.page.create.notification.success.title"),
message: t("integration.page.create.notification.success.message"),
});
void revalidatePathActionAsync("/manage/integrations").then(() => router.push("/manage/integrations"));

if (!values.createApp) {
await revalidatePathActionAsync("/manage/integrations").then(() => router.push("/manage/integrations"));
return;
}

const hasCustomHref = values.appHref !== null && values.appHref.trim().length >= 1;
await createAppAsync(
{
name: values.name,
href: hasCustomHref ? values.appHref : values.url,
iconUrl: getIconUrl(searchParams.kind),
description: null,
pingUrl: values.url,
},
{
async onSettled() {
await revalidatePathActionAsync("/manage/integrations").then(() => router.push("/manage/integrations"));
},
onError() {
showErrorNotification({
title: t("app.page.create.notification.error.title"),
message: t("app.page.create.notification.error.message"),
});
},
},
);
},
onError: (error) => {
const testConnectionError = convertIntegrationTestConnectionError(error.data?.error);
Expand Down Expand Up @@ -117,6 +173,16 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) =>
/>
)}

<Checkbox
{...form.getInputProps("createApp", { type: "checkbox" })}
label={t("integration.field.createApp.label")}
description={t("integration.field.createApp.description")}
/>

<Collapse in={opened}>
<TextInput placeholder={t("integration.field.appHref.placeholder")} {...form.getInputProps("appHref")} />
</Collapse>

<Group justify="end" align="center">
<Button variant="default" component={Link} href="/manage/integrations">
{t("common.action.backToOverview")}
Expand Down Expand Up @@ -166,4 +232,4 @@ const SecretKindsSegmentedControl = ({ secretKinds, form }: SecretKindsSegmented
return <SegmentedControl fullWidth data={secretKindGroups} onChange={onChange}></SegmentedControl>;
};

type FormType = Omit<z.infer<typeof validation.integration.create>, "kind">;
type FormType = z.infer<typeof formSchema>;
7 changes: 7 additions & 0 deletions packages/translation/src/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -675,6 +675,13 @@
"attemptSearchEngineCreation": {
"label": "Create Search Engine",
"description": "Integration \"{kind}\" can be used with the search engines. Check this to automatically configure the search engine."
},
"createApp": {
"label": "Create app",
"description": "Create an app with the same name and icon as the integration. Leave the input field below empty to create the app with the integration URL."
},
"appHref": {
"placeholder": "Custom app URL"
}
},
"action": {
Expand Down
Loading