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:
Carina Wollendorfer
2022-09-02 14:39:44 -06:00
committed by GitHub
co-authored by CarinaWolli zomars
parent bcb7861a04
commit 46f2142e5f
12 changed files with 773 additions and 2 deletions
@@ -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">
+41
View File
@@ -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&apos;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>
);
}