Admin apps UI (#5494)
* Abstract app category navigation * Send key schema to frontend Co-authored-by: Omar López <zomars@users.noreply.github.com> * Render keys for apps on admin * Add enabled col for apps * Save app keys to DB * Add checks for admin role * Abstract setup components * Add AdminAppsList to setup wizard * Migrate to v10 tRPC * Default hide keys * Display enabled apps * Merge branch 'main' into admin-apps-ui * Toggle calendars * WIP * Add params and include AppCategoryNavigation * Refactor getEnabledApps * Add warning for disabling apps * Fallback to cal video when a video app is disabled * WIP send disabled email * Send email to all users of event types with payment app * Disable Stripe when app is disabled * Disable apps in event types * Send email to users on disabled apps * Send email based on what app was disabled * WIP type fix * Disable navigation to apps list if already setup * UI import fixes * Waits for session data before redirecting * Updates admin seeded password To comply with admin password requirements * Update yarn.lock * Flex fixes * Adds admin middleware * Clean up * WIP * WIP * NTS * Add dirName to app metadata * Upsert app if not in db * Upsert app if not in db * Add dirName to app metadata * Add keys to app packages w/ keys * Merge with main * Toggle show keys & on enable * Fix empty keys * Fix lark calendar metadata * Fix some type errors * Fix Lark metadata & check for category when upserting * More type fixes * Fix types & add keys to google cal * WIP * WIP * WIP * More type fixes * Fix type errors * Fix type errors * More type fixes * More type fixes * More type fixes * Feedback * Fixes default value * Feedback * Migrate credential invalid col default value "false" * Upsert app on saving keys * Clean up * Validate app keys on frontend * Add nonempty to app keys schemas * Add web3 * Listlocale filter on categories / category * Grab app metadata via category or categories * Show empty screen if no apps are enabled * Fix type checks * Fix type checks * Fix type checks * Fix type checks * Fix type checks * Fix type checks * Replace .nonempty() w/ .min(1) * Fix type error * Address feedback * Added migration to keep current apps enabled * Update apps.tsx * Fix bug * Add keys schema to Plausible app * Add appKeysSchema to zod.ts template * Update AdminAppsList.tsx Co-authored-by: Omar López <zomars@users.noreply.github.com> Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: zomars <zomars@me.com> Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
This commit is contained in:
parent
21dd1f4e95
commit
a9a295dc54
2
apps/api
2
apps/api
|
@ -1 +1 @@
|
||||||
Subproject commit 41d22c8ccb64f30a8f2a4e5ed106828e0c075027
|
Subproject commit 3d84ce68c9baa5d4ce7c85a37a9b8678f399b7a7
|
|
@ -60,9 +60,10 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
res.setHeader("Content-Type", "text/html");
|
res.setHeader("Content-Type", "text/html");
|
||||||
res.setHeader("Cache-Control", "no-cache, no-store, private, must-revalidate");
|
res.setHeader("Cache-Control", "no-cache, no-store, private, must-revalidate");
|
||||||
res.write(
|
res.write(
|
||||||
renderEmail("AttendeeScheduledEmail", {
|
renderEmail("DisabledAppEmail", {
|
||||||
attendee: evt.attendees[0],
|
appName: "Stripe",
|
||||||
calEvent: evt,
|
appType: ["payment"],
|
||||||
|
t,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
res.end();
|
res.end();
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
import { GetServerSideProps, InferGetServerSidePropsType } from "next";
|
||||||
import { InferGetStaticPropsType, NextPageContext } from "next";
|
|
||||||
import { ChangeEventHandler, useState } from "react";
|
import { ChangeEventHandler, useState } from "react";
|
||||||
|
|
||||||
import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry";
|
import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry";
|
||||||
|
@ -31,7 +30,10 @@ function AppsSearch({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Apps({ appStore, categories }: InferGetStaticPropsType<typeof getServerSideProps>) {
|
export default function Apps({
|
||||||
|
categories,
|
||||||
|
appStore,
|
||||||
|
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const [searchText, setSearchText] = useState<string | undefined>(undefined);
|
const [searchText, setSearchText] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
|
@ -42,7 +44,8 @@ export default function Apps({ appStore, categories }: InferGetStaticPropsType<t
|
||||||
subtitle={t("app_store_description")}
|
subtitle={t("app_store_description")}
|
||||||
actions={(className) => (
|
actions={(className) => (
|
||||||
<AppsSearch className={className} onChange={(e) => setSearchText(e.target.value)} />
|
<AppsSearch className={className} onChange={(e) => setSearchText(e.target.value)} />
|
||||||
)}>
|
)}
|
||||||
|
emptyStore={!appStore.length}>
|
||||||
{!searchText && (
|
{!searchText && (
|
||||||
<>
|
<>
|
||||||
<AppStoreCategories categories={categories} />
|
<AppStoreCategories categories={categories} />
|
||||||
|
@ -54,7 +57,7 @@ export default function Apps({ appStore, categories }: InferGetStaticPropsType<t
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getServerSideProps = async (context: NextPageContext) => {
|
export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||||
const ssg = await ssgInit(context);
|
const ssg = await ssgInit(context);
|
||||||
|
|
||||||
const session = await getSession(context);
|
const session = await getSession(context);
|
||||||
|
@ -77,7 +80,6 @@ export const getServerSideProps = async (context: NextPageContext) => {
|
||||||
}, {} as Record<string, number>);
|
}, {} as Record<string, number>);
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
trpcState: ssg.dehydrate(),
|
|
||||||
categories: Object.entries(categories)
|
categories: Object.entries(categories)
|
||||||
.map(([name, count]): { name: AppCategories; count: number } => ({
|
.map(([name, count]): { name: AppCategories; count: number } => ({
|
||||||
name: name as AppCategories,
|
name: name as AppCategories,
|
||||||
|
@ -87,6 +89,7 @@ export const getServerSideProps = async (context: NextPageContext) => {
|
||||||
return b.count - a.count;
|
return b.count - a.count;
|
||||||
}),
|
}),
|
||||||
appStore,
|
appStore,
|
||||||
|
trpcState: ssg.dehydrate(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import AdminAppsList from "@calcom/features/apps/AdminAppsList";
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
|
import { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||||
|
import { WizardForm } from "@calcom/ui";
|
||||||
|
|
||||||
|
import SetupFormStep1 from "./steps/SetupFormStep1";
|
||||||
|
import StepDone from "./steps/StepDone";
|
||||||
|
|
||||||
|
export default function Setup(props: inferSSRProps<typeof getServerSideProps>) {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const [isLoadingStep1, setIsLoadingStep1] = useState(false);
|
||||||
|
const shouldDisable = props.userCount !== 0;
|
||||||
|
|
||||||
|
const steps = [
|
||||||
|
{
|
||||||
|
title: t("administrator_user"),
|
||||||
|
description: t("lets_create_first_administrator_user"),
|
||||||
|
content: shouldDisable ? <StepDone /> : <SetupFormStep1 setIsLoading={setIsLoadingStep1} />,
|
||||||
|
isLoading: isLoadingStep1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("enable_apps"),
|
||||||
|
description: t("enable_apps_description"),
|
||||||
|
content: <AdminAppsList baseURL="/auth/setup" />,
|
||||||
|
isLoading: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<main className="flex items-center bg-gray-100 print:h-full">
|
||||||
|
<WizardForm href="/auth/setup" steps={steps} disableNavigation={shouldDisable} />
|
||||||
|
</main>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getServerSideProps = async () => {
|
||||||
|
const userCount = await prisma.user.count();
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
userCount,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
|
@ -1,34 +1,14 @@
|
||||||
import { CheckIcon } from "@heroicons/react/solid";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { GetServerSidePropsContext } from "next";
|
import { signIn } from "next-auth/react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useState } from "react";
|
|
||||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||||
import * as z from "zod";
|
import * as z from "zod";
|
||||||
|
|
||||||
import { isPasswordValid } from "@calcom/lib/auth";
|
import { isPasswordValid } from "@calcom/lib/auth";
|
||||||
|
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import prisma from "@calcom/prisma";
|
import { EmailField, Label, PasswordField, TextField } from "@calcom/ui";
|
||||||
import { inferSSRProps } from "@calcom/types/inferSSRProps";
|
|
||||||
import { EmailField, Label, PasswordField, TextField, WizardForm } from "@calcom/ui";
|
|
||||||
|
|
||||||
import { ssrInit } from "@server/lib/ssr";
|
|
||||||
|
|
||||||
const StepDone = () => {
|
|
||||||
const { t } = useLocale();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-36 my-6 flex flex-col items-center justify-center">
|
|
||||||
<div className="flex h-[72px] w-[72px] items-center justify-center rounded-full bg-gray-600 dark:bg-white">
|
|
||||||
<CheckIcon className="inline-block h-10 w-10 text-white dark:bg-white dark:text-gray-600" />
|
|
||||||
</div>
|
|
||||||
<div className="max-w-[420px] text-center">
|
|
||||||
<h2 className="mt-6 mb-1 text-lg font-medium dark:text-gray-300">{t("all_done")}</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const SetupFormStep1 = (props: { setIsLoading: (val: boolean) => void }) => {
|
const SetupFormStep1 = (props: { setIsLoading: (val: boolean) => void }) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
@ -83,13 +63,19 @@ const SetupFormStep1 = (props: { setIsLoading: (val: boolean) => void }) => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
router.replace(`/auth/login?email=${data.email_address.toLowerCase()}`);
|
await signIn("credentials", {
|
||||||
|
redirect: false,
|
||||||
|
callbackUrl: "/",
|
||||||
|
email: data.email_address.toLowerCase(),
|
||||||
|
password: data.password,
|
||||||
|
});
|
||||||
|
router.replace(`/auth/setup?step=2&category=calendar`);
|
||||||
} else {
|
} else {
|
||||||
router.replace("/auth/setup");
|
router.replace("/auth/setup");
|
||||||
}
|
}
|
||||||
}, onError);
|
}, onError);
|
||||||
|
|
||||||
const longWebsiteUrl = process.env.NEXT_PUBLIC_WEBSITE_URL.length > 30;
|
const longWebsiteUrl = WEBSITE_URL.length > 30;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormProvider {...formMethods}>
|
<FormProvider {...formMethods}>
|
||||||
|
@ -201,37 +187,4 @@ const SetupFormStep1 = (props: { setIsLoading: (val: boolean) => void }) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Setup(props: inferSSRProps<typeof getServerSideProps>) {
|
export default SetupFormStep1;
|
||||||
const { t } = useLocale();
|
|
||||||
const [isLoadingStep1, setIsLoadingStep1] = useState(false);
|
|
||||||
|
|
||||||
const steps = [
|
|
||||||
{
|
|
||||||
title: t("administrator_user"),
|
|
||||||
description: t("lets_create_first_administrator_user"),
|
|
||||||
content: props.userCount !== 0 ? <StepDone /> : <SetupFormStep1 setIsLoading={setIsLoadingStep1} />,
|
|
||||||
enabled: props.userCount === 0, // to check if the wizard should show buttons to navigate through more steps
|
|
||||||
isLoading: isLoadingStep1,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<main className="flex h-screen items-center bg-gray-100 print:h-full">
|
|
||||||
<WizardForm href="/auth/setup" steps={steps} containerClassname="max-w-sm" />
|
|
||||||
</main>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
|
||||||
const ssr = await ssrInit(context);
|
|
||||||
const userCount = await prisma.user.count();
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
trpcState: ssr.dehydrate(),
|
|
||||||
userCount,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { Icon } from "@calcom/ui";
|
||||||
|
|
||||||
|
const StepDone = () => {
|
||||||
|
const { t } = useLocale();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-36 my-6 flex flex-col items-center justify-center">
|
||||||
|
<div className="flex h-[72px] w-[72px] items-center justify-center rounded-full bg-gray-600 dark:bg-white">
|
||||||
|
<Icon.FiCheck className="inline-block h-10 w-10 text-white dark:bg-white dark:text-gray-600" />
|
||||||
|
</div>
|
||||||
|
<div className="max-w-[420px] text-center">
|
||||||
|
<h2 className="mt-6 mb-1 text-lg font-medium dark:text-gray-300">{t("all_done")}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StepDone;
|
|
@ -8,9 +8,10 @@ import { useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { StripeData } from "@calcom/app-store/stripepayment/lib/server";
|
import { StripeData } from "@calcom/app-store/stripepayment/lib/server";
|
||||||
import getApps, { getEventTypeAppData, getLocationOptions } from "@calcom/app-store/utils";
|
import { getEventTypeAppData, getLocationOptions } from "@calcom/app-store/utils";
|
||||||
import { EventLocationType, LocationObject } from "@calcom/core/location";
|
import { EventLocationType, LocationObject } from "@calcom/core/location";
|
||||||
import { parseBookingLimit, parseRecurringEvent, validateBookingLimitOrder } from "@calcom/lib";
|
import { parseBookingLimit, parseRecurringEvent, validateBookingLimitOrder } from "@calcom/lib";
|
||||||
|
import getEnabledApps from "@calcom/lib/apps/getEnabledApps";
|
||||||
import { CAL_URL } from "@calcom/lib/constants";
|
import { CAL_URL } from "@calcom/lib/constants";
|
||||||
import convertToNewDurationType from "@calcom/lib/convertToNewDurationType";
|
import convertToNewDurationType from "@calcom/lib/convertToNewDurationType";
|
||||||
import findDurationType from "@calcom/lib/findDurationType";
|
import findDurationType from "@calcom/lib/findDurationType";
|
||||||
|
@ -467,6 +468,9 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||||
const credentials = await prisma.credential.findMany({
|
const credentials = await prisma.credential.findMany({
|
||||||
where: {
|
where: {
|
||||||
userId: session.user.id,
|
userId: session.user.id,
|
||||||
|
app: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
|
@ -474,6 +478,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||||
key: true,
|
key: true,
|
||||||
userId: true,
|
userId: true,
|
||||||
appId: true,
|
appId: true,
|
||||||
|
invalid: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -526,7 +531,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||||
}
|
}
|
||||||
const currentUser = eventType.users.find((u) => u.id === session.user.id);
|
const currentUser = eventType.users.find((u) => u.id === session.user.id);
|
||||||
const t = await getTranslation(currentUser?.locale ?? "en", "common");
|
const t = await getTranslation(currentUser?.locale ?? "en", "common");
|
||||||
const integrations = getApps(credentials);
|
const integrations = await getEnabledApps(credentials);
|
||||||
const locationOptions = getLocationOptions(integrations, t);
|
const locationOptions = getLocationOptions(integrations, t);
|
||||||
|
|
||||||
const eventTypeObject = Object.assign({}, eventType, {
|
const eventTypeObject = Object.assign({}, eventType, {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { GetServerSidePropsContext } from "next";
|
import { GetServerSidePropsContext } from "next";
|
||||||
|
|
||||||
|
import AdminAppsList from "@calcom/features/apps/AdminAppsList";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { getAdminLayout as getLayout, Meta } from "@calcom/ui";
|
import { getAdminLayout as getLayout, Meta } from "@calcom/ui";
|
||||||
|
|
||||||
|
@ -10,7 +11,7 @@ function AdminAppsView() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Meta title={t("apps")} description={t("apps_description")} />
|
<Meta title={t("apps")} description={t("apps_description")} />
|
||||||
<h1>{t("apps_listing")}</h1>
|
<AdminAppsList baseURL="/settings/admin/apps" className="w-0" />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -1403,8 +1403,8 @@
|
||||||
"route_to": "Route to",
|
"route_to": "Route to",
|
||||||
"test_preview_description": "Test your routing form without submitting any data",
|
"test_preview_description": "Test your routing form without submitting any data",
|
||||||
"test_routing": "Test Routing",
|
"test_routing": "Test Routing",
|
||||||
"booking_limit_reached": "Booking Limit for this event type has been reached",
|
"payment_app_disabled": "An admin has disabled a payment app",
|
||||||
"fill_this_field": "Please fill in this field",
|
"edit_event_type": "Edit event type",
|
||||||
"collective_scheduling": "Collective Scheduling",
|
"collective_scheduling": "Collective Scheduling",
|
||||||
"make_it_easy_to_book": "Make it easy to book your team when everyone is available.",
|
"make_it_easy_to_book": "Make it easy to book your team when everyone is available.",
|
||||||
"find_the_best_person": "Find the best person available and cycle through your team.",
|
"find_the_best_person": "Find the best person available and cycle through your team.",
|
||||||
|
@ -1413,6 +1413,28 @@
|
||||||
"calcom_is_better_with_team": "Cal.com is better with teams",
|
"calcom_is_better_with_team": "Cal.com is better with teams",
|
||||||
"add_your_team_members": "Add your team members to your event types. Use collective scheduling to include everyone or find the most suitable person with round robin scheduling.",
|
"add_your_team_members": "Add your team members to your event types. Use collective scheduling to include everyone or find the most suitable person with round robin scheduling.",
|
||||||
"booking_limit_reached":"Booking Limit for this event type has been reached",
|
"booking_limit_reached":"Booking Limit for this event type has been reached",
|
||||||
|
"admin_has_disabled": "An admin has disabled {{appName}}",
|
||||||
|
"disabled_app_affects_event_type": "An admin has disabled {{appName}} which affects your event type {{eventType}}",
|
||||||
|
"disable_payment_app": "The admin has disabled {{appName}} which affects your event type {{title}}. Attendees are still able to book this type of event but will not be prompted to pay. You may hide hide the event type to prevent this until your admin renables your payment method.",
|
||||||
|
"payment_disabled_still_able_to_book": "Attendees are still able to book this type of event but will not be prompted to pay. You may hide hide the event type to prevent this until your admin reenables your payment method.",
|
||||||
|
"app_disabled_with_event_type": "The admin has disabled {{appName}} which affects your event type {{title}}.",
|
||||||
|
"app_disabled_video": "The admin has disabled {{appName}} which may affect your event types. If you have event types with {{appName}} as the location then it will default to Cal Video.",
|
||||||
|
"app_disabled_subject": "{{appName}} has been disabled",
|
||||||
|
"navigate_installed_apps": "Go to installed apps",
|
||||||
|
"disabled_calendar": "If you have another calendar installed new bookings will be added to it. If not then connect a new calendar so you do not miss any new bookings.",
|
||||||
|
"enable_apps": "Enable Apps",
|
||||||
|
"enable_apps_description": "Enable apps that users can integrate with Cal.com",
|
||||||
|
"app_is_enabled": "{{appName}} is enabled",
|
||||||
|
"app_is_disabled": "{{appName}} is disabled",
|
||||||
|
"keys_have_been_saved": "Keys have been saved",
|
||||||
|
"disable_app": "Disable App",
|
||||||
|
"disable_app_description": "Disabling this app could cause problems with how your users interact with Cal",
|
||||||
|
"edit_keys": "Edit Keys",
|
||||||
|
"apps_description": "Enable apps for your instance of Cal",
|
||||||
|
"no_available_apps": "There are no available apps",
|
||||||
|
"no_available_apps_description": "Please ensure there are apps in your deployment under 'packages/app-store'",
|
||||||
|
"no_apps": "There are no apps enabled in this instance of Cal",
|
||||||
|
"apps_settings": "Apps settings",
|
||||||
"fill_this_field": "Please fill in this field",
|
"fill_this_field": "Please fill in this field",
|
||||||
"options": "Options",
|
"options": "Options",
|
||||||
"enter_option": "Enter Option {{index}}",
|
"enter_option": "Enter Option {{index}}",
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 925f81ed92ee54cb1512c2836077e06d68c95123
|
Subproject commit c358039b1c1d0c5c0b7901c6489261c4e8e8c6e0
|
|
@ -62,6 +62,7 @@ function generateFiles() {
|
||||||
const browserOutput = [`import dynamic from "next/dynamic"`];
|
const browserOutput = [`import dynamic from "next/dynamic"`];
|
||||||
const metadataOutput = [];
|
const metadataOutput = [];
|
||||||
const schemasOutput = [];
|
const schemasOutput = [];
|
||||||
|
const appKeysSchemasOutput = [];
|
||||||
const serverOutput = [];
|
const serverOutput = [];
|
||||||
const appDirs: { name: string; path: string }[] = [];
|
const appDirs: { name: string; path: string }[] = [];
|
||||||
|
|
||||||
|
@ -190,6 +191,20 @@ function generateFiles() {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
appKeysSchemasOutput.push(
|
||||||
|
...getObjectExporter("appKeysSchemas", {
|
||||||
|
fileToBeImported: "zod.ts",
|
||||||
|
// Import path must have / even for windows and not \
|
||||||
|
importBuilder: (app) =>
|
||||||
|
`import { appKeysSchema as ${getVariableName(app.name)}_keys_schema } from "./${app.path.replace(
|
||||||
|
/\\/g,
|
||||||
|
"/"
|
||||||
|
)}/zod";`,
|
||||||
|
// Key must be appId as this is used by eventType metadata and lookup is by appId
|
||||||
|
entryBuilder: (app) => ` "${getAppId(app)}":${getVariableName(app.name)}_keys_schema ,`,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
browserOutput.push(
|
browserOutput.push(
|
||||||
...getObjectExporter("InstallAppButtonMap", {
|
...getObjectExporter("InstallAppButtonMap", {
|
||||||
fileToBeImported: "components/InstallAppButton.tsx",
|
fileToBeImported: "components/InstallAppButton.tsx",
|
||||||
|
@ -225,6 +240,7 @@ function generateFiles() {
|
||||||
["apps.server.generated.ts", serverOutput],
|
["apps.server.generated.ts", serverOutput],
|
||||||
["apps.browser.generated.tsx", browserOutput],
|
["apps.browser.generated.tsx", browserOutput],
|
||||||
["apps.schemas.generated.ts", schemasOutput],
|
["apps.schemas.generated.ts", schemasOutput],
|
||||||
|
["apps.keys-schemas.generated.ts", appKeysSchemasOutput],
|
||||||
];
|
];
|
||||||
filesToGenerate.forEach(([fileName, output]) => {
|
filesToGenerate.forEach(([fileName, output]) => {
|
||||||
fs.writeFileSync(`${APP_STORE_PATH}/${fileName}`, formatOutput(`${banner}${output.join("\n")}`));
|
fs.writeFileSync(`${APP_STORE_PATH}/${fileName}`, formatOutput(`${banner}${output.join("\n")}`));
|
||||||
|
|
|
@ -26,7 +26,10 @@ export async function getAppWithMetadata(app: { dirName: string }) {
|
||||||
|
|
||||||
/** Mainly to use in listings for the frontend, use in getStaticProps or getServerSideProps */
|
/** Mainly to use in listings for the frontend, use in getStaticProps or getServerSideProps */
|
||||||
export async function getAppRegistry() {
|
export async function getAppRegistry() {
|
||||||
const dbApps = await prisma.app.findMany({ select: { dirName: true, slug: true, categories: true } });
|
const dbApps = await prisma.app.findMany({
|
||||||
|
where: { enabled: true },
|
||||||
|
select: { dirName: true, slug: true, categories: true, enabled: true },
|
||||||
|
});
|
||||||
const apps = [] as Omit<App, "key">[];
|
const apps = [] as Omit<App, "key">[];
|
||||||
for await (const dbapp of dbApps) {
|
for await (const dbapp of dbApps) {
|
||||||
const app = await getAppWithMetadata(dbapp);
|
const app = await getAppWithMetadata(dbapp);
|
||||||
|
@ -51,7 +54,10 @@ export async function getAppRegistry() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAppRegistryWithCredentials(userId: number) {
|
export async function getAppRegistryWithCredentials(userId: number) {
|
||||||
const dbApps = await prisma.app.findMany({ include: { credentials: { where: { userId } } } });
|
const dbApps = await prisma.app.findMany({
|
||||||
|
where: { enabled: true },
|
||||||
|
include: { credentials: { where: { userId } } },
|
||||||
|
});
|
||||||
const apps = [] as (Omit<App, "key"> & {
|
const apps = [] as (Omit<App, "key"> & {
|
||||||
credentials: Credential[];
|
credentials: Credential[];
|
||||||
})[];
|
})[];
|
||||||
|
|
|
@ -26,7 +26,7 @@ export default function AppCard({
|
||||||
const [animationRef] = useAutoAnimate<HTMLDivElement>();
|
const [animationRef] = useAutoAnimate<HTMLDivElement>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-4 mt-2 rounded-md border border-gray-200">
|
<div className={`mb-4 mt-2 rounded-md border border-gray-200 ${!app.enabled && "grayscale"}`}>
|
||||||
<div className="p-4 text-sm sm:p-8">
|
<div className="p-4 text-sm sm:p-8">
|
||||||
<div className="flex w-full flex-col gap-2 sm:flex-row sm:gap-0">
|
<div className="flex w-full flex-col gap-2 sm:flex-row sm:gap-0">
|
||||||
{/* Don't know why but w-[42px] isn't working, started happening when I started using next/dynamic */}
|
{/* Don't know why but w-[42px] isn't working, started happening when I started using next/dynamic */}
|
||||||
|
@ -44,6 +44,7 @@ export default function AppCard({
|
||||||
{app?.isInstalled ? (
|
{app?.isInstalled ? (
|
||||||
<div className="ml-auto flex items-center">
|
<div className="ml-auto flex items-center">
|
||||||
<Switch
|
<Switch
|
||||||
|
disabled={!app.enabled}
|
||||||
onCheckedChange={(enabled) => {
|
onCheckedChange={(enabled) => {
|
||||||
if (switchOnClick) {
|
if (switchOnClick) {
|
||||||
switchOnClick(enabled);
|
switchOnClick(enabled);
|
||||||
|
@ -54,7 +55,7 @@ export default function AppCard({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<OmniInstallAppButton className="ml-auto flex items-center" appId={app?.slug} />
|
<OmniInstallAppButton className="ml-auto flex items-center" appId={app.slug} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
import { classNames } from "@calcom/lib";
|
||||||
|
import { HorizontalTabs, VerticalTabs } from "@calcom/ui";
|
||||||
|
|
||||||
|
import getAppCategories from "../_utils/getAppCategories";
|
||||||
|
|
||||||
|
const AppCategoryNavigation = ({
|
||||||
|
baseURL,
|
||||||
|
children,
|
||||||
|
containerClassname,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
baseURL: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
containerClassname: string;
|
||||||
|
className?: string;
|
||||||
|
}) => {
|
||||||
|
const appCategories = useMemo(() => getAppCategories(baseURL), [baseURL]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classNames("flex flex-col p-2 md:p-0 xl:flex-row", className)}>
|
||||||
|
<div className="hidden xl:block">
|
||||||
|
<VerticalTabs tabs={appCategories} sticky linkProps={{ shallow: true }} />
|
||||||
|
</div>
|
||||||
|
<div className="block overflow-x-scroll xl:hidden">
|
||||||
|
<HorizontalTabs tabs={appCategories} linkProps={{ shallow: true }} />
|
||||||
|
</div>
|
||||||
|
<main className={containerClassname}>{children}</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppCategoryNavigation;
|
|
@ -7,3 +7,5 @@ export const appDataSchema = eventTypeAppCardZod.merge(
|
||||||
isSunrise: z.boolean(),
|
isSunrise: z.boolean(),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const appKeysSchema = z.object({});
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { Icon } from "@calcom/ui";
|
||||||
|
|
||||||
|
const getAppCategories = (baseURL: string) => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: "calendar",
|
||||||
|
href: `${baseURL}/calendar`,
|
||||||
|
icon: Icon.FiCalendar,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "conferencing",
|
||||||
|
href: `${baseURL}/conferencing`,
|
||||||
|
icon: Icon.FiVideo,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "payment",
|
||||||
|
href: `${baseURL}/payment`,
|
||||||
|
icon: Icon.FiCreditCard,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "automation",
|
||||||
|
href: `${baseURL}/automation`,
|
||||||
|
icon: Icon.FiShare2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "analytics",
|
||||||
|
href: `${baseURL}/analytics`,
|
||||||
|
icon: Icon.FiBarChart,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "web3",
|
||||||
|
href: `${baseURL}/web3`,
|
||||||
|
icon: Icon.FiBarChart,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "other",
|
||||||
|
href: `${baseURL}/other`,
|
||||||
|
icon: Icon.FiGrid,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getAppCategories;
|
|
@ -20,6 +20,7 @@ export const metadata = {
|
||||||
url: "https://cal.com/",
|
url: "https://cal.com/",
|
||||||
verified: true,
|
verified: true,
|
||||||
email: "help@cal.com",
|
email: "help@cal.com",
|
||||||
|
dirName: "applecalendar",
|
||||||
} as AppMeta;
|
} as AppMeta;
|
||||||
|
|
||||||
export default metadata;
|
export default metadata;
|
||||||
|
|
|
@ -25,6 +25,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
key: symmetricEncrypt(JSON.stringify({ username, password }), process.env.CALENDSO_ENCRYPTION_KEY!),
|
key: symmetricEncrypt(JSON.stringify({ username, password }), process.env.CALENDSO_ENCRYPTION_KEY!),
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
appId: "apple-calendar",
|
appId: "apple-calendar",
|
||||||
|
invalid: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
/**
|
||||||
|
This file is autogenerated using the command `yarn app-store:build --watch`.
|
||||||
|
Don't modify this file manually.
|
||||||
|
**/
|
||||||
|
import { appKeysSchema as dailyvideo_keys_schema } from "./dailyvideo/zod";
|
||||||
|
import { appKeysSchema as routing_forms_keys_schema } from "./ee/routing-forms/zod";
|
||||||
|
import { appKeysSchema as fathom_keys_schema } from "./fathom/zod";
|
||||||
|
import { appKeysSchema as ga4_keys_schema } from "./ga4/zod";
|
||||||
|
import { appKeysSchema as giphy_keys_schema } from "./giphy/zod";
|
||||||
|
import { appKeysSchema as googlecalendar_keys_schema } from "./googlecalendar/zod";
|
||||||
|
import { appKeysSchema as hubspot_keys_schema } from "./hubspot/zod";
|
||||||
|
import { appKeysSchema as larkcalendar_keys_schema } from "./larkcalendar/zod";
|
||||||
|
import { appKeysSchema as office365calendar_keys_schema } from "./office365calendar/zod";
|
||||||
|
import { appKeysSchema as office365video_keys_schema } from "./office365video/zod";
|
||||||
|
import { appKeysSchema as plausible_keys_schema } from "./plausible/zod";
|
||||||
|
import { appKeysSchema as qr_code_keys_schema } from "./qr_code/zod";
|
||||||
|
import { appKeysSchema as rainbow_keys_schema } from "./rainbow/zod";
|
||||||
|
import { appKeysSchema as salesforce_keys_schema } from "./salesforce/zod";
|
||||||
|
import { appKeysSchema as stripepayment_keys_schema } from "./stripepayment/zod";
|
||||||
|
import { appKeysSchema as tandemvideo_keys_schema } from "./tandemvideo/zod";
|
||||||
|
import { appKeysSchema as vital_keys_schema } from "./vital/zod";
|
||||||
|
import { appKeysSchema as wordpress_keys_schema } from "./wordpress/zod";
|
||||||
|
import { appKeysSchema as zapier_keys_schema } from "./zapier/zod";
|
||||||
|
import { appKeysSchema as zoomvideo_keys_schema } from "./zoomvideo/zod";
|
||||||
|
|
||||||
|
export const appKeysSchemas = {
|
||||||
|
dailyvideo: dailyvideo_keys_schema,
|
||||||
|
"routing-forms": routing_forms_keys_schema,
|
||||||
|
fathom: fathom_keys_schema,
|
||||||
|
ga4: ga4_keys_schema,
|
||||||
|
giphy: giphy_keys_schema,
|
||||||
|
googlecalendar: googlecalendar_keys_schema,
|
||||||
|
hubspot: hubspot_keys_schema,
|
||||||
|
larkcalendar: larkcalendar_keys_schema,
|
||||||
|
office365calendar: office365calendar_keys_schema,
|
||||||
|
office365video: office365video_keys_schema,
|
||||||
|
plausible: plausible_keys_schema,
|
||||||
|
qr_code: qr_code_keys_schema,
|
||||||
|
rainbow: rainbow_keys_schema,
|
||||||
|
salesforce: salesforce_keys_schema,
|
||||||
|
stripe: stripepayment_keys_schema,
|
||||||
|
tandemvideo: tandemvideo_keys_schema,
|
||||||
|
vital: vital_keys_schema,
|
||||||
|
wordpress: wordpress_keys_schema,
|
||||||
|
zapier: zapier_keys_schema,
|
||||||
|
zoomvideo: zoomvideo_keys_schema,
|
||||||
|
};
|
|
@ -2,24 +2,46 @@
|
||||||
This file is autogenerated using the command `yarn app-store:build --watch`.
|
This file is autogenerated using the command `yarn app-store:build --watch`.
|
||||||
Don't modify this file manually.
|
Don't modify this file manually.
|
||||||
**/
|
**/
|
||||||
|
import { appDataSchema as dailyvideo_schema } from "./dailyvideo/zod";
|
||||||
import { appDataSchema as routing_forms_schema } from "./ee/routing-forms/zod";
|
import { appDataSchema as routing_forms_schema } from "./ee/routing-forms/zod";
|
||||||
import { appDataSchema as fathom_schema } from "./fathom/zod";
|
import { appDataSchema as fathom_schema } from "./fathom/zod";
|
||||||
import { appDataSchema as ga4_schema } from "./ga4/zod";
|
import { appDataSchema as ga4_schema } from "./ga4/zod";
|
||||||
import { appDataSchema as giphy_schema } from "./giphy/zod";
|
import { appDataSchema as giphy_schema } from "./giphy/zod";
|
||||||
|
import { appDataSchema as googlecalendar_schema } from "./googlecalendar/zod";
|
||||||
|
import { appDataSchema as hubspot_schema } from "./hubspot/zod";
|
||||||
|
import { appDataSchema as larkcalendar_schema } from "./larkcalendar/zod";
|
||||||
|
import { appDataSchema as office365calendar_schema } from "./office365calendar/zod";
|
||||||
|
import { appDataSchema as office365video_schema } from "./office365video/zod";
|
||||||
import { appDataSchema as plausible_schema } from "./plausible/zod";
|
import { appDataSchema as plausible_schema } from "./plausible/zod";
|
||||||
import { appDataSchema as qr_code_schema } from "./qr_code/zod";
|
import { appDataSchema as qr_code_schema } from "./qr_code/zod";
|
||||||
import { appDataSchema as rainbow_schema } from "./rainbow/zod";
|
import { appDataSchema as rainbow_schema } from "./rainbow/zod";
|
||||||
|
import { appDataSchema as salesforce_schema } from "./salesforce/zod";
|
||||||
import { appDataSchema as stripepayment_schema } from "./stripepayment/zod";
|
import { appDataSchema as stripepayment_schema } from "./stripepayment/zod";
|
||||||
|
import { appDataSchema as tandemvideo_schema } from "./tandemvideo/zod";
|
||||||
|
import { appDataSchema as vital_schema } from "./vital/zod";
|
||||||
import { appDataSchema as wordpress_schema } from "./wordpress/zod";
|
import { appDataSchema as wordpress_schema } from "./wordpress/zod";
|
||||||
|
import { appDataSchema as zapier_schema } from "./zapier/zod";
|
||||||
|
import { appDataSchema as zoomvideo_schema } from "./zoomvideo/zod";
|
||||||
|
|
||||||
export const appDataSchemas = {
|
export const appDataSchemas = {
|
||||||
|
dailyvideo: dailyvideo_schema,
|
||||||
"routing-forms": routing_forms_schema,
|
"routing-forms": routing_forms_schema,
|
||||||
fathom: fathom_schema,
|
fathom: fathom_schema,
|
||||||
ga4: ga4_schema,
|
ga4: ga4_schema,
|
||||||
giphy: giphy_schema,
|
giphy: giphy_schema,
|
||||||
|
googlecalendar: googlecalendar_schema,
|
||||||
|
hubspot: hubspot_schema,
|
||||||
|
larkcalendar: larkcalendar_schema,
|
||||||
|
office365calendar: office365calendar_schema,
|
||||||
|
office365video: office365video_schema,
|
||||||
plausible: plausible_schema,
|
plausible: plausible_schema,
|
||||||
qr_code: qr_code_schema,
|
qr_code: qr_code_schema,
|
||||||
rainbow: rainbow_schema,
|
rainbow: rainbow_schema,
|
||||||
|
salesforce: salesforce_schema,
|
||||||
stripe: stripepayment_schema,
|
stripe: stripepayment_schema,
|
||||||
|
tandemvideo: tandemvideo_schema,
|
||||||
|
vital: vital_schema,
|
||||||
wordpress: wordpress_schema,
|
wordpress: wordpress_schema,
|
||||||
|
zapier: zapier_schema,
|
||||||
|
zoomvideo: zoomvideo_schema,
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,6 +4,7 @@ import config from "./config.json";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
category: "other",
|
category: "other",
|
||||||
|
dirName: "around",
|
||||||
appData: {
|
appData: {
|
||||||
location: {
|
location: {
|
||||||
linkType: "static",
|
linkType: "static",
|
||||||
|
|
|
@ -20,6 +20,7 @@ export const metadata = {
|
||||||
url: "https://cal.com/",
|
url: "https://cal.com/",
|
||||||
verified: true,
|
verified: true,
|
||||||
email: "ali@cal.com",
|
email: "ali@cal.com",
|
||||||
|
dirName: "caldavcalendar",
|
||||||
} as AppMeta;
|
} as AppMeta;
|
||||||
|
|
||||||
export default metadata;
|
export default metadata;
|
||||||
|
|
|
@ -28,6 +28,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
),
|
),
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
appId: "caldav-calendar",
|
appId: "caldav-calendar",
|
||||||
|
invalid: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -4,6 +4,7 @@ import config from "./config.json";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
category: "other",
|
category: "other",
|
||||||
|
dirName: "caldavcalendar",
|
||||||
...config,
|
...config,
|
||||||
} as AppMeta;
|
} as AppMeta;
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ export const metadata = {
|
||||||
verified: true,
|
verified: true,
|
||||||
licenseRequired: true,
|
licenseRequired: true,
|
||||||
isProOnly: true,
|
isProOnly: true,
|
||||||
|
dirName: "closecom",
|
||||||
...config,
|
...config,
|
||||||
} as App;
|
} as App;
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,7 @@ export const metadata = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
key: { apikey: process.env.DAILY_API_KEY },
|
key: { apikey: process.env.DAILY_API_KEY },
|
||||||
|
dirName: "dailyvideo",
|
||||||
} as AppMeta;
|
} as AppMeta;
|
||||||
|
|
||||||
export default metadata;
|
export default metadata;
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const appKeysSchema = z.object({
|
||||||
|
api_key: z.string().min(1),
|
||||||
|
scale_plan: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const appDataSchema = z.object({});
|
|
@ -12,6 +12,7 @@ export const metadata = {
|
||||||
verified: true,
|
verified: true,
|
||||||
licenseRequired: true,
|
licenseRequired: true,
|
||||||
isProOnly: true,
|
isProOnly: true,
|
||||||
|
dirName: "routing-forms",
|
||||||
...config,
|
...config,
|
||||||
} as AppMeta;
|
} as AppMeta;
|
||||||
|
|
||||||
|
|
|
@ -44,3 +44,5 @@ export const zodRoutes = z
|
||||||
// TODO: This is a requirement right now that zod.ts file (if it exists) must have appDataSchema export(which is only required by apps having EventTypeAppCard interface)
|
// TODO: This is a requirement right now that zod.ts file (if it exists) must have appDataSchema export(which is only required by apps having EventTypeAppCard interface)
|
||||||
// This is a temporary solution and will be removed in future
|
// This is a temporary solution and will be removed in future
|
||||||
export const appDataSchema = z.any();
|
export const appDataSchema = z.any();
|
||||||
|
|
||||||
|
export const appKeysSchema = z.object({});
|
||||||
|
|
|
@ -21,6 +21,7 @@ export const metadata = {
|
||||||
url: "https://cal.com/",
|
url: "https://cal.com/",
|
||||||
verified: true,
|
verified: true,
|
||||||
email: "help@cal.com",
|
email: "help@cal.com",
|
||||||
|
dirName: "exchange2013calendar",
|
||||||
} as AppMeta;
|
} as AppMeta;
|
||||||
|
|
||||||
export default metadata;
|
export default metadata;
|
||||||
|
|
|
@ -34,6 +34,7 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
key: symmetricEncrypt(JSON.stringify(body), process.env.CALENDSO_ENCRYPTION_KEY!),
|
key: symmetricEncrypt(JSON.stringify(body), process.env.CALENDSO_ENCRYPTION_KEY!),
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
appId: "exchange2013-calendar",
|
appId: "exchange2013-calendar",
|
||||||
|
invalid: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -21,6 +21,7 @@ export const metadata = {
|
||||||
url: "https://cal.com/",
|
url: "https://cal.com/",
|
||||||
verified: true,
|
verified: true,
|
||||||
email: "help@cal.com",
|
email: "help@cal.com",
|
||||||
|
dirName: "exchange2016calendar",
|
||||||
} as AppMeta;
|
} as AppMeta;
|
||||||
|
|
||||||
export default metadata;
|
export default metadata;
|
||||||
|
|
|
@ -34,6 +34,7 @@ async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
key: symmetricEncrypt(JSON.stringify(body), process.env.CALENDSO_ENCRYPTION_KEY!),
|
key: symmetricEncrypt(JSON.stringify(body), process.env.CALENDSO_ENCRYPTION_KEY!),
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
appId: "exchange2016-calendar",
|
appId: "exchange2016-calendar",
|
||||||
|
invalid: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -9,6 +9,7 @@ export const metadata = {
|
||||||
reviews: 0,
|
reviews: 0,
|
||||||
trending: true,
|
trending: true,
|
||||||
verified: true,
|
verified: true,
|
||||||
|
dirName: "exchangecalendar",
|
||||||
...config,
|
...config,
|
||||||
} as App;
|
} as App;
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,13 @@ export async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const session = checkSession(req);
|
const session = checkSession(req);
|
||||||
const body = formSchema.parse(req.body);
|
const body = formSchema.parse(req.body);
|
||||||
const encrypted = symmetricEncrypt(JSON.stringify(body), process.env.CALENDSO_ENCRYPTION_KEY || "");
|
const encrypted = symmetricEncrypt(JSON.stringify(body), process.env.CALENDSO_ENCRYPTION_KEY || "");
|
||||||
const data = { type: "exchange_calendar", key: encrypted, userId: session.user?.id, appId: "exchange" };
|
const data = {
|
||||||
|
type: "exchange_calendar",
|
||||||
|
key: encrypted,
|
||||||
|
userId: session.user?.id,
|
||||||
|
appId: "exchange",
|
||||||
|
invalid: false,
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const service = new CalendarService({ id: 0, ...data });
|
const service = new CalendarService({ id: 0, ...data });
|
||||||
|
|
|
@ -4,6 +4,7 @@ import config from "./config.json";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
category: "analytics",
|
category: "analytics",
|
||||||
|
dirName: "fathom",
|
||||||
...config,
|
...config,
|
||||||
} as AppMeta;
|
} as AppMeta;
|
||||||
|
|
||||||
|
|
|
@ -7,3 +7,5 @@ export const appDataSchema = eventTypeAppCardZod.merge(
|
||||||
trackingId: z.string(),
|
trackingId: z.string(),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const appKeysSchema = z.object({});
|
||||||
|
|
|
@ -4,6 +4,7 @@ import config from "./config.json";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
category: "other",
|
category: "other",
|
||||||
|
dirName: "ga4",
|
||||||
...config,
|
...config,
|
||||||
} as AppMeta;
|
} as AppMeta;
|
||||||
|
|
||||||
|
|
|
@ -7,3 +7,5 @@ export const appDataSchema = eventTypeAppCardZod.merge(
|
||||||
trackingId: z.string(),
|
trackingId: z.string(),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const appKeysSchema = z.object({});
|
||||||
|
|
|
@ -22,6 +22,7 @@ export const metadata = {
|
||||||
verified: true,
|
verified: true,
|
||||||
extendsFeature: "EventType",
|
extendsFeature: "EventType",
|
||||||
email: "help@cal.com",
|
email: "help@cal.com",
|
||||||
|
dirName: "giphy",
|
||||||
} as AppMeta;
|
} as AppMeta;
|
||||||
|
|
||||||
export default metadata;
|
export default metadata;
|
||||||
|
|
|
@ -7,3 +7,7 @@ export const appDataSchema = eventTypeAppCardZod.merge(
|
||||||
thankYouPage: z.string().optional(),
|
thankYouPage: z.string().optional(),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const appKeysSchema = z.object({
|
||||||
|
app_key: z.string().min(1),
|
||||||
|
});
|
||||||
|
|
|
@ -21,6 +21,7 @@ export const metadata = {
|
||||||
url: "https://cal.com/",
|
url: "https://cal.com/",
|
||||||
verified: true,
|
verified: true,
|
||||||
email: "help@cal.com",
|
email: "help@cal.com",
|
||||||
|
dirName: "googlecalendar",
|
||||||
} as AppMeta;
|
} as AppMeta;
|
||||||
|
|
||||||
export default metadata;
|
export default metadata;
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const appDataSchema = z.object({});
|
||||||
|
|
||||||
|
export const appKeysSchema = z.object({
|
||||||
|
client_id: z.string().min(1),
|
||||||
|
client_secret: z.string().min(1),
|
||||||
|
redirect_uris: z.string().min(1),
|
||||||
|
});
|
|
@ -20,7 +20,7 @@ export const metadata = {
|
||||||
trending: false,
|
trending: false,
|
||||||
url: "https://cal.com/",
|
url: "https://cal.com/",
|
||||||
verified: true,
|
verified: true,
|
||||||
isGlobal: true,
|
isGlobal: false,
|
||||||
email: "help@cal.com",
|
email: "help@cal.com",
|
||||||
appData: {
|
appData: {
|
||||||
location: {
|
location: {
|
||||||
|
@ -29,6 +29,7 @@ export const metadata = {
|
||||||
label: "Google Meet",
|
label: "Google Meet",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
dirName: "googlevideo",
|
||||||
} as AppMeta;
|
} as AppMeta;
|
||||||
|
|
||||||
export default metadata;
|
export default metadata;
|
||||||
|
|
|
@ -21,6 +21,7 @@ export const metadata = {
|
||||||
title: "HubSpot CRM",
|
title: "HubSpot CRM",
|
||||||
trending: true,
|
trending: true,
|
||||||
email: "help@cal.com",
|
email: "help@cal.com",
|
||||||
|
dirName: "hubspot",
|
||||||
} as AppMeta;
|
} as AppMeta;
|
||||||
|
|
||||||
export default metadata;
|
export default metadata;
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const appKeysSchema = z.object({
|
||||||
|
client_id: z.string().min(1),
|
||||||
|
client_secret: z.string().min(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const appDataSchema = z.object({});
|
|
@ -30,6 +30,7 @@ export const metadata = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
key: { apikey: randomString(12) },
|
key: { apikey: randomString(12) },
|
||||||
|
dirName: "huddle01video",
|
||||||
} as AppMeta;
|
} as AppMeta;
|
||||||
|
|
||||||
export default metadata;
|
export default metadata;
|
||||||
|
|
|
@ -28,6 +28,7 @@ export const metadata = {
|
||||||
label: "Jitsi Video",
|
label: "Jitsi Video",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
dirName: "jitsivideo",
|
||||||
} as AppMeta;
|
} as AppMeta;
|
||||||
|
|
||||||
export default metadata;
|
export default metadata;
|
||||||
|
|
|
@ -20,6 +20,7 @@ export const metadata = {
|
||||||
url: "https://larksuite.com/",
|
url: "https://larksuite.com/",
|
||||||
verified: true,
|
verified: true,
|
||||||
email: "alan@larksuite.com",
|
email: "alan@larksuite.com",
|
||||||
|
dirName: "larkcalendar",
|
||||||
} as AppMeta;
|
} as AppMeta;
|
||||||
|
|
||||||
export default metadata;
|
export default metadata;
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const appDataSchema = z.object({});
|
||||||
|
|
||||||
|
export const appKeysSchema = z.object({
|
||||||
|
app_id: z.string().min(1),
|
||||||
|
app_secret: z.string().min(1),
|
||||||
|
open_verfication_token: z.string().min(1),
|
||||||
|
});
|
|
@ -4,6 +4,7 @@ import config from "./config.json";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
category: "automation",
|
category: "automation",
|
||||||
|
dirName: "n8n",
|
||||||
...config,
|
...config,
|
||||||
} as AppMeta;
|
} as AppMeta;
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ export const metadata = {
|
||||||
url: "https://cal.com/",
|
url: "https://cal.com/",
|
||||||
verified: true,
|
verified: true,
|
||||||
email: "help@cal.com",
|
email: "help@cal.com",
|
||||||
|
dirName: "office365calendar",
|
||||||
} as AppMeta;
|
} as AppMeta;
|
||||||
|
|
||||||
export default metadata;
|
export default metadata;
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const appKeysSchema = z.object({
|
||||||
|
client_id: z.string().min(1),
|
||||||
|
client_secret: z.string().min(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const appDataSchema = z.object({
|
||||||
|
client_id: z.string(),
|
||||||
|
client_secret: z.string(),
|
||||||
|
});
|
|
@ -1,31 +1,10 @@
|
||||||
import type { AppMeta } from "@calcom/types/App";
|
import type { AppMeta } from "@calcom/types/App";
|
||||||
|
|
||||||
import _package from "./package.json";
|
import config from "./config.json";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
name: "Microsoft 365/Teams (Requires work/school account)",
|
dirName: "office365video",
|
||||||
description: _package.description,
|
...config,
|
||||||
type: "office365_video",
|
|
||||||
imageSrc: "/api/app-store/office365video/icon.svg",
|
|
||||||
variant: "conferencing",
|
|
||||||
logo: "/api/app-store/office365video/icon.svg",
|
|
||||||
publisher: "Cal.com",
|
|
||||||
url: "https://www.microsoft.com/en-ca/microsoft-teams/group-chat-software",
|
|
||||||
verified: true,
|
|
||||||
rating: 4.3, // TODO: placeholder for now, pull this from TrustPilot or G2
|
|
||||||
reviews: 69, // TODO: placeholder for now, pull this from TrustPilot or G2
|
|
||||||
category: "video",
|
|
||||||
slug: "msteams",
|
|
||||||
title: "MS Teams (Requires work/school account)",
|
|
||||||
trending: true,
|
|
||||||
email: "help@cal.com",
|
|
||||||
appData: {
|
|
||||||
location: {
|
|
||||||
linkType: "dynamic",
|
|
||||||
type: "integrations:office365_video",
|
|
||||||
label: "MS Teams",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as AppMeta;
|
} as AppMeta;
|
||||||
|
|
||||||
export default metadata;
|
export default metadata;
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"name": "Microsoft 365/Teams (Requires work/school account)",
|
||||||
|
"description": "Microsoft Teams is a business communication platform and collaborative workspace included in Microsoft 365. It offers workspace chat and video conferencing, file storage, and application integration. Both web versions and desktop/mobile applications are available. NOTE: MUST HAVE A WORK / SCHOOL ACCOUNT",
|
||||||
|
"type": "office365_video",
|
||||||
|
"imageSrc": "/api/app-store/office365video/icon.svg",
|
||||||
|
"variant": "conferencing",
|
||||||
|
"logo": "/api/app-store/office365video/icon.svg",
|
||||||
|
"publisher": "Cal.com",
|
||||||
|
"url": "https://www.microsoft.com/en-ca/microsoft-teams/group-chat-software",
|
||||||
|
"verified": true,
|
||||||
|
"rating": 4.3,
|
||||||
|
"reviews": 69,
|
||||||
|
"category": "video",
|
||||||
|
"slug": "msteams",
|
||||||
|
"title": "MS Teams (Requires work/school account)",
|
||||||
|
"trending": true,
|
||||||
|
"email": "help@cal.com",
|
||||||
|
"appData": {
|
||||||
|
"location": {
|
||||||
|
"linkType": "dynamic",
|
||||||
|
"type": "integrations:office365_video",
|
||||||
|
"label": "MS Teams"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const appKeysSchema = z.object({
|
||||||
|
client_id: z.string().min(1),
|
||||||
|
client_secret: z.string().min(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const appDataSchema = z.object({
|
||||||
|
client_id: z.string(),
|
||||||
|
client_secret: z.string(),
|
||||||
|
});
|
|
@ -19,6 +19,7 @@ export const metadata = {
|
||||||
urlRegExp: "^http(s)?:\\/\\/(www\\.)?ping.gg\\/call\\/[a-zA-Z0-9]*",
|
urlRegExp: "^http(s)?:\\/\\/(www\\.)?ping.gg\\/call\\/[a-zA-Z0-9]*",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
dirName: "ping",
|
||||||
...config,
|
...config,
|
||||||
} as AppMeta;
|
} as AppMeta;
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import config from "./config.json";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
category: "other",
|
category: "other",
|
||||||
|
dirName: "pipedream",
|
||||||
...config,
|
...config,
|
||||||
} as AppMeta;
|
} as AppMeta;
|
||||||
|
|
||||||
|
|
|
@ -7,3 +7,5 @@ export const appDataSchema = eventTypeAppCardZod.merge(
|
||||||
trackedDomain: z.string(),
|
trackedDomain: z.string(),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const appKeysSchema = z.object({});
|
||||||
|
|
|
@ -4,6 +4,7 @@ import config from "./config.json";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
category: "other",
|
category: "other",
|
||||||
|
dirName: "qr_code",
|
||||||
...config,
|
...config,
|
||||||
} as AppMeta;
|
} as AppMeta;
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { eventTypeAppCardZod } from "../eventTypeAppCardZod";
|
import { eventTypeAppCardZod } from "../eventTypeAppCardZod";
|
||||||
|
|
||||||
export const appDataSchema = eventTypeAppCardZod;
|
export const appDataSchema = eventTypeAppCardZod;
|
||||||
|
|
||||||
|
export const appKeysSchema = z.object({});
|
||||||
|
|
|
@ -4,6 +4,7 @@ import config from "./config.json";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
category: "other",
|
category: "other",
|
||||||
|
dirName: "rainbow",
|
||||||
...config,
|
...config,
|
||||||
} as AppMeta;
|
} as AppMeta;
|
||||||
|
|
||||||
|
|
|
@ -8,3 +8,5 @@ export const appDataSchema = eventTypeAppCardZod.merge(
|
||||||
blockchainId: z.number().optional(),
|
blockchainId: z.number().optional(),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const appKeysSchema = z.object({});
|
||||||
|
|
|
@ -4,6 +4,7 @@ import config from "./config.json";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
category: "other",
|
category: "other",
|
||||||
|
dirName: "raycast",
|
||||||
...config,
|
...config,
|
||||||
} as AppMeta;
|
} as AppMeta;
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ export const metadata = {
|
||||||
linkType: "static",
|
linkType: "static",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
dirName: "riverside",
|
||||||
...config,
|
...config,
|
||||||
} as AppMeta;
|
} as AppMeta;
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import config from "./config.json";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
category: "other",
|
category: "other",
|
||||||
|
dirName: "salesforce",
|
||||||
...config,
|
...config,
|
||||||
} as AppMeta;
|
} as AppMeta;
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const appDataSchema = z.object({});
|
||||||
|
|
||||||
|
export const appKeysSchema = z.object({
|
||||||
|
consumer_key: z.string().min(1),
|
||||||
|
consumer_secret: z.string().min(1),
|
||||||
|
});
|
|
@ -4,6 +4,7 @@ import config from "./config.json";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
category: "other",
|
category: "other",
|
||||||
|
dirName: "sendgrid",
|
||||||
...config,
|
...config,
|
||||||
} as AppMeta;
|
} as AppMeta;
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import config from "./config.json";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
category: "other",
|
category: "other",
|
||||||
|
dirName: "signal",
|
||||||
...config,
|
...config,
|
||||||
} as AppMeta;
|
} as AppMeta;
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import config from "./config.json";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
category: "other",
|
category: "other",
|
||||||
|
dirName: "sirius_video",
|
||||||
...config,
|
...config,
|
||||||
} as AppMeta;
|
} as AppMeta;
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,7 @@ export const metadata = {
|
||||||
extendsFeature: "EventType",
|
extendsFeature: "EventType",
|
||||||
verified: true,
|
verified: true,
|
||||||
email: "help@cal.com",
|
email: "help@cal.com",
|
||||||
|
dirName: "stripepayment",
|
||||||
} as AppMeta;
|
} as AppMeta;
|
||||||
|
|
||||||
export default metadata;
|
export default metadata;
|
||||||
|
|
|
@ -8,3 +8,12 @@ export const appDataSchema = eventTypeAppCardZod.merge(
|
||||||
currency: z.string(),
|
currency: z.string(),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const appKeysSchema = z.object({
|
||||||
|
client_id: z.string().startsWith("ca_").min(1),
|
||||||
|
client_secret: z.string().startsWith("sk_").min(1),
|
||||||
|
public_key: z.string().startsWith("pk_").min(1),
|
||||||
|
webhook_secret: z.string().startsWith("whsec_").min(1),
|
||||||
|
payment_fee_fixed: z.number().int().min(0),
|
||||||
|
payment_fee_percentage: z.number().min(0),
|
||||||
|
});
|
||||||
|
|
|
@ -27,6 +27,7 @@ export const metadata = {
|
||||||
label: "Tandem Video",
|
label: "Tandem Video",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
dirName: "tandemvideo",
|
||||||
} as AppMeta;
|
} as AppMeta;
|
||||||
|
|
||||||
export default metadata;
|
export default metadata;
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const appDataSchema = z.object({});
|
||||||
|
|
||||||
|
export const appKeysSchema = z.object({
|
||||||
|
client_id: z.string().min(1),
|
||||||
|
client_secret: z.string().min(1),
|
||||||
|
base_url: z.string().min(1),
|
||||||
|
});
|
|
@ -4,6 +4,7 @@ import config from "./config.json";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
category: "other",
|
category: "other",
|
||||||
|
dirName: "telegram",
|
||||||
...config,
|
...config,
|
||||||
} as AppMeta;
|
} as AppMeta;
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import config from "./config.json";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
category: "other",
|
category: "other",
|
||||||
|
dirName: "typeform",
|
||||||
...config,
|
...config,
|
||||||
} as AppMeta;
|
} as AppMeta;
|
||||||
|
|
||||||
|
|
|
@ -26,10 +26,10 @@ const ALL_APPS_MAP = Object.keys(appStoreMetadata).reduce((store, key) => {
|
||||||
}, {} as Record<string, AppMeta>);
|
}, {} as Record<string, AppMeta>);
|
||||||
|
|
||||||
const credentialData = Prisma.validator<Prisma.CredentialArgs>()({
|
const credentialData = Prisma.validator<Prisma.CredentialArgs>()({
|
||||||
select: { id: true, type: true, key: true, userId: true, appId: true },
|
select: { id: true, type: true, key: true, userId: true, appId: true, invalid: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
type CredentialData = Prisma.CredentialGetPayload<typeof credentialData>;
|
export type CredentialData = Prisma.CredentialGetPayload<typeof credentialData>;
|
||||||
|
|
||||||
export enum InstalledAppVariants {
|
export enum InstalledAppVariants {
|
||||||
"conferencing" = "conferencing",
|
"conferencing" = "conferencing",
|
||||||
|
@ -146,6 +146,7 @@ function getApps(userCredentials: CredentialData[]) {
|
||||||
key: appMeta.key!,
|
key: appMeta.key!,
|
||||||
userId: +new Date().getTime(),
|
userId: +new Date().getTime(),
|
||||||
appId: appMeta.slug,
|
appId: appMeta.slug,
|
||||||
|
invalid: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -174,6 +175,10 @@ function getApps(userCredentials: CredentialData[]) {
|
||||||
return apps;
|
return apps;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getLocalAppMetadata() {
|
||||||
|
return ALL_APPS;
|
||||||
|
}
|
||||||
|
|
||||||
export function hasIntegrationInstalled(type: App["type"]): boolean {
|
export function hasIntegrationInstalled(type: App["type"]): boolean {
|
||||||
return ALL_APPS.some((app) => app.type === type && !!app.installed);
|
return ALL_APPS.some((app) => app.type === type && !!app.installed);
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ export const metadata = {
|
||||||
variant: "other",
|
variant: "other",
|
||||||
verified: true,
|
verified: true,
|
||||||
email: "support@tryvital.io",
|
email: "support@tryvital.io",
|
||||||
|
dirName: "vital",
|
||||||
} as AppMeta;
|
} as AppMeta;
|
||||||
|
|
||||||
export default metadata;
|
export default metadata;
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const appDataSchema = z.object({});
|
||||||
|
|
||||||
|
export const appKeysSchema = z.object({
|
||||||
|
mode: z.string().min(1),
|
||||||
|
region: z.string().min(1),
|
||||||
|
api_key: z.string().min(1),
|
||||||
|
webhook_secret: z.string().min(1),
|
||||||
|
});
|
|
@ -4,6 +4,7 @@ import config from "./config.json";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
category: "other",
|
category: "other",
|
||||||
|
dirName: "weather_in_your_calendar",
|
||||||
...config,
|
...config,
|
||||||
} as AppMeta;
|
} as AppMeta;
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import config from "./config.json";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
category: "other",
|
category: "other",
|
||||||
|
dirName: "whatsapp",
|
||||||
...config,
|
...config,
|
||||||
} as AppMeta;
|
} as AppMeta;
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ export const metadata = {
|
||||||
urlRegExp: "^http(s)?:\\/\\/(www\\.)?(team.)?whereby.com\\/[a-zA-Z0-9]*",
|
urlRegExp: "^http(s)?:\\/\\/(www\\.)?(team.)?whereby.com\\/[a-zA-Z0-9]*",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
dirName: "whereby",
|
||||||
...config,
|
...config,
|
||||||
} as AppMeta;
|
} as AppMeta;
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@ export const metadata = {
|
||||||
variant: "other",
|
variant: "other",
|
||||||
verified: true,
|
verified: true,
|
||||||
email: "help@cal.com",
|
email: "help@cal.com",
|
||||||
|
dirName: "wipemycalother",
|
||||||
} as AppMeta;
|
} as AppMeta;
|
||||||
|
|
||||||
export default metadata;
|
export default metadata;
|
||||||
|
|
|
@ -7,3 +7,5 @@ export const appDataSchema = eventTypeAppCardZod.merge(
|
||||||
isSunrise: z.boolean(),
|
isSunrise: z.boolean(),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const appKeysSchema = z.object({});
|
||||||
|
|
|
@ -20,6 +20,7 @@ export const metadata = {
|
||||||
variant: "automation",
|
variant: "automation",
|
||||||
verified: true,
|
verified: true,
|
||||||
email: "help@cal.com",
|
email: "help@cal.com",
|
||||||
|
dirName: "zapier",
|
||||||
} as AppMeta;
|
} as AppMeta;
|
||||||
|
|
||||||
export default metadata;
|
export default metadata;
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const appDataSchema = z.object({});
|
||||||
|
|
||||||
|
export const appKeysSchema = z.object({
|
||||||
|
invite_link: z.string().min(1),
|
||||||
|
});
|
|
@ -28,6 +28,7 @@ export const metadata = {
|
||||||
label: "Zoom Video",
|
label: "Zoom Video",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
dirName: "zoomvideo",
|
||||||
} as AppMeta;
|
} as AppMeta;
|
||||||
|
|
||||||
export default metadata;
|
export default metadata;
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const appDataSchema = z.object({});
|
||||||
|
|
||||||
|
export const appKeysSchema = z.object({
|
||||||
|
client_id: z.string().min(1),
|
||||||
|
client_secret: z.string().min(1),
|
||||||
|
});
|
|
@ -6,6 +6,7 @@ import { getDailyAppKeys } from "@calcom/app-store/dailyvideo/lib/getDailyAppKey
|
||||||
import { sendBrokenIntegrationEmail } from "@calcom/emails";
|
import { sendBrokenIntegrationEmail } from "@calcom/emails";
|
||||||
import { getUid } from "@calcom/lib/CalEventParser";
|
import { getUid } from "@calcom/lib/CalEventParser";
|
||||||
import logger from "@calcom/lib/logger";
|
import logger from "@calcom/lib/logger";
|
||||||
|
import { prisma } from "@calcom/prisma";
|
||||||
import type { CalendarEvent, EventBusyDate } from "@calcom/types/Calendar";
|
import type { CalendarEvent, EventBusyDate } from "@calcom/types/Calendar";
|
||||||
import { CredentialPayload, CredentialWithAppName } from "@calcom/types/Credential";
|
import { CredentialPayload, CredentialWithAppName } from "@calcom/types/Credential";
|
||||||
import type { EventResult, PartialReference } from "@calcom/types/EventManager";
|
import type { EventResult, PartialReference } from "@calcom/types/EventManager";
|
||||||
|
@ -37,7 +38,7 @@ const getBusyVideoTimes = (withCredentials: CredentialPayload[]) =>
|
||||||
const createMeeting = async (credential: CredentialWithAppName, calEvent: CalendarEvent) => {
|
const createMeeting = async (credential: CredentialWithAppName, calEvent: CalendarEvent) => {
|
||||||
const uid: string = getUid(calEvent);
|
const uid: string = getUid(calEvent);
|
||||||
|
|
||||||
if (!credential) {
|
if (!credential || !credential.appId) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Credentials must be set! Video platforms are optional, so this method shouldn't even be called when no video credentials are set."
|
"Credentials must be set! Video platforms are optional, so this method shouldn't even be called when no video credentials are set."
|
||||||
);
|
);
|
||||||
|
@ -46,26 +47,37 @@ const createMeeting = async (credential: CredentialWithAppName, calEvent: Calend
|
||||||
const videoAdapters = getVideoAdapters([credential]);
|
const videoAdapters = getVideoAdapters([credential]);
|
||||||
const [firstVideoAdapter] = videoAdapters;
|
const [firstVideoAdapter] = videoAdapters;
|
||||||
let createdMeeting;
|
let createdMeeting;
|
||||||
|
let returnObject: {
|
||||||
|
appName: string;
|
||||||
|
type: string;
|
||||||
|
uid: string;
|
||||||
|
originalEvent: CalendarEvent;
|
||||||
|
success: boolean;
|
||||||
|
createdEvent: VideoCallData | undefined;
|
||||||
|
} = {
|
||||||
|
appName: credential.appName,
|
||||||
|
type: credential.type,
|
||||||
|
uid,
|
||||||
|
originalEvent: calEvent,
|
||||||
|
success: false,
|
||||||
|
createdEvent: undefined,
|
||||||
|
};
|
||||||
try {
|
try {
|
||||||
if (!calEvent.location) {
|
// Check to see if video app is enabled
|
||||||
const defaultMeeting = await createMeetingWithCalVideo(calEvent);
|
const enabledApp = await prisma.app.findFirst({
|
||||||
if (defaultMeeting) {
|
where: {
|
||||||
createdMeeting = defaultMeeting;
|
slug: credential.appId,
|
||||||
calEvent.location = "integrations:dailyvideo";
|
},
|
||||||
}
|
select: {
|
||||||
}
|
enabled: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!enabledApp?.enabled) throw "Current location app is not enabled";
|
||||||
|
|
||||||
createdMeeting = await firstVideoAdapter?.createMeeting(calEvent);
|
createdMeeting = await firstVideoAdapter?.createMeeting(calEvent);
|
||||||
|
|
||||||
if (!createdMeeting) {
|
returnObject = { ...returnObject, createdEvent: createdMeeting, success: true };
|
||||||
return {
|
|
||||||
appName: credential.appName,
|
|
||||||
type: credential.type,
|
|
||||||
success: false,
|
|
||||||
uid,
|
|
||||||
originalEvent: calEvent,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
await sendBrokenIntegrationEmail(calEvent, "video");
|
await sendBrokenIntegrationEmail(calEvent, "video");
|
||||||
console.error("createMeeting failed", err, calEvent);
|
console.error("createMeeting failed", err, calEvent);
|
||||||
|
@ -73,19 +85,13 @@ const createMeeting = async (credential: CredentialWithAppName, calEvent: Calend
|
||||||
// Default to calVideo
|
// Default to calVideo
|
||||||
const defaultMeeting = await createMeetingWithCalVideo(calEvent);
|
const defaultMeeting = await createMeetingWithCalVideo(calEvent);
|
||||||
if (defaultMeeting) {
|
if (defaultMeeting) {
|
||||||
createdMeeting = defaultMeeting;
|
|
||||||
calEvent.location = "integrations:dailyvideo";
|
calEvent.location = "integrations:dailyvideo";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
returnObject = { ...returnObject, createdEvent: defaultMeeting };
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return returnObject;
|
||||||
appName: credential.appName,
|
|
||||||
type: credential.type,
|
|
||||||
success: true,
|
|
||||||
uid,
|
|
||||||
createdEvent: createdMeeting,
|
|
||||||
originalEvent: calEvent,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateMeeting = async (
|
const updateMeeting = async (
|
||||||
|
@ -155,6 +161,7 @@ const createMeetingWithCalVideo = async (calEvent: CalendarEvent) => {
|
||||||
type: "daily_video",
|
type: "daily_video",
|
||||||
userId: null,
|
userId: null,
|
||||||
key: dailyAppKeys,
|
key: dailyAppKeys,
|
||||||
|
invalid: false,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
return videoAdapter?.createMeeting(calEvent);
|
return videoAdapter?.createMeeting(calEvent);
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { TFunction } from "next-i18next";
|
||||||
|
|
||||||
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
||||||
|
|
||||||
import AttendeeAwaitingPaymentEmail from "./templates/attendee-awaiting-payment-email";
|
import AttendeeAwaitingPaymentEmail from "./templates/attendee-awaiting-payment-email";
|
||||||
|
@ -9,6 +11,7 @@ import AttendeeRescheduledEmail from "./templates/attendee-rescheduled-email";
|
||||||
import AttendeeScheduledEmail from "./templates/attendee-scheduled-email";
|
import AttendeeScheduledEmail from "./templates/attendee-scheduled-email";
|
||||||
import AttendeeWasRequestedToRescheduleEmail from "./templates/attendee-was-requested-to-reschedule-email";
|
import AttendeeWasRequestedToRescheduleEmail from "./templates/attendee-was-requested-to-reschedule-email";
|
||||||
import BrokenIntegrationEmail from "./templates/broken-integration-email";
|
import BrokenIntegrationEmail from "./templates/broken-integration-email";
|
||||||
|
import DisabledAppEmail from "./templates/disabled-app-email";
|
||||||
import FeedbackEmail, { Feedback } from "./templates/feedback-email";
|
import FeedbackEmail, { Feedback } from "./templates/feedback-email";
|
||||||
import ForgotPasswordEmail, { PasswordReset } from "./templates/forgot-password-email";
|
import ForgotPasswordEmail, { PasswordReset } from "./templates/forgot-password-email";
|
||||||
import OrganizerCancelledEmail from "./templates/organizer-cancelled-email";
|
import OrganizerCancelledEmail from "./templates/organizer-cancelled-email";
|
||||||
|
@ -328,3 +331,28 @@ export const sendBrokenIntegrationEmail = async (evt: CalendarEvent, type: "vide
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const sendDisabledAppEmail = async ({
|
||||||
|
email,
|
||||||
|
appName,
|
||||||
|
appType,
|
||||||
|
t,
|
||||||
|
title = undefined,
|
||||||
|
eventTypeId = undefined,
|
||||||
|
}: {
|
||||||
|
email: string;
|
||||||
|
appName: string;
|
||||||
|
appType: string[];
|
||||||
|
t: TFunction;
|
||||||
|
title?: string;
|
||||||
|
eventTypeId?: number;
|
||||||
|
}) => {
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
const disabledPaymentEmail = new DisabledAppEmail(email, appName, appType, t, title, eventTypeId);
|
||||||
|
resolve(disabledPaymentEmail.sendEmail());
|
||||||
|
} catch (e) {
|
||||||
|
reject(console.error("DisabledPaymentEmail.sendEmail failed", e));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
|
@ -0,0 +1,85 @@
|
||||||
|
import { TFunction } from "next-i18next";
|
||||||
|
|
||||||
|
import { CAL_URL } from "@calcom/lib/constants";
|
||||||
|
|
||||||
|
import { BaseEmailHtml, CallToAction } from "../components";
|
||||||
|
|
||||||
|
export const DisabledAppEmail = (
|
||||||
|
props: {
|
||||||
|
appName: string;
|
||||||
|
appType: string[];
|
||||||
|
t: TFunction;
|
||||||
|
title?: string;
|
||||||
|
eventTypeId?: number;
|
||||||
|
} & Partial<React.ComponentProps<typeof BaseEmailHtml>>
|
||||||
|
) => {
|
||||||
|
const { title, appName, eventTypeId, t, appType } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseEmailHtml subject={t("app_disabled", { appName: appName })}>
|
||||||
|
{appType.some((type) => type === "payment") ? (
|
||||||
|
<>
|
||||||
|
<p>
|
||||||
|
<>{t("disabled_app_affects_event_type", { appName: appName, eventType: title })}</>
|
||||||
|
</p>
|
||||||
|
<p style={{ fontWeight: 400, lineHeight: "24px" }}>
|
||||||
|
<>{t("payment_disabled_still_able_to_book")}</>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<hr style={{ marginBottom: "24px" }} />
|
||||||
|
|
||||||
|
<CallToAction
|
||||||
|
label={t("edit_event_type")}
|
||||||
|
href={`${CAL_URL}/event-types/${eventTypeId}?tabName=apps`}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : title && eventTypeId ? (
|
||||||
|
<>
|
||||||
|
<p>
|
||||||
|
<>{(t("app_disabled_with_event_type"), { appName: appName, title: title })}</>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<hr style={{ marginBottom: "24px" }} />
|
||||||
|
|
||||||
|
<CallToAction
|
||||||
|
label={t("edit_event_type")}
|
||||||
|
href={`${CAL_URL}/event-types/${eventTypeId}?tabName=apps`}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : appType.some((type) => type === "video") ? (
|
||||||
|
<>
|
||||||
|
<p>
|
||||||
|
<>{t("app_disabled_video", { appName: appName })}</>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<hr style={{ marginBottom: "24px" }} />
|
||||||
|
|
||||||
|
<CallToAction label={t("navigate_installed_apps")} href={`${CAL_URL}/apps/installed`} />
|
||||||
|
</>
|
||||||
|
) : appType.some((type) => type === "calendar") ? (
|
||||||
|
<>
|
||||||
|
<p>
|
||||||
|
<>{t("admin_has_disabled", { appName: appName })}</>
|
||||||
|
</p>
|
||||||
|
<p style={{ fontWeight: 400, lineHeight: "24px" }}>
|
||||||
|
<>{t("disabled_calendar")}</>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<hr style={{ marginBottom: "24px" }} />
|
||||||
|
|
||||||
|
<CallToAction label={t("navigate_installed_apps")} href={`${CAL_URL}/apps/installed`} />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<p>
|
||||||
|
<>{t("admin_has_disabled", { appName: appName })}</>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<hr style={{ marginBottom: "24px" }} />
|
||||||
|
|
||||||
|
<CallToAction label={t("navigate_installed_apps")} href={`${CAL_URL}/apps/installed`} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</BaseEmailHtml>
|
||||||
|
);
|
||||||
|
};
|
|
@ -6,6 +6,7 @@ export { AttendeeRequestEmail } from "./AttendeeRequestEmail";
|
||||||
export { AttendeeWasRequestedToRescheduleEmail } from "./AttendeeWasRequestedToRescheduleEmail";
|
export { AttendeeWasRequestedToRescheduleEmail } from "./AttendeeWasRequestedToRescheduleEmail";
|
||||||
export { AttendeeRescheduledEmail } from "./AttendeeRescheduledEmail";
|
export { AttendeeRescheduledEmail } from "./AttendeeRescheduledEmail";
|
||||||
export { AttendeeScheduledEmail } from "./AttendeeScheduledEmail";
|
export { AttendeeScheduledEmail } from "./AttendeeScheduledEmail";
|
||||||
|
export { DisabledAppEmail } from "./DisabledAppEmail";
|
||||||
export { FeedbackEmail } from "./FeedbackEmail";
|
export { FeedbackEmail } from "./FeedbackEmail";
|
||||||
export { ForgotPasswordEmail } from "./ForgotPasswordEmail";
|
export { ForgotPasswordEmail } from "./ForgotPasswordEmail";
|
||||||
export { OrganizerCancelledEmail } from "./OrganizerCancelledEmail";
|
export { OrganizerCancelledEmail } from "./OrganizerCancelledEmail";
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { TFunction } from "next-i18next";
|
||||||
|
|
||||||
|
import { renderEmail } from "..";
|
||||||
|
import BaseEmail from "./_base-email";
|
||||||
|
|
||||||
|
export default class DisabledAppEmail extends BaseEmail {
|
||||||
|
email: string;
|
||||||
|
appName: string;
|
||||||
|
appType: string[];
|
||||||
|
t: TFunction;
|
||||||
|
title?: string;
|
||||||
|
eventTypeId?: number;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
email: string,
|
||||||
|
appName: string,
|
||||||
|
appType: string[],
|
||||||
|
t: TFunction,
|
||||||
|
title?: string,
|
||||||
|
eventTypeId?: number
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
this.email = email;
|
||||||
|
this.appName = appName;
|
||||||
|
this.appType = appType;
|
||||||
|
this.t = t;
|
||||||
|
this.title = title;
|
||||||
|
this.eventTypeId = eventTypeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getNodeMailerPayload(): Record<string, unknown> {
|
||||||
|
return {
|
||||||
|
from: `Cal.com <${this.getMailerOptions().from}>`,
|
||||||
|
to: this.email,
|
||||||
|
subject:
|
||||||
|
this.title && this.eventTypeId
|
||||||
|
? this.t("disabled_app_affects_event_type", { appName: this.appName, eventType: this.title })
|
||||||
|
: this.t("admin_has_disabled", { appName: this.appName }),
|
||||||
|
html: renderEmail("DisabledAppEmail", {
|
||||||
|
title: this.title,
|
||||||
|
appName: this.appName,
|
||||||
|
eventTypeId: this.eventTypeId,
|
||||||
|
appType: this.appType,
|
||||||
|
t: this.t,
|
||||||
|
}),
|
||||||
|
text: this.getTextBody(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getTextBody(): string {
|
||||||
|
return this.appType.some((type) => type === "payment")
|
||||||
|
? this.t("disable_payment_app", { appName: this.appName, title: this.title })
|
||||||
|
: this.appType.some((type) => type === "video")
|
||||||
|
? this.t("app_disabled_video", { appName: this.appName })
|
||||||
|
: this.t("app_disabled", { appName: this.appName });
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,236 @@
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { AppCategories } from "@prisma/client";
|
||||||
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import AppCategoryNavigation from "@calcom/app-store/_components/AppCategoryNavigation";
|
||||||
|
import { appKeysSchemas } from "@calcom/app-store/apps.keys-schemas.generated";
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { RouterOutputs, trpc } from "@calcom/trpc/react";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
ConfirmationDialogContent,
|
||||||
|
Dialog,
|
||||||
|
EmptyScreen,
|
||||||
|
Form,
|
||||||
|
Icon,
|
||||||
|
showToast,
|
||||||
|
SkeletonButton,
|
||||||
|
SkeletonContainer,
|
||||||
|
SkeletonText,
|
||||||
|
Switch,
|
||||||
|
TextField,
|
||||||
|
VerticalDivider,
|
||||||
|
} from "@calcom/ui";
|
||||||
|
|
||||||
|
const IntegrationContainer = ({
|
||||||
|
app,
|
||||||
|
lastEntry,
|
||||||
|
category,
|
||||||
|
}: {
|
||||||
|
app: RouterOutputs["viewer"]["appsRouter"]["listLocal"][number];
|
||||||
|
lastEntry: boolean;
|
||||||
|
category: string;
|
||||||
|
}) => {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const utils = trpc.useContext();
|
||||||
|
const [disableDialog, setDisableDialog] = useState(false);
|
||||||
|
const [showKeys, setShowKeys] = useState(false);
|
||||||
|
|
||||||
|
const appKeySchema = appKeysSchemas[app.dirName as keyof typeof appKeysSchemas];
|
||||||
|
|
||||||
|
const formMethods = useForm({
|
||||||
|
resolver: zodResolver(appKeySchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
const enableAppMutation = trpc.viewer.appsRouter.toggle.useMutation({
|
||||||
|
onSuccess: (enabled) => {
|
||||||
|
utils.viewer.appsRouter.listLocal.invalidate({ category });
|
||||||
|
setDisableDialog(false);
|
||||||
|
showToast(
|
||||||
|
enabled ? t("app_is_enabled", { appName: app.name }) : t("app_is_disabled", { appName: app.name }),
|
||||||
|
"success"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
showToast(error.message, "error");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const saveKeysMutation = trpc.viewer.appsRouter.saveKeys.useMutation({
|
||||||
|
onSuccess: () => {
|
||||||
|
showToast(t("keys_have_been_saved"), "success");
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
showToast(error.message, "error");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Collapsible key={app.name} open={showKeys}>
|
||||||
|
<div className={`${!lastEntry && "border-b"}`}>
|
||||||
|
<div className="flex w-full flex-1 items-center justify-between space-x-3 p-4 rtl:space-x-reverse md:max-w-3xl">
|
||||||
|
{app.logo && <img className="h-10 w-10" src={app.logo} alt={app.title} />}
|
||||||
|
<div className="flex-grow truncate pl-2">
|
||||||
|
<h3 className="truncate text-sm font-medium text-neutral-900">
|
||||||
|
<p>{app.name || app.title}</p>
|
||||||
|
</h3>
|
||||||
|
<p className="truncate text-sm text-gray-500">{app.description}</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-self-end">
|
||||||
|
<>
|
||||||
|
<Switch
|
||||||
|
checked={app.enabled}
|
||||||
|
onClick={() => {
|
||||||
|
if (app.enabled) {
|
||||||
|
setDisableDialog(true);
|
||||||
|
} else {
|
||||||
|
enableAppMutation.mutate({ slug: app.slug, enabled: app.enabled });
|
||||||
|
setShowKeys(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{app.keys && (
|
||||||
|
<>
|
||||||
|
<VerticalDivider className="h-10" />
|
||||||
|
|
||||||
|
<CollapsibleTrigger>
|
||||||
|
<Button
|
||||||
|
color="secondary"
|
||||||
|
size="icon"
|
||||||
|
tooltip={t("edit_keys")}
|
||||||
|
onClick={() => setShowKeys(!showKeys)}>
|
||||||
|
<Icon.FiEdit />
|
||||||
|
</Button>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<CollapsibleContent>
|
||||||
|
{!!app.keys && typeof app.keys === "object" && (
|
||||||
|
<Form
|
||||||
|
form={formMethods}
|
||||||
|
handleSubmit={(values) =>
|
||||||
|
saveKeysMutation.mutate({
|
||||||
|
slug: app.slug,
|
||||||
|
type: app.type,
|
||||||
|
keys: values,
|
||||||
|
dirName: app.dirName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="px-4 pb-4">
|
||||||
|
{Object.keys(app.keys).map((key) => (
|
||||||
|
<Controller
|
||||||
|
name={key}
|
||||||
|
key={key}
|
||||||
|
control={formMethods.control}
|
||||||
|
defaultValue={app.keys && app.keys[key] ? app?.keys[key] : ""}
|
||||||
|
render={({ field: { value } }) => (
|
||||||
|
<TextField
|
||||||
|
label={key}
|
||||||
|
key={key}
|
||||||
|
name={key}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => {
|
||||||
|
formMethods.setValue(key, e?.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<Button type="submit" loading={saveKeysMutation.isLoading}>
|
||||||
|
{t("save")}
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
</CollapsibleContent>
|
||||||
|
</div>
|
||||||
|
</Collapsible>
|
||||||
|
|
||||||
|
<Dialog open={disableDialog} onOpenChange={setDisableDialog}>
|
||||||
|
<ConfirmationDialogContent
|
||||||
|
title={t("disable_app")}
|
||||||
|
variety="danger"
|
||||||
|
onConfirm={() => {
|
||||||
|
enableAppMutation.mutate({ slug: app.slug, enabled: app.enabled });
|
||||||
|
}}>
|
||||||
|
{t("disable_app_description")}
|
||||||
|
</ConfirmationDialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const querySchema = z.object({
|
||||||
|
category: z
|
||||||
|
.nativeEnum({ ...AppCategories, conferencing: "conferencing" })
|
||||||
|
.optional()
|
||||||
|
.default(AppCategories.calendar),
|
||||||
|
});
|
||||||
|
|
||||||
|
const AdminAppsList = ({ baseURL, className }: { baseURL: string; className?: string }) => (
|
||||||
|
<AppCategoryNavigation
|
||||||
|
baseURL={baseURL}
|
||||||
|
containerClassname="w-full xl:mx-5 xl:w-2/3 xl:pr-5"
|
||||||
|
className={className}>
|
||||||
|
<AdminAppsListContainer />
|
||||||
|
</AppCategoryNavigation>
|
||||||
|
);
|
||||||
|
|
||||||
|
const AdminAppsListContainer = () => {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const router = useRouter();
|
||||||
|
const { category } = querySchema.parse(router.query);
|
||||||
|
const { data: apps, isLoading } = trpc.viewer.appsRouter.listLocal.useQuery(
|
||||||
|
{ category },
|
||||||
|
{ enabled: router.isReady }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isLoading) return <SkeletonLoader />;
|
||||||
|
|
||||||
|
if (!apps) {
|
||||||
|
return (
|
||||||
|
<EmptyScreen
|
||||||
|
Icon={Icon.FiAlertCircle}
|
||||||
|
headline={t("no_available_apps")}
|
||||||
|
description={t("no_available_apps_description")}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-md border border-gray-200">
|
||||||
|
{apps.map((app, index) => (
|
||||||
|
<IntegrationContainer
|
||||||
|
app={app}
|
||||||
|
lastEntry={index === apps.length - 1}
|
||||||
|
key={app.name}
|
||||||
|
category={category}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AdminAppsList;
|
||||||
|
|
||||||
|
const SkeletonLoader = () => {
|
||||||
|
return (
|
||||||
|
<SkeletonContainer>
|
||||||
|
<div className="mt-6 mb-8 space-y-6">
|
||||||
|
<SkeletonText className="h-8 w-full" />
|
||||||
|
<SkeletonText className="h-8 w-full" />
|
||||||
|
<SkeletonText className="h-8 w-full" />
|
||||||
|
<SkeletonText className="h-8 w-full" />
|
||||||
|
|
||||||
|
<SkeletonButton className="mr-6 h-8 w-20 rounded-md p-5" />
|
||||||
|
</div>
|
||||||
|
</SkeletonContainer>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,21 @@
|
||||||
|
import getApps from "@calcom/app-store/utils";
|
||||||
|
import type { CredentialData } from "@calcom/app-store/utils";
|
||||||
|
import { prisma } from "@calcom/prisma";
|
||||||
|
|
||||||
|
const getEnabledApps = async (userCredentials: CredentialData[]) => {
|
||||||
|
const enabledApps = await prisma.app.findMany({ select: { slug: true, enabled: true } });
|
||||||
|
|
||||||
|
const apps = getApps(userCredentials);
|
||||||
|
|
||||||
|
const filteredApps = enabledApps.reduce((reducedArray, app) => {
|
||||||
|
const appMetadata = apps.find((metadata) => metadata.slug === app.slug);
|
||||||
|
if (appMetadata) {
|
||||||
|
reducedArray.push({ ...appMetadata, enabled: app.enabled });
|
||||||
|
}
|
||||||
|
return reducedArray;
|
||||||
|
}, [] as (ReturnType<typeof getApps>[number] & { enabled: boolean })[]);
|
||||||
|
|
||||||
|
return filteredApps;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getEnabledApps;
|
|
@ -0,0 +1,2 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "App" ADD COLUMN "enabled" BOOLEAN NOT NULL DEFAULT false;
|
|
@ -0,0 +1,2 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Credential" ALTER COLUMN "invalid" SET DEFAULT false;
|
|
@ -0,0 +1,3 @@
|
||||||
|
-- AlterTable
|
||||||
|
-- The new default is false but we set true for all current apps
|
||||||
|
UPDATE "App" SET "enabled" = true;
|
|
@ -100,7 +100,7 @@ model Credential {
|
||||||
// How to make it a required column?
|
// How to make it a required column?
|
||||||
appId String?
|
appId String?
|
||||||
destinationCalendars DestinationCalendar[]
|
destinationCalendars DestinationCalendar[]
|
||||||
invalid Boolean?
|
invalid Boolean? @default(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum UserPlan {
|
enum UserPlan {
|
||||||
|
@ -510,6 +510,7 @@ model App {
|
||||||
credentials Credential[]
|
credentials Credential[]
|
||||||
Webhook Webhook[]
|
Webhook Webhook[]
|
||||||
ApiKey ApiKey[]
|
ApiKey ApiKey[]
|
||||||
|
enabled Boolean @default(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
model App_RoutingForms_Form {
|
model App_RoutingForms_Form {
|
||||||
|
|
|
@ -158,8 +158,8 @@ async function createApp(
|
||||||
) {
|
) {
|
||||||
await prisma.app.upsert({
|
await prisma.app.upsert({
|
||||||
where: { slug },
|
where: { slug },
|
||||||
create: { slug, dirName, categories, keys },
|
create: { slug, dirName, categories, keys, enabled: true },
|
||||||
update: { dirName, categories, keys },
|
update: { dirName, categories, keys, enabled: true },
|
||||||
});
|
});
|
||||||
await prisma.credential.updateMany({
|
await prisma.credential.updateMany({
|
||||||
where: { type },
|
where: { type },
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user