refactor: Add crm type to app credentials (#14147)
* fix timezone display on booking page to reflect event availability timezone * migrate fetching event owner's schedule to server side * migrate fetching event owner's schedule to server side * fix e2e test errors * Add WEBAPP_URL_FOR_OAUTH to salesforce auth * In event manager constructor include "_crm" credentials as calendar creds * Change crm apps to type to end with `_crm` * Move sendgrid out of CRM * Add zoho bigin to CRM apps * When getting apps, use slug * Add `crm` variants * Hubspot Oauth use `WEBAPP_URL_FOR_OAUTH` * Refactor creating credentials * Fix empty CRM page * Use credentials with `_crm` * Abstract getAppCategoryTitle * Add integration.handler changes * Fix tests * Type fix --------- Co-authored-by: Shaik-Sirajuddin <sirajuddinshaik30gmail.com> Co-authored-by: Shaik-Sirajuddin <sirajudddinshaik30@gmail.com> Co-authored-by: Shaik-Sirajuddin <89742297+Shaik-Sirajuddin@users.noreply.github.com> Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
This commit is contained in:
21 files changed
+96
-44
No files matched your search
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useReducer } from "react";
|
||||
|
||||
import getAppCategoryTitle from "@calcom/app-store/_utils/getAppCategoryTitle";
|
||||
import DisconnectIntegrationModal from "@calcom/features/apps/components/DisconnectIntegrationModal";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
@@ -57,11 +58,13 @@ const IntegrationsContainer = ({
|
||||
customLoader={<SkeletonLoader />}
|
||||
success={({ data }) => {
|
||||
if (!data.items.length) {
|
||||
const emptyHeaderCategory = getAppCategoryTitle(variant || "other", true);
|
||||
|
||||
return (
|
||||
<EmptyScreen
|
||||
Icon={emptyIcon[variant || "other"]}
|
||||
headline={t("no_category_apps", {
|
||||
category: (variant && t(variant).toLowerCase()) || t("other").toLowerCase(),
|
||||
category: emptyHeaderCategory,
|
||||
})}
|
||||
description={t(`no_category_apps_description_${variant || "other"}`)}
|
||||
buttonRaw={
|
||||
|
||||
@@ -17,6 +17,7 @@ test.describe("Payment app", () => {
|
||||
await prisma.credential.create({
|
||||
data: {
|
||||
type: "alby_payment",
|
||||
appId: "alby",
|
||||
userId: user.id,
|
||||
key: {
|
||||
account_id: "random",
|
||||
@@ -57,6 +58,7 @@ test.describe("Payment app", () => {
|
||||
await prisma.credential.create({
|
||||
data: {
|
||||
type: "stripe_payment",
|
||||
appId: "stripe",
|
||||
userId: user.id,
|
||||
key: {
|
||||
scope: "read_write",
|
||||
@@ -101,6 +103,7 @@ test.describe("Payment app", () => {
|
||||
await prisma.credential.create({
|
||||
data: {
|
||||
type: "paypal_payment",
|
||||
appId: "paypal",
|
||||
userId: user.id,
|
||||
key: {
|
||||
client_id: "randomString",
|
||||
@@ -147,6 +150,7 @@ test.describe("Payment app", () => {
|
||||
await prisma.credential.create({
|
||||
data: {
|
||||
type: "alby_payment",
|
||||
appId: "alby",
|
||||
userId: user.id,
|
||||
key: {},
|
||||
},
|
||||
@@ -173,6 +177,7 @@ test.describe("Payment app", () => {
|
||||
await prisma.credential.create({
|
||||
data: {
|
||||
type: "paypal_payment",
|
||||
appId: "paypal",
|
||||
userId: user.id,
|
||||
key: {},
|
||||
},
|
||||
@@ -234,6 +239,7 @@ test.describe("Payment app", () => {
|
||||
data: [
|
||||
{
|
||||
type: "paypal_payment",
|
||||
appId: "paypal",
|
||||
userId: user.id,
|
||||
key: {
|
||||
client_id: "randomString",
|
||||
@@ -243,6 +249,7 @@ test.describe("Payment app", () => {
|
||||
},
|
||||
{
|
||||
type: "stripe_payment",
|
||||
appId: "stripe",
|
||||
userId: user.id,
|
||||
key: {
|
||||
scope: "read_write",
|
||||
@@ -279,6 +286,7 @@ test.describe("Payment app", () => {
|
||||
data: [
|
||||
{
|
||||
type: "paypal_payment",
|
||||
appId: "paypal",
|
||||
userId: user.id,
|
||||
key: {
|
||||
client_id: "randomString",
|
||||
@@ -288,6 +296,7 @@ test.describe("Payment app", () => {
|
||||
},
|
||||
{
|
||||
type: "stripe_payment",
|
||||
appId: "stripe",
|
||||
userId: user.id,
|
||||
key: {
|
||||
scope: "read_write",
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { AppCategories } from "@calcom/prisma/enums";
|
||||
|
||||
/**
|
||||
* Handles if the app category should be full capitalized ex. CRM
|
||||
*
|
||||
* @param {App["variant"]} variant - The variant of the app.
|
||||
* @param {boolean} [returnLowerCase] - Optional flag to return the title in lowercase.
|
||||
*/
|
||||
const getAppCategoryTitle = (variant: AppCategories, returnLowerCase?: boolean) => {
|
||||
let title: string;
|
||||
|
||||
if (variant === "crm") {
|
||||
title = "CRM";
|
||||
return title;
|
||||
} else {
|
||||
title = variant;
|
||||
}
|
||||
|
||||
return returnLowerCase ? title.toLowerCase() : title;
|
||||
};
|
||||
|
||||
export default getAppCategoryTitle;
|
||||
@@ -29,6 +29,11 @@ export const getCalendar = async (credential: CredentialPayload | null): Promise
|
||||
if (calendarType?.endsWith("_other_calendar")) {
|
||||
calendarType = calendarType.split("_other_calendar")[0];
|
||||
}
|
||||
// Backwards compatibility until CRM manager is created
|
||||
if (calendarType?.endsWith("_crm")) {
|
||||
calendarType = calendarType.split("_crm")[0];
|
||||
}
|
||||
|
||||
const calendarAppImportFn = appStore[calendarType.split("_").join("") as keyof typeof appStore];
|
||||
|
||||
if (!calendarAppImportFn) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import prisma from "@calcom/prisma";
|
||||
|
||||
import checkSession from "../../_utils/auth";
|
||||
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
|
||||
import appConfig from "../config.json";
|
||||
|
||||
export async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const session = checkSession(req);
|
||||
@@ -18,10 +19,10 @@ export async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const encrypted = symmetricEncrypt(JSON.stringify({ api_key }), process.env.CALENDSO_ENCRYPTION_KEY || "");
|
||||
|
||||
const data = {
|
||||
type: "closecom_other_calendar",
|
||||
type: appConfig.type,
|
||||
key: { encrypted },
|
||||
userId: session.user?.id,
|
||||
appId: "closecom",
|
||||
appId: appConfig.slug,
|
||||
};
|
||||
|
||||
try {
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
"name": "Close.com",
|
||||
"title": "Close.com",
|
||||
"slug": "closecom",
|
||||
"type": "closecom_other_calendar",
|
||||
"type": "closecom_crm",
|
||||
"logo": "icon.svg",
|
||||
"url": "https://cal.com/",
|
||||
"variant": "other",
|
||||
"variant": "crm",
|
||||
"categories": ["crm"],
|
||||
"publisher": "Cal.com, Inc.",
|
||||
"email": "help@cal.com",
|
||||
|
||||
@@ -6,8 +6,8 @@ export const metadata = {
|
||||
name: "HubSpot CRM",
|
||||
installed: !!process.env.HUBSPOT_CLIENT_ID,
|
||||
description: _package.description,
|
||||
type: "hubspot_other_calendar",
|
||||
variant: "other_calendar",
|
||||
type: "hubspot_crm",
|
||||
variant: "crm",
|
||||
logo: "icon.svg",
|
||||
publisher: "Cal.com",
|
||||
url: "https://hubspot.com/",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as hubspot from "@hubspot/api-client";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { WEBAPP_URL_FOR_OAUTH } from "@calcom/lib/constants";
|
||||
|
||||
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||
import { encodeOAuthState } from "../../_utils/oauth/encodeOAuthState";
|
||||
@@ -18,7 +18,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
if (typeof appKeys.client_id === "string") client_id = appKeys.client_id;
|
||||
if (!client_id) return res.status(400).json({ message: "HubSpot client id missing." });
|
||||
|
||||
const redirectUri = `${WEBAPP_URL}/api/integrations/hubspot/callback`;
|
||||
const redirectUri = `${WEBAPP_URL_FOR_OAUTH}/api/integrations/hubspot/callback`;
|
||||
const url = hubspotClient.oauth.getAuthorizationUrl(
|
||||
client_id,
|
||||
redirectUri,
|
||||
|
||||
@@ -2,13 +2,14 @@ import * as hubspot from "@hubspot/api-client";
|
||||
import type { TokenResponseIF } from "@hubspot/api-client/lib/codegen/oauth/models/TokenResponseIF";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { WEBAPP_URL_FOR_OAUTH } from "@calcom/lib/constants";
|
||||
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
||||
|
||||
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
|
||||
import createOAuthAppCredential from "../../_utils/oauth/createOAuthAppCredential";
|
||||
import { decodeOAuthState } from "../../_utils/oauth/decodeOAuthState";
|
||||
import metadata from "../_metadata";
|
||||
|
||||
let client_id = "";
|
||||
let client_secret = "";
|
||||
@@ -39,7 +40,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
const hubspotToken: HubspotToken = await hubspotClient.oauth.tokensApi.createToken(
|
||||
"authorization_code",
|
||||
code,
|
||||
`${WEBAPP_URL}/api/integrations/hubspot/callback`,
|
||||
`${WEBAPP_URL_FOR_OAUTH}/api/integrations/hubspot/callback`,
|
||||
client_id,
|
||||
client_secret
|
||||
);
|
||||
@@ -47,7 +48,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
// set expiry date as offset from current time.
|
||||
hubspotToken.expiryDate = Math.round(Date.now() + hubspotToken.expiresIn * 1000);
|
||||
|
||||
await createOAuthAppCredential({ appId: "hubspot", type: "hubspot_other_calendar" }, hubspotToken, req);
|
||||
await createOAuthAppCredential({ appId: metadata.slug, type: metadata.type }, hubspotToken, req);
|
||||
|
||||
const state = decodeOAuthState(req);
|
||||
res.redirect(
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"/*": "Don't modify slug - If required, do it using cli edit command",
|
||||
"name": "Pipedrive CRM",
|
||||
"slug": "pipedrive-crm",
|
||||
"type": "pipedrive-crm_other_calendar",
|
||||
"type": "pipedrive-crm_crm",
|
||||
"logo": "icon.svg",
|
||||
"url": "https://revert.dev",
|
||||
"variant": "crm",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import jsforce from "jsforce";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { WEBAPP_URL_FOR_OAUTH } from "@calcom/lib/constants";
|
||||
|
||||
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||
import { encodeOAuthState } from "../../_utils/oauth/encodeOAuthState";
|
||||
@@ -17,7 +17,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
|
||||
const salesforceClient = new jsforce.Connection({
|
||||
clientId: consumer_key,
|
||||
redirectUri: `${WEBAPP_URL}/api/integrations/salesforce/callback`,
|
||||
redirectUri: `${WEBAPP_URL_FOR_OAUTH}/api/integrations/salesforce/callback`,
|
||||
});
|
||||
|
||||
const url = salesforceClient.oauth2.getAuthorizationUrl({
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import jsforce from "jsforce";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { WEBAPP_URL_FOR_OAUTH } from "@calcom/lib/constants";
|
||||
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
||||
|
||||
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
|
||||
import createOAuthAppCredential from "../../_utils/oauth/createOAuthAppCredential";
|
||||
import { decodeOAuthState } from "../../_utils/oauth/decodeOAuthState";
|
||||
import appConfig from "../config.json";
|
||||
|
||||
let consumer_key = "";
|
||||
let consumer_secret = "";
|
||||
@@ -33,16 +34,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
const conn = new jsforce.Connection({
|
||||
clientId: consumer_key,
|
||||
clientSecret: consumer_secret,
|
||||
redirectUri: `${WEBAPP_URL}/api/integrations/salesforce/callback`,
|
||||
redirectUri: `${WEBAPP_URL_FOR_OAUTH}/api/integrations/salesforce/callback`,
|
||||
});
|
||||
|
||||
const salesforceTokenInfo = await conn.oauth2.requestToken(code as string);
|
||||
|
||||
await createOAuthAppCredential(
|
||||
{ appId: "salesforce", type: "salesforce_other_calendar" },
|
||||
salesforceTokenInfo,
|
||||
req
|
||||
);
|
||||
await createOAuthAppCredential({ appId: appConfig.slug, type: appConfig.type }, salesforceTokenInfo, req);
|
||||
|
||||
const state = decodeOAuthState(req);
|
||||
res.redirect(
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
"/*": "Don't modify slug - If required, do it using cli edit command",
|
||||
"name": "Salesforce",
|
||||
"slug": "salesforce",
|
||||
"type": "salesforce_other_calendar",
|
||||
"type": "salesforce_crm",
|
||||
"logo": "icon.png",
|
||||
"url": "https://cal.com/",
|
||||
"variant": "other_calendar",
|
||||
"variant": "crm",
|
||||
"categories": ["crm"],
|
||||
"publisher": "Cal.com, Inc.",
|
||||
"email": "help@cal.com",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"logo": "logo.png",
|
||||
"url": "https://cal.com/",
|
||||
"variant": "other_calendar",
|
||||
"categories": ["crm"],
|
||||
"categories": ["other"],
|
||||
"publisher": "Cal.com, Inc.",
|
||||
"email": "help@cal.com",
|
||||
"description": "SendGrid delivers your transactional and marketing emails through the world's largest cloud-based email delivery platform.",
|
||||
|
||||
@@ -47,7 +47,7 @@ export const ALL_APPS = Object.values(ALL_APPS_MAP);
|
||||
*/
|
||||
function getApps(credentials: CredentialDataWithTeamName[], filterOnCredentials?: boolean) {
|
||||
const apps = ALL_APPS.reduce((reducedArray, appMeta) => {
|
||||
const appCredentials = credentials.filter((credential) => credential.type === appMeta.type);
|
||||
const appCredentials = credentials.filter((credential) => credential.appId === appMeta.slug);
|
||||
|
||||
if (filterOnCredentials && !appCredentials.length && !appMeta.isGlobal) return reducedArray;
|
||||
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
"/*": "Don't modify slug - If required, do it using cli edit command",
|
||||
"name": "Zoho Bigin",
|
||||
"slug": "zoho-bigin",
|
||||
"type": "zoho-bigin_other_calendar",
|
||||
"type": "zoho-bigin_crm",
|
||||
"logo": "zohobigin.svg",
|
||||
"url": "https://github.com/ShaneMaglangit",
|
||||
"variant": "other",
|
||||
"categories": ["other"],
|
||||
"variant": "crm",
|
||||
"categories": ["crm"],
|
||||
"publisher": "Shane Maglangit",
|
||||
"email": "help@cal.com",
|
||||
"description": "Bigin easily transforms your day-to-day customer processes into actionable pipelines. From qualifying leads to closing deals to managing important after-sales operations—Bigin connects your different teams to work together so that you can offer the best possible experience to your customers. Say goodbye to missing follow-ups, manual data entry, lack of team communication, and information silos.",
|
||||
|
||||
@@ -9,6 +9,7 @@ import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
|
||||
import createOAuthAppCredential from "../../_utils/oauth/createOAuthAppCredential";
|
||||
import { decodeOAuthState } from "../../_utils/oauth/decodeOAuthState";
|
||||
import appConfig from "../config.json";
|
||||
|
||||
let client_id = "";
|
||||
let client_secret = "";
|
||||
@@ -51,11 +52,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
zohoCrmTokenInfo.data.expiryDate = Math.round(Date.now() + 60 * 60);
|
||||
zohoCrmTokenInfo.data.accountServer = req.query["accounts-server"];
|
||||
|
||||
await createOAuthAppCredential(
|
||||
{ appId: "zohocrm", type: "zohocrm_other_calendar" },
|
||||
zohoCrmTokenInfo.data,
|
||||
req
|
||||
);
|
||||
await createOAuthAppCredential({ appId: appConfig.slug, type: appConfig.type }, zohoCrmTokenInfo.data, req);
|
||||
|
||||
const state = decodeOAuthState(req);
|
||||
res.redirect(
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
"/*": "Don't modify slug - If required, do it using cli edit command",
|
||||
"name": "ZohoCRM",
|
||||
"slug": "zohocrm",
|
||||
"type": "zohocrm_other_calendar",
|
||||
"type": "zohocrm_crm",
|
||||
"logo": "icon.svg",
|
||||
"url": "https://github.com/jatinsandilya",
|
||||
"variant": "other",
|
||||
"variant": "crm",
|
||||
"categories": ["crm"],
|
||||
"publisher": "Jatin Sandilya",
|
||||
"email": "help@cal.com",
|
||||
|
||||
@@ -95,7 +95,10 @@ export default class EventManager {
|
||||
// This includes all calendar-related apps, traditional calendars such as Google Calendar
|
||||
// (type google_calendar) and non-traditional calendars such as CRMs like Close.com
|
||||
// (type closecom_other_calendar)
|
||||
this.calendarCredentials = appCredentials.filter((cred) => cred.type.endsWith("_calendar"));
|
||||
this.calendarCredentials = appCredentials.filter(
|
||||
// Backwards compatibility until CRM manager is implemented
|
||||
(cred) => cred.type.endsWith("_calendar") || cred.type.endsWith("_crm")
|
||||
);
|
||||
this.videoCredentials = appCredentials
|
||||
.filter((cred) => cred.type.endsWith("_video") || cred.type.endsWith("_conferencing"))
|
||||
// Whenever a new video connection is added, latest credentials are added with the highest ID.
|
||||
@@ -518,7 +521,10 @@ export default class EventManager {
|
||||
* Not ideal but, if we don't find a destination calendar,
|
||||
* fallback to the first connected calendar - Shouldn't be a CRM calendar
|
||||
*/
|
||||
const [credential] = this.calendarCredentials.filter((cred) => !cred.type.endsWith("other_calendar"));
|
||||
// Backwards compatibility until CRM manager is created
|
||||
const [credential] = this.calendarCredentials.filter(
|
||||
(cred) => !cred.type.endsWith("other_calendar") || !cred.type.endsWith("crm")
|
||||
);
|
||||
if (credential) {
|
||||
const createdEvent = await createEvent(credential, event);
|
||||
log.silly("Created Calendar event", safeStringify({ createdEvent }));
|
||||
@@ -618,7 +624,8 @@ export default class EventManager {
|
||||
createdEvents = createdEvents.concat(
|
||||
await Promise.all(
|
||||
this.calendarCredentials
|
||||
.filter((cred) => cred.type.includes("other_calendar"))
|
||||
// Backwards compatibility until CRM manager is created
|
||||
.filter((cred) => cred.type.includes("other_calendar") || cred.type.includes("crm"))
|
||||
.map(async (cred) => await createEvent(cred, event))
|
||||
)
|
||||
);
|
||||
@@ -790,7 +797,8 @@ export default class EventManager {
|
||||
// Taking care of non-traditional calendar integrations
|
||||
result = result.concat(
|
||||
this.calendarCredentials
|
||||
.filter((cred) => cred.type.includes("other_calendar"))
|
||||
// Backwards compatibility until CRM manager is created
|
||||
.filter((cred) => cred.type.includes("other_calendar") && cred.type.includes("crm"))
|
||||
.map(async (cred) => {
|
||||
const calendarReference = booking.references.find((ref) => ref.type === cred.type);
|
||||
|
||||
|
||||
@@ -135,13 +135,13 @@ export const integrationsHandler = async ({ ctx, input }: IntegrationsOptions) =
|
||||
//TODO: Refactor this to pick up only needed fields and prevent more leaking
|
||||
let apps = await Promise.all(
|
||||
enabledApps.map(async ({ credentials: _, credential, key: _2 /* don't leak to frontend */, ...app }) => {
|
||||
const userCredentialIds = credentials.filter((c) => c.type === app.type && !c.teamId).map((c) => c.id);
|
||||
const userCredentialIds = credentials.filter((c) => c.appId === app.slug && !c.teamId).map((c) => c.id);
|
||||
const invalidCredentialIds = credentials
|
||||
.filter((c) => c.type === app.type && c.invalid)
|
||||
.filter((c) => c.appId === app.slug && c.invalid)
|
||||
.map((c) => c.id);
|
||||
const teams = await Promise.all(
|
||||
credentials
|
||||
.filter((c) => c.type === app.type && c.teamId)
|
||||
.filter((c) => c.appId === app.slug && c.teamId)
|
||||
.map(async (c) => {
|
||||
const team = userTeams.find((team) => team.id === c.teamId);
|
||||
if (!team) {
|
||||
|
||||
Vendored
+10
-1
@@ -66,6 +66,7 @@ export interface App {
|
||||
| `${string}_other`
|
||||
| `${string}_automation`
|
||||
| `${string}_analytics`
|
||||
| `${string}_crm`
|
||||
| `${string}_other_calendar`;
|
||||
|
||||
/**
|
||||
@@ -79,7 +80,15 @@ export interface App {
|
||||
/** A brief description, usually found in the app's package.json */
|
||||
description: string;
|
||||
/** TODO determine if we should use this instead of category */
|
||||
variant: "calendar" | "payment" | "conferencing" | "video" | "other" | "other_calendar" | "automation";
|
||||
variant:
|
||||
| "calendar"
|
||||
| "payment"
|
||||
| "conferencing"
|
||||
| "video"
|
||||
| "other"
|
||||
| "other_calendar"
|
||||
| "automation"
|
||||
| "crm";
|
||||
/** The slug for the app store public page inside `/apps/[slug] */
|
||||
slug: string;
|
||||
|
||||
|
||||
Reference in new issue
Block a user