V2 for app setup pages (#4086)
* add v2 for app setup pages * create v2 app setup page * Build fixes Co-authored-by: CarinaWolli <wollencarina@gmail.com> Co-authored-by: zomars <zomars@me.com>
This commit is contained in:
co-authored by
CarinaWolli
zomars
parent
bcb7861a04
commit
46f2142e5f
@@ -151,7 +151,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
|
||||
const RequestSentMessage = () => {
|
||||
return (
|
||||
<div className="flex ml-1 mr-8 text-gray-500" data-testid="request_reschedule_sent">
|
||||
<div className="ml-1 mr-8 flex text-gray-500" data-testid="request_reschedule_sent">
|
||||
<Icon.FiSend className="-mt-[1px] w-4 rotate-45" />
|
||||
<p className="ml-2 ">{t("reschedule_request_sent")}</p>
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,7 @@ export default SkeletonLoader;
|
||||
|
||||
function SkeletonItem() {
|
||||
return (
|
||||
<li className="flex group w-full items-center justify-between px-2 py-4 sm:px-6">
|
||||
<li className="group flex w-full items-center justify-between px-2 py-4 sm:px-6">
|
||||
<div className="flex-grow truncate text-sm">
|
||||
<div className="flex">
|
||||
<div className="flex flex-col space-y-2">
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { GetStaticPaths, InferGetStaticPropsType } from "next";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { getStaticProps } from "@calcom/app-store/_pages/setup/_getStaticProps";
|
||||
import { AppSetupPage } from "@calcom/app-store/_pages/v2/setup";
|
||||
import Loader from "@calcom/ui/Loader";
|
||||
|
||||
export default function SetupInformation(props: InferGetStaticPropsType<typeof getStaticProps>) {
|
||||
const router = useRouter();
|
||||
const slug = router.query.slug as string;
|
||||
const { status } = useSession();
|
||||
|
||||
if (status === "loading") {
|
||||
return (
|
||||
<div className="absolute z-50 flex h-screen w-full items-center bg-gray-200">
|
||||
<Loader />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === "unauthenticated") {
|
||||
router.replace({
|
||||
pathname: "/auth/login",
|
||||
query: {
|
||||
callbackUrl: `/apps/${slug}/setup`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return <AppSetupPage slug={`${slug}-V2`} {...props} />;
|
||||
}
|
||||
|
||||
export const getStaticPaths: GetStaticPaths = async () => {
|
||||
return {
|
||||
paths: [],
|
||||
fallback: "blocking",
|
||||
};
|
||||
};
|
||||
|
||||
export { getStaticProps };
|
||||
@@ -0,0 +1,20 @@
|
||||
import { GetStaticPropsContext } from "next";
|
||||
|
||||
export const AppSetupPageMap = {
|
||||
zapier: import("../../../zapier/pages/setup/_getStaticProps"),
|
||||
};
|
||||
|
||||
export const getStaticProps = async (ctx: GetStaticPropsContext) => {
|
||||
const { slug } = ctx.params || {};
|
||||
if (typeof slug !== "string") return { notFound: true } as const;
|
||||
|
||||
if (!(slug in AppSetupPageMap)) return { props: {} };
|
||||
|
||||
const page = await AppSetupPageMap[slug as keyof typeof AppSetupPageMap];
|
||||
|
||||
if (!page.getStaticProps) return { props: {} };
|
||||
|
||||
const props = await page.getStaticProps(ctx);
|
||||
|
||||
return props;
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
import { DynamicComponent } from "../../../_components/DynamicComponent";
|
||||
|
||||
export const AppSetupMap = {
|
||||
"apple-calendar-V2": dynamic(() => import("../../../applecalendar/pages/v2/setup")),
|
||||
"exchange2013-calendar-V2": dynamic(() => import("../../../exchange2013calendar/pages/v2/setup")),
|
||||
"exchange2016-calendar-V2": dynamic(() => import("../../../exchange2016calendar/pages/v2/setup")),
|
||||
"caldav-calendar-V2": dynamic(() => import("../../../caldavcalendar/pages/v2/setup")),
|
||||
"zapier-V2": dynamic(() => import("../../../zapier/pages/v2/setup")),
|
||||
"closecom-V2": dynamic(() => import("../../../closecomothercalendar/pages/v2/setup")),
|
||||
};
|
||||
|
||||
export const AppSetupPage = (props: { slug: string }) => {
|
||||
return <DynamicComponent<typeof AppSetupMap> componentMap={AppSetupMap} {...props} />;
|
||||
};
|
||||
|
||||
export default AppSetupPage;
|
||||
@@ -0,0 +1,101 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Alert, Button, Form, TextField } from "@calcom/ui/v2";
|
||||
|
||||
export default function AppleCalendarSetup() {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const form = useForm({
|
||||
defaultValues: {
|
||||
username: "",
|
||||
password: "",
|
||||
},
|
||||
});
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-gray-200">
|
||||
<div className="m-auto rounded bg-white p-5 md:w-[560px] md:p-10">
|
||||
<div className="flex flex-col space-y-5 md:flex-row md:space-y-0 md:space-x-5">
|
||||
<div>
|
||||
{/* eslint-disable @next/next/no-img-element */}
|
||||
<img
|
||||
src="/api/app-store/applecalendar/icon.svg"
|
||||
alt="Apple Calendar"
|
||||
className="h-12 w-12 max-w-2xl"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-gray-600">{t("connect_apple_server")}</h1>
|
||||
|
||||
<div className="mt-1 text-sm">
|
||||
{t("apple_server_generate_password")}{" "}
|
||||
<a
|
||||
className="text-indigo-400"
|
||||
href="https://appleid.apple.com/account/manage"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
https://appleid.apple.com/account/manage
|
||||
</a>
|
||||
. {t("credentials_stored_encrypted")}
|
||||
</div>
|
||||
<div className="my-2 mt-3">
|
||||
<Form
|
||||
form={form}
|
||||
handleSubmit={async (values) => {
|
||||
setErrorMessage("");
|
||||
const res = await fetch("/api/integrations/applecalendar/add", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(values),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const json = await res.json();
|
||||
if (!res.ok) {
|
||||
setErrorMessage(json?.message || t("something_went_wrong"));
|
||||
} else {
|
||||
router.push(json.url);
|
||||
}
|
||||
}}>
|
||||
<fieldset className="space-y-2" disabled={form.formState.isSubmitting}>
|
||||
<TextField
|
||||
required
|
||||
type="text"
|
||||
{...form.register("username")}
|
||||
label="Apple ID"
|
||||
placeholder="appleid@domain.com"
|
||||
/>
|
||||
<TextField
|
||||
required
|
||||
type="password"
|
||||
{...form.register("password")}
|
||||
label={t("password")}
|
||||
placeholder="•••••••••••••"
|
||||
autoComplete="password"
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
{errorMessage && <Alert severity="error" title={errorMessage} className="my-4" />}
|
||||
<div className="mt-5 justify-end space-x-2 sm:mt-4 sm:flex">
|
||||
<Button type="button" color="secondary" onClick={() => router.back()}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button type="submit" loading={form.formState.isSubmitting}>
|
||||
{t("save")}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Toaster position="bottom-right" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Alert, Button, Form, TextField } from "@calcom/ui/v2";
|
||||
|
||||
export default function CalDavCalendarSetup() {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const form = useForm({
|
||||
defaultValues: {
|
||||
url: "",
|
||||
username: "",
|
||||
password: "",
|
||||
},
|
||||
});
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-gray-200">
|
||||
<div className="m-auto rounded bg-white p-5 md:w-[560px] md:p-10">
|
||||
<div className="flex flex-col space-y-5 md:flex-row md:space-y-0 md:space-x-5">
|
||||
<div>
|
||||
{/* eslint-disable @next/next/no-img-element */}
|
||||
<img
|
||||
src="/api/app-store/caldavcalendar/icon.svg"
|
||||
alt="CalDav Calendar"
|
||||
className="h-12 w-12 max-w-2xl"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-10/12 flex-col">
|
||||
<h1 className="text-gray-600">{t("connect_caldav_server")}</h1>
|
||||
<div className="mt-1 text-sm">{t("credentials_stored_encrypted")}</div>
|
||||
<div className="my-2 mt-3">
|
||||
<Form
|
||||
form={form}
|
||||
handleSubmit={async (values) => {
|
||||
setErrorMessage("");
|
||||
const res = await fetch("/api/integrations/caldavcalendar/add", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(values),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const json = await res.json();
|
||||
if (!res.ok) {
|
||||
setErrorMessage(json?.message || t("something_went_wrong"));
|
||||
} else {
|
||||
router.push(json.url);
|
||||
}
|
||||
}}>
|
||||
<fieldset className="space-y-2" disabled={form.formState.isSubmitting}>
|
||||
<TextField
|
||||
required
|
||||
type="text"
|
||||
{...form.register("url")}
|
||||
label={t("calendar_url")}
|
||||
placeholder="https://example.com/calendar"
|
||||
/>
|
||||
<TextField
|
||||
required
|
||||
type="text"
|
||||
{...form.register("username")}
|
||||
label={t("username")}
|
||||
placeholder="rickroll"
|
||||
/>
|
||||
<TextField
|
||||
required
|
||||
type="password"
|
||||
{...form.register("password")}
|
||||
label={t("password")}
|
||||
placeholder="•••••••••••••"
|
||||
autoComplete="password"
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
{errorMessage && <Alert severity="error" title={errorMessage} className="my-4" />}
|
||||
<div className="mt-5 justify-end space-x-2 sm:mt-4 sm:flex">
|
||||
<Button type="button" color="secondary" onClick={() => router.back()}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button type="submit" loading={form.formState.isSubmitting}>
|
||||
{t("save")}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Toaster position="bottom-right" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useRouter } from "next/router";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Check, X } from "react-feather";
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
import z from "zod";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, Form, showToast, TextField } from "@calcom/ui/v2";
|
||||
|
||||
const formSchema = z.object({
|
||||
api_key: z.string(),
|
||||
});
|
||||
|
||||
export default function CloseComSetup() {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const [testPassed, setTestPassed] = useState<boolean | undefined>(undefined);
|
||||
const [testLoading, setTestLoading] = useState<boolean>(false);
|
||||
|
||||
const form = useForm<{
|
||||
api_key: string;
|
||||
}>({
|
||||
resolver: zodResolver(formSchema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
if (testPassed === false) {
|
||||
setTestPassed(undefined);
|
||||
}
|
||||
}, 3000);
|
||||
return () => clearTimeout(timer);
|
||||
}, [testPassed]);
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-gray-200">
|
||||
<div className="m-auto rounded bg-white p-5 md:w-[520px] md:p-10">
|
||||
<div className="flex flex-col space-y-5 md:flex-row md:space-y-0 md:space-x-5">
|
||||
<div>
|
||||
{/* eslint-disable @next/next/no-img-element */}
|
||||
<img
|
||||
src="/api/app-store/closecomothercalendar/icon.svg"
|
||||
alt="Apple Calendar"
|
||||
className="h-12 w-12 max-w-2xl"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-gray-600">{t("provide_api_key")}</h1>
|
||||
|
||||
<div className="mt-1 text-sm">
|
||||
{t("generate_api_key_description")}{" "}
|
||||
<a
|
||||
className="text-indigo-400"
|
||||
href="https://app.close.com/settings/api/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
Close.com
|
||||
</a>
|
||||
. {t("it_stored_encrypted")}
|
||||
</div>
|
||||
<div className="my-2 mt-3">
|
||||
<Form
|
||||
form={form}
|
||||
handleSubmit={async (values) => {
|
||||
const res = await fetch("/api/integrations/closecomothercalendar/add", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(values),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
if (res.ok) {
|
||||
router.push(json.url);
|
||||
} else {
|
||||
showToast(json.message, "error");
|
||||
}
|
||||
}}>
|
||||
<fieldset className="space-y-2" disabled={form.formState.isSubmitting}>
|
||||
<Controller
|
||||
name="api_key"
|
||||
control={form.control}
|
||||
render={({ field: { onBlur, onChange } }) => (
|
||||
<TextField
|
||||
className="my-0"
|
||||
onBlur={onBlur}
|
||||
disabled={testPassed === true}
|
||||
name="api_key"
|
||||
placeholder="api_xyz..."
|
||||
onChange={async (e) => {
|
||||
onChange(e.target.value);
|
||||
form.setValue("api_key", e.target.value);
|
||||
await form.trigger("api_key");
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</fieldset>
|
||||
<div className="mt-5 justify-end space-x-2 sm:mt-4 sm:flex">
|
||||
<Button type="button" color="secondary" onClick={() => router.back()}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
loading={testLoading}
|
||||
disabled={testPassed === true}
|
||||
StartIcon={testPassed !== undefined ? (testPassed ? Check : X) : undefined}
|
||||
className={
|
||||
testPassed !== undefined
|
||||
? testPassed
|
||||
? " !bg-green-100 !text-green-700 hover:bg-green-100"
|
||||
: "!border-red-700 bg-red-100 !text-red-700 hover:bg-red-100"
|
||||
: "secondary"
|
||||
}
|
||||
color={testPassed === true ? "minimal" : "secondary"}
|
||||
onClick={async () => {
|
||||
const check = await form.trigger("api_key");
|
||||
if (!check) return;
|
||||
const api_key = form.getValues("api_key");
|
||||
setTestLoading(true);
|
||||
const res = await fetch("/api/integrations/closecomothercalendar/check", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ api_key }),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status === 200) {
|
||||
setTestPassed(true);
|
||||
} else {
|
||||
setTestPassed(false);
|
||||
}
|
||||
setTestLoading(false);
|
||||
}}>
|
||||
{t(
|
||||
testPassed !== undefined ? (testPassed ? "test_passed" : "test_failed") : "test_api_key"
|
||||
)}
|
||||
</Button>
|
||||
<Button type="submit" loading={form.formState.isSubmitting}>
|
||||
{t("save")}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Toaster position="bottom-right" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Alert, Button, Form, TextField } from "@calcom/ui/v2";
|
||||
|
||||
export default function Exchange2013CalendarSetup() {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const form = useForm({
|
||||
defaultValues: {
|
||||
username: "",
|
||||
password: "",
|
||||
url: process.env.EXCHANGE_DEFAULT_EWS_URL || "",
|
||||
},
|
||||
});
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-gray-200">
|
||||
<div className="m-auto rounded bg-white p-5 md:w-[560px] md:p-10">
|
||||
<div>
|
||||
<img
|
||||
src="/api/app-store/exchange2013calendar/icon.svg"
|
||||
alt="Exchange 2013 Calendar"
|
||||
className="h-12 w-12 max-w-2xl"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-10/12 flex-col">
|
||||
<h1 className="text-gray-600">{t("add_exchange2013")}</h1>
|
||||
<div className="mt-1 text-sm">{t("credentials_stored_encrypted")}</div>
|
||||
<div className="my-2 mt-3">
|
||||
<Form
|
||||
form={form}
|
||||
handleSubmit={async (values) => {
|
||||
setErrorMessage("");
|
||||
const res = await fetch("/api/integrations/exchange2013calendar/add", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(values),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const json = await res.json();
|
||||
if (!res.ok) {
|
||||
setErrorMessage(json?.message || t("something_went_wrong"));
|
||||
} else {
|
||||
router.push(json.url);
|
||||
}
|
||||
}}>
|
||||
<fieldset className="space-y-2" disabled={form.formState.isSubmitting}>
|
||||
<TextField
|
||||
required
|
||||
type="text"
|
||||
{...form.register("url")}
|
||||
label={t("calendar_url")}
|
||||
placeholder="https://example.com/Ews/Exchange.asmx"
|
||||
/>
|
||||
<TextField
|
||||
required
|
||||
type="text"
|
||||
{...form.register("username")}
|
||||
label="E-Mail"
|
||||
placeholder="rickroll@example.com"
|
||||
/>
|
||||
<TextField
|
||||
required
|
||||
type="password"
|
||||
{...form.register("password")}
|
||||
label="Password"
|
||||
placeholder="•••••••••••••"
|
||||
autoComplete="password"
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
{errorMessage && <Alert severity="error" title={errorMessage} className="my-4" />}
|
||||
<div className="mt-5 justify-end space-x-2 sm:mt-4 sm:flex">
|
||||
<Button type="button" color="secondary" onClick={() => router.back()}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button type="submit" loading={form.formState.isSubmitting}>
|
||||
{t("save")}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Toaster position="bottom-right" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Alert, Button, Form, TextField } from "@calcom/ui/v2";
|
||||
|
||||
export default function Exchange2016CalendarSetup() {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const form = useForm({
|
||||
defaultValues: {
|
||||
username: "",
|
||||
password: "",
|
||||
url: process.env.EXCHANGE_DEFAULT_EWS_URL || "",
|
||||
},
|
||||
});
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-gray-200">
|
||||
<div className="m-auto rounded bg-white p-5 md:w-[560px] md:p-10">
|
||||
<div>
|
||||
<img
|
||||
src="/api/app-store/exchange2016calendar/icon.svg"
|
||||
alt="Exchange 2016 Calendar"
|
||||
className="h-12 w-12 max-w-2xl"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-10/12 flex-col">
|
||||
<h1 className="text-gray-600">{t("add_exchange2016")}</h1>
|
||||
<div className="mt-1 text-sm">{t("credentials_stored_encrypted")}</div>
|
||||
<div className="my-2 mt-3">
|
||||
<Form
|
||||
form={form}
|
||||
handleSubmit={async (values) => {
|
||||
setErrorMessage("");
|
||||
const res = await fetch("/api/integrations/exchange2016calendar/add", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(values),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const json = await res.json();
|
||||
if (!res.ok) {
|
||||
setErrorMessage(json?.message || t("something_went_wrong"));
|
||||
} else {
|
||||
router.push(json.url);
|
||||
}
|
||||
}}>
|
||||
<fieldset className="space-y-2" disabled={form.formState.isSubmitting}>
|
||||
<TextField
|
||||
required
|
||||
type="text"
|
||||
{...form.register("url")}
|
||||
label={t("calendar_url")}
|
||||
placeholder="https://example.com/Ews/Exchange.asmx"
|
||||
/>
|
||||
<TextField
|
||||
required
|
||||
type="text"
|
||||
{...form.register("username")}
|
||||
label="E-Mail"
|
||||
placeholder="rickroll@example.com"
|
||||
/>
|
||||
<TextField
|
||||
required
|
||||
type="password"
|
||||
{...form.register("password")}
|
||||
label="Password"
|
||||
placeholder="•••••••••••••"
|
||||
autoComplete="password"
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
{errorMessage && <Alert severity="error" title={errorMessage} className="my-4" />}
|
||||
<div className="mt-5 justify-end space-x-2 sm:mt-4 sm:flex">
|
||||
<Button type="button" color="secondary" onClick={() => router.back()}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button type="submit" loading={form.formState.isSubmitting}>
|
||||
{t("save")}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Toaster position="bottom-right" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { GetStaticPropsContext } from "next";
|
||||
|
||||
import getAppKeysFromSlug from "../../../../_utils/getAppKeysFromSlug";
|
||||
|
||||
export interface IZapierSetupProps {
|
||||
inviteLink: string;
|
||||
}
|
||||
|
||||
export const getStaticProps = async (ctx: GetStaticPropsContext) => {
|
||||
if (typeof ctx.params?.slug !== "string") return { notFound: true } as const;
|
||||
let inviteLink = "";
|
||||
const appKeys = await getAppKeysFromSlug("zapier");
|
||||
if (typeof appKeys.invite_link === "string") inviteLink = appKeys.invite_link;
|
||||
|
||||
return {
|
||||
props: {
|
||||
inviteLink,
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,128 @@
|
||||
import { ClipboardCopyIcon } from "@heroicons/react/solid";
|
||||
import { Trans } from "next-i18next";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button, Loader, showToast, Tooltip } from "@calcom/ui/v2";
|
||||
|
||||
export interface IZapierSetupProps {
|
||||
inviteLink: string;
|
||||
}
|
||||
|
||||
const ZAPIER = "zapier";
|
||||
|
||||
export default function ZapierSetup(props: IZapierSetupProps) {
|
||||
const [newApiKey, setNewApiKey] = useState("");
|
||||
const { t } = useLocale();
|
||||
const utils = trpc.useContext();
|
||||
const integrations = trpc.useQuery(["viewer.integrations", { variant: "other" }]);
|
||||
const oldApiKey = trpc.useQuery(["viewer.apiKeys.findKeyOfType", { appId: ZAPIER }]);
|
||||
|
||||
const deleteApiKey = trpc.useMutation("viewer.apiKeys.delete");
|
||||
const zapierCredentials: { credentialIds: number[] } | undefined = integrations.data?.items.find(
|
||||
(item: { type: string }) => item.type === "zapier_other"
|
||||
);
|
||||
const [credentialId] = zapierCredentials?.credentialIds || [false];
|
||||
const showContent = integrations.data && integrations.isSuccess && credentialId;
|
||||
const isCalDev = process.env.NEXT_PUBLIC_WEBAPP_URL === "https://app.cal.dev";
|
||||
|
||||
async function createApiKey() {
|
||||
const event = { note: "Zapier", expiresAt: null, appId: ZAPIER };
|
||||
const apiKey = await utils.client.mutation("viewer.apiKeys.create", event);
|
||||
if (oldApiKey.data) {
|
||||
deleteApiKey.mutate({
|
||||
id: oldApiKey.data.id,
|
||||
});
|
||||
}
|
||||
setNewApiKey(apiKey);
|
||||
}
|
||||
|
||||
if (integrations.isLoading) {
|
||||
return (
|
||||
<div className="absolute z-50 flex h-screen w-full items-center bg-gray-200">
|
||||
<Loader />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-gray-200">
|
||||
{showContent ? (
|
||||
<div className="m-auto max-w-[43em] overflow-auto rounded bg-white pb-10 md:p-10">
|
||||
<div className="md:flex md:flex-row">
|
||||
<div className="invisible md:visible">
|
||||
<img className="h-11" src="/api/app-store/zapier/icon.svg" alt="Zapier Logo" />
|
||||
</div>
|
||||
<div className="ml-2 mr-2 md:ml-5">
|
||||
<div className="text-gray-600">{t("setting_up_zapier")}</div>
|
||||
{!newApiKey ? (
|
||||
<>
|
||||
<div className="mt-1 text-xl">{t("generate_api_key")}:</div>
|
||||
<Button onClick={() => createApiKey()} className="mt-4 mb-4">
|
||||
{t("generate_api_key")}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="mt-1 text-xl">{t("your_unique_api_key")}</div>
|
||||
<div className="my-2 mt-3 flex">
|
||||
<div className="w-full rounded bg-gray-100 py-2 pl-2 pr-5">{newApiKey}</div>
|
||||
<Tooltip side="top" content="copy">
|
||||
<Button
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(newApiKey);
|
||||
showToast(t("api_key_copied"), "success");
|
||||
}}
|
||||
type="button"
|
||||
className="rounded-l-none px-4 text-base">
|
||||
<ClipboardCopyIcon className="mr-2 h-5 w-5 text-neutral-100" />
|
||||
{t("copy")}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="mt-2 mb-5 text-sm font-semibold text-gray-600">
|
||||
{t("copy_safe_api_key")}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<ol className="mt-5 mb-5 ml-5 mr-5 list-decimal">
|
||||
{isCalDev && (
|
||||
<li>
|
||||
{t("go_to")}
|
||||
<a href={props.inviteLink} className="text-orange-600 underline">
|
||||
{t("zapier_invite_link")}
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
<Trans i18nKey="zapier_setup_instructions">
|
||||
<li>Log into your Zapier account and create a new Zap.</li>
|
||||
<li>Select Cal.com as your Trigger app. Also choose a Trigger event.</li>
|
||||
<li>Choose your account and then enter your Unique API Key.</li>
|
||||
<li>Test your Trigger.</li>
|
||||
<li>You're set!</li>
|
||||
</Trans>
|
||||
</ol>
|
||||
<Link href="/apps/installed" passHref={true}>
|
||||
<Button color="secondary">{t("done")}</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-5 ml-5">
|
||||
<div>{t("install_zapier_app")}</div>
|
||||
<div className="mt-3">
|
||||
<Link href="/apps/zapier" passHref={true}>
|
||||
<Button>{t("go_to_app_store")}</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Toaster position="bottom-right" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user