Close.com App (#3709)
* WIP close.com app * Removing leaked dev key (now invalid) * Misspelled env variable * Making progress still WIP * Progress + tests * Final touches * More unit tests * Finished up tests * Merge main * Removing unneeded stuff + submodules * Removing static props, fields fix * Removing unneeded stuff p2 * Commenting * Standarizing APIs * Zodifying Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: zomars <zomars@me.com>
This commit is contained in:
co-authored by
kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
zomars
parent
8d0caa522e
commit
4ce6b91a79
@@ -805,7 +805,12 @@
|
||||
"enter_number_of_seats": "Enter number of seats",
|
||||
"you_can_manage_your_schedules": "You can manage your schedules on the Availability page.",
|
||||
"booking_full": "No more seats available",
|
||||
"api_keys": "API Keys",
|
||||
"api_keys": "API keys",
|
||||
"api_key": "API key",
|
||||
"test_api_key": "Test API key",
|
||||
"test_passed": "Test passed!",
|
||||
"test_failed": "Test failed",
|
||||
"provide_api_key": "Provide API key",
|
||||
"api_key_modal_subtitle": "API keys allow you to make API calls for your own account.",
|
||||
"api_keys_subtitle": "Generate API keys to use for accessing your own account.",
|
||||
"create_api_key": "Create an API key",
|
||||
@@ -869,7 +874,8 @@
|
||||
"how_you_want_add_cal_site": "How do you want to add Cal to your site?",
|
||||
"choose_ways_put_cal_site": "Choose one of the following ways to put Cal on your site.",
|
||||
"setting_up_zapier": "Setting up your Zapier integration",
|
||||
"generate_api_key": "Generate Api Key",
|
||||
"generate_api_key": "Generate API key",
|
||||
"generate_api_key_description": "Generate an API key to use with Cal.com at",
|
||||
"your_unique_api_key": "Your unique API key",
|
||||
"copy_safe_api_key": "Copy this API key and save it somewhere safe. If you lose this key you have to generate a new one.",
|
||||
"zapier_setup_instructions": "<0>Log into your Zapier account and create a new Zap.</0><1>Select Cal.com as your Trigger app. Also choose a Trigger event.</1><2>Choose your account and then enter your Unique API Key.</2><3>Test your Trigger.</3><4>You're set!</4>",
|
||||
@@ -879,6 +885,7 @@
|
||||
"calendar_url": "Calendar URL",
|
||||
"apple_server_generate_password": "Generate an app specific password to use with Cal.com at",
|
||||
"credentials_stored_encrypted": "Your credentials will be stored and encrypted.",
|
||||
"it_stored_encrypted": "It will be stored and encrypted.",
|
||||
"go_to_app_store": "Go to App Store",
|
||||
"calendar_error": "Something went wrong, try reconnecting your calendar with all necessary permissions",
|
||||
"set_your_phone_number": "Set a phone number for the meeting",
|
||||
|
||||
@@ -8,6 +8,7 @@ export const AppSetupMap = {
|
||||
"exchange2016-calendar": dynamic(() => import("../../exchange2016calendar/pages/setup")),
|
||||
"caldav-calendar": dynamic(() => import("../../caldavcalendar/pages/setup")),
|
||||
zapier: dynamic(() => import("../../zapier/pages/setup")),
|
||||
closecom: dynamic(() => import("../../closecomothercalendar/pages/setup")),
|
||||
};
|
||||
|
||||
export const AppSetupPage = (props: { slug: string }) => {
|
||||
|
||||
@@ -7,6 +7,7 @@ import dynamic from "next/dynamic";
|
||||
import { metadata as applecalendar_meta } from "./applecalendar/_metadata";
|
||||
import { metadata as around_meta } from "./around/_metadata";
|
||||
import { metadata as caldavcalendar_meta } from "./caldavcalendar/_metadata";
|
||||
import { metadata as closecomothercalendar_meta } from "./closecomothercalendar/_metadata";
|
||||
import { metadata as dailyvideo_meta } from "./dailyvideo/_metadata";
|
||||
import { metadata as routing_forms_meta } from "./ee/routing_forms/_metadata";
|
||||
import { metadata as exchange2013calendar_meta } from "./exchange2013calendar/_metadata";
|
||||
@@ -37,6 +38,7 @@ export const appStoreMetadata = {
|
||||
applecalendar: applecalendar_meta,
|
||||
around: around_meta,
|
||||
caldavcalendar: caldavcalendar_meta,
|
||||
closecomothercalendar: closecomothercalendar_meta,
|
||||
dailyvideo: dailyvideo_meta,
|
||||
routing_forms: routing_forms_meta,
|
||||
exchange2013calendar: exchange2013calendar_meta,
|
||||
@@ -67,6 +69,7 @@ export const InstallAppButtonMap = {
|
||||
applecalendar: dynamic(() => import("./applecalendar/components/InstallAppButton")),
|
||||
around: dynamic(() => import("./around/components/InstallAppButton")),
|
||||
caldavcalendar: dynamic(() => import("./caldavcalendar/components/InstallAppButton")),
|
||||
closecomothercalendar: dynamic(() => import("./closecomothercalendar/components/InstallAppButton")),
|
||||
exchange2013calendar: dynamic(() => import("./exchange2013calendar/components/InstallAppButton")),
|
||||
exchange2016calendar: dynamic(() => import("./exchange2016calendar/components/InstallAppButton")),
|
||||
giphy: dynamic(() => import("./giphy/components/InstallAppButton")),
|
||||
|
||||
@@ -6,6 +6,7 @@ export const apiHandlers = {
|
||||
applecalendar: import("./applecalendar/api"),
|
||||
around: import("./around/api"),
|
||||
caldavcalendar: import("./caldavcalendar/api"),
|
||||
closecomothercalendar: import("./closecomothercalendar/api"),
|
||||
routing_forms: import("./ee/routing_forms/api"),
|
||||
exchange2013calendar: import("./exchange2013calendar/api"),
|
||||
exchange2016calendar: import("./exchange2016calendar/api"),
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
items:
|
||||
- /api/app-store/closecomothercalendar/screenshot.png
|
||||
---
|
||||
|
||||
<Slider items={items} />
|
||||
|
||||
Close is the inside sales CRM of choice for startups and SMBs. Make more calls, send more emails and close more deals starting today.
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { App } from "@calcom/types/App";
|
||||
|
||||
import config from "./config.json";
|
||||
|
||||
export const metadata = {
|
||||
category: "other",
|
||||
// FIXME: Currently for an app to be shown as installed, it must have this variable set. Either hardcoded or if it depends on some env variable, that should be checked here
|
||||
installed: true,
|
||||
rating: 0,
|
||||
reviews: 0,
|
||||
trending: true,
|
||||
verified: true,
|
||||
licenseRequired: true,
|
||||
isProOnly: true,
|
||||
...config,
|
||||
} as App;
|
||||
|
||||
export default metadata;
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import checkSession from "../../_utils/auth";
|
||||
import { checkInstalled } from "../../_utils/installation";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const session = checkSession(req);
|
||||
await checkInstalled("closecom", session.user?.id);
|
||||
return res.status(200).json({ url: "/apps/closecom/setup" });
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { symmetricEncrypt } from "@calcom/lib/crypto";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import checkSession from "../../_utils/auth";
|
||||
|
||||
export async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const session = checkSession(req);
|
||||
|
||||
const { api_key } = req.body;
|
||||
if (!api_key) throw new HttpError({ statusCode: 400, message: "No Api Key provoided to check" });
|
||||
|
||||
const encrypted = symmetricEncrypt(JSON.stringify({ api_key }), process.env.CALENDSO_ENCRYPTION_KEY || "");
|
||||
|
||||
const data = {
|
||||
type: "closecom_other_calendar",
|
||||
key: { encrypted },
|
||||
userId: session.user?.id,
|
||||
appId: "closecom",
|
||||
};
|
||||
|
||||
try {
|
||||
await prisma.credential.create({
|
||||
data,
|
||||
});
|
||||
} catch (reason) {
|
||||
logger.error("Could not add Close.com app", reason);
|
||||
return res.status(500).json({ message: "Could not add Close.com app" });
|
||||
}
|
||||
|
||||
return res.status(200).json({ url: "/apps/installed" });
|
||||
}
|
||||
|
||||
export default defaultResponder(getHandler);
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import CloseCom from "@calcom/lib/CloseCom";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
import checkSession from "../../_utils/auth";
|
||||
|
||||
export async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { api_key } = req.body;
|
||||
if (!api_key) throw new HttpError({ statusCode: 400, message: "No Api Key provoided to check" });
|
||||
|
||||
checkSession(req);
|
||||
|
||||
const closeCom: CloseCom = new CloseCom(api_key);
|
||||
|
||||
try {
|
||||
const userInfo = await closeCom.me();
|
||||
if (userInfo.first_name) {
|
||||
return res.status(200).end();
|
||||
} else {
|
||||
return res.status(404).end();
|
||||
}
|
||||
} catch (e) {
|
||||
return res.status(500).json({ message: e });
|
||||
}
|
||||
}
|
||||
|
||||
export default defaultResponder(getHandler);
|
||||
@@ -0,0 +1,6 @@
|
||||
import { defaultHandler } from "@calcom/lib/server";
|
||||
|
||||
export default defaultHandler({
|
||||
GET: import("./_getAdd"),
|
||||
POST: import("./_postAdd"),
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
import { defaultHandler } from "@calcom/lib/server";
|
||||
|
||||
export default defaultHandler({
|
||||
POST: import("./_postCheck"),
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as add } from "./add";
|
||||
export { default as check } from "./check";
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { InstallAppButtonProps } from "@calcom/app-store/types";
|
||||
|
||||
import useAddAppMutation from "../../_utils/useAddAppMutation";
|
||||
|
||||
export default function InstallAppButton(props: InstallAppButtonProps) {
|
||||
const mutation = useAddAppMutation("closecom_other_calendar");
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.render({
|
||||
onClick() {
|
||||
mutation.mutate("");
|
||||
},
|
||||
loading: mutation.isLoading,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as InstallAppButton } from "./InstallAppButton";
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"/*": "Don't modify slug - If required, do it using cli edit command",
|
||||
"name": "Close.com",
|
||||
"title": "Close.com",
|
||||
"slug": "closecom",
|
||||
"type": "closecom_other_calendar",
|
||||
"imageSrc": "/api/app-store/closecomothercalendar/icon.svg",
|
||||
"logo": "/api/app-store/closecomothercalendar/icon.svg",
|
||||
"url": "https://cal.com/apps/closecom",
|
||||
"variant": "other",
|
||||
"categories": ["other"],
|
||||
"publisher": "Cal.com",
|
||||
"email": "help@cal.com",
|
||||
"description": "Close is the inside sales CRM of choice for startups and SMBs. Make more calls, send more emails and close more deals starting today.",
|
||||
"__createdUsingCli": true
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * as api from "./api";
|
||||
export * as lib from "./lib";
|
||||
export { metadata } from "./_metadata";
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { Config } from "@jest/types";
|
||||
|
||||
const config: Config.InitialOptions = {
|
||||
verbose: true,
|
||||
roots: ["<rootDir>"],
|
||||
testMatch: ["**/test/lib/**/*.(spec|test).(ts|tsx|js)"],
|
||||
transform: {
|
||||
"^.+\\.ts?$": "ts-jest",
|
||||
},
|
||||
transformIgnorePatterns: ["/node_modules/", "^.+\\.module\\.(css|sass|scss)$"],
|
||||
testEnvironment: "jsdom",
|
||||
setupFiles: ["<rootDir>/test/globals.ts"],
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,298 @@
|
||||
import { Credential } from "@prisma/client";
|
||||
import z from "zod";
|
||||
|
||||
import CloseCom, { CloseComCustomActivityCreate } from "@calcom/lib/CloseCom";
|
||||
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import type {
|
||||
Calendar,
|
||||
CalendarEvent,
|
||||
EventBusyDate,
|
||||
IntegrationCalendar,
|
||||
NewCalendarEventType,
|
||||
} from "@calcom/types/Calendar";
|
||||
|
||||
const apiKeySchema = z.object({
|
||||
encrypted: z.string(),
|
||||
});
|
||||
|
||||
const CALENDSO_ENCRYPTION_KEY = process.env.CALENDSO_ENCRYPTION_KEY || "";
|
||||
|
||||
// Cal.com Custom Activity Fields
|
||||
const calComCustomActivityFields: [string, string, boolean, boolean][] = [
|
||||
// Field name, field type, required?, multiple values?
|
||||
["Attendees", "contact", false, true],
|
||||
["Date & Time", "datetime", true, false],
|
||||
["Time zone", "text", true, false],
|
||||
["Organizer", "contact", true, false],
|
||||
["Additional notes", "text", false, false],
|
||||
];
|
||||
|
||||
/**
|
||||
* Authentication
|
||||
* Close.com requires Basic Auth for any request to their APIs, which is far from
|
||||
* ideal considering that such a strategy requires generating an API Key by the
|
||||
* user and input it in our system. A Setup page was created when trying to install
|
||||
* Close.com App in order to instruct how to create such resource and to obtain it.
|
||||
*
|
||||
* Meeting creation
|
||||
* Close.com does not expose a "Meeting" API, it may be available in the future.
|
||||
*
|
||||
* Per Close.com documentation (https://developer.close.com/resources/custom-activities):
|
||||
* "To work with Custom Activities, you will need to create a Custom Activity Type and
|
||||
* likely add one or more Activity Custom Fields to that type. Once the Custom Activity
|
||||
* Type is defined, you can create Custom Activity instances of that type as you would
|
||||
* any other activity."
|
||||
*
|
||||
* Contact creation
|
||||
* Every contact in Close.com need to belong to a Lead. When creating a contact in
|
||||
* Close.com as part of this integration, a new generic Lead will be created in order
|
||||
* to assign every contact created by this process, and it is named "From Cal.com"
|
||||
*/
|
||||
export default class CloseComCalendarService implements Calendar {
|
||||
private integrationName = "";
|
||||
private closeCom: CloseCom;
|
||||
private log: typeof logger;
|
||||
|
||||
constructor(credential: Credential) {
|
||||
this.integrationName = "closecom_other_calendar";
|
||||
this.log = logger.getChildLogger({ prefix: [`[[lib] ${this.integrationName}`] });
|
||||
|
||||
const parsedCredentialKey = apiKeySchema.safeParse(credential.key);
|
||||
|
||||
let decrypted;
|
||||
if (parsedCredentialKey.success) {
|
||||
decrypted = symmetricDecrypt(parsedCredentialKey.data.encrypted, CALENDSO_ENCRYPTION_KEY);
|
||||
const { api_key } = JSON.parse(decrypted);
|
||||
this.closeCom = new CloseCom(api_key);
|
||||
} else {
|
||||
throw Error(
|
||||
`No API Key found for userId ${credential.userId} and appId ${credential.appId}: ${parsedCredentialKey.error}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async closeComUpdateCustomActivity(uid: string, event: CalendarEvent) {
|
||||
const customActivityTypeInstanceData = await this.getCustomActivityTypeInstanceData(event);
|
||||
// Create Custom Activity type instance
|
||||
const customActivityTypeInstance = await this.closeCom.activity.custom.create(
|
||||
customActivityTypeInstanceData
|
||||
);
|
||||
return this.closeCom.activity.custom.update(uid, customActivityTypeInstance);
|
||||
}
|
||||
|
||||
private async closeComDeleteCustomActivity(uid: string) {
|
||||
return this.closeCom.activity.custom.delete(uid);
|
||||
}
|
||||
|
||||
getCloseComContactIds = async (event: CalendarEvent, leadFromCalComId: string) => {
|
||||
// Check if attendees exist or to see if any should be created
|
||||
const closeComContacts = await this.closeCom.contact.search({
|
||||
emails: event.attendees.map((att) => att.email),
|
||||
});
|
||||
// NOTE: If contact is duplicated in Close.com we will get more results
|
||||
// messing around with the expected number of contacts retrieved
|
||||
if (closeComContacts.data.length < event.attendees.length) {
|
||||
// Create missing contacts
|
||||
const attendeesEmails = event.attendees.map((att) => att.email);
|
||||
// Existing contacts based on attendees emails: contacts may have more
|
||||
// than one email, we just need the one used by the event.
|
||||
const existingContactsEmails = closeComContacts.data.flatMap((cont) =>
|
||||
cont.emails.filter((em) => attendeesEmails.includes(em.email)).map((ems) => ems.email)
|
||||
);
|
||||
const nonExistingContacts = event.attendees.filter(
|
||||
(attendee) => !existingContactsEmails.includes(attendee.email)
|
||||
);
|
||||
const createdContacts = await Promise.all(
|
||||
nonExistingContacts.map(
|
||||
async (att) =>
|
||||
await this.closeCom.contact.create({
|
||||
attendee: att,
|
||||
leadId: leadFromCalComId,
|
||||
})
|
||||
)
|
||||
);
|
||||
if (createdContacts.length === nonExistingContacts.length) {
|
||||
// All non existent contacts where created
|
||||
return closeComContacts.data.map((cont) => cont.id).concat(createdContacts.map((cont) => cont.id));
|
||||
} else {
|
||||
return Promise.reject("Some contacts were not possible to create in Close.com");
|
||||
}
|
||||
} else {
|
||||
return closeComContacts.data.map((cont) => cont.id);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if generic "From Cal.com" Lead exists, create it if not
|
||||
*/
|
||||
getCloseComGenericLeadId = async (): Promise<string> => {
|
||||
const closeComLeadNames = await this.closeCom.lead.list({ query: { _fields: ["name", "id"] } });
|
||||
const searchLeadFromCalCom = closeComLeadNames.data.filter((lead) => lead.name === "From Cal.com");
|
||||
if (searchLeadFromCalCom.length === 0) {
|
||||
// No Lead exists, create it
|
||||
const createdLeadFromCalCom = await this.closeCom.lead.create({
|
||||
companyName: "From Cal.com",
|
||||
description: "Generic Lead for Contacts created by Cal.com",
|
||||
});
|
||||
return createdLeadFromCalCom.id;
|
||||
} else {
|
||||
return searchLeadFromCalCom[0].id;
|
||||
}
|
||||
};
|
||||
|
||||
getCloseComCustomActivityTypeFieldsIds = async () => {
|
||||
// Check if Custom Activity Type exists
|
||||
const customActivities = await this.closeCom.customActivity.type.get();
|
||||
const calComCustomActivity = customActivities.data.filter((act) => act.name === "Cal.com Activity");
|
||||
if (calComCustomActivity.length > 0) {
|
||||
// Cal.com Custom Activity type exist
|
||||
// Get Custom Activity Fields
|
||||
const customActivityAllFields = await this.closeCom.customField.activity.get({
|
||||
query: { _fields: ["name", "custom_activity_type_id", "id"] },
|
||||
});
|
||||
const customActivityRelevantFields = customActivityAllFields.data.filter(
|
||||
(fie) => fie.custom_activity_type_id === calComCustomActivity[0].id
|
||||
);
|
||||
const customActivityFieldsNames = customActivityRelevantFields.map((fie) => fie.name);
|
||||
const customActivityFieldsExist = calComCustomActivityFields.map((cusFie) =>
|
||||
customActivityFieldsNames.includes(cusFie[0])
|
||||
);
|
||||
const [attendee, dateTime, timezone, organizer, additionalNotes] = await Promise.all(
|
||||
customActivityFieldsExist.map(async (exist, idx) => {
|
||||
if (!exist) {
|
||||
const [name, type, required, multiple] = calComCustomActivityFields[idx];
|
||||
const created = await this.closeCom.customField.activity.create({
|
||||
custom_activity_type_id: calComCustomActivity[0].id,
|
||||
name,
|
||||
type,
|
||||
required,
|
||||
accepts_multiple_values: multiple,
|
||||
editable_with_roles: [],
|
||||
});
|
||||
return created.id;
|
||||
} else {
|
||||
const index = customActivityFieldsNames.findIndex(
|
||||
(val) => val === calComCustomActivityFields[idx][0]
|
||||
);
|
||||
if (index >= 0) {
|
||||
return customActivityRelevantFields[index].id;
|
||||
} else {
|
||||
throw Error("Couldn't find the field index");
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
return {
|
||||
activityType: calComCustomActivity[0].id,
|
||||
fields: {
|
||||
attendee,
|
||||
dateTime,
|
||||
timezone,
|
||||
organizer,
|
||||
additionalNotes,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
// Cal.com Custom Activity type doesn't exist
|
||||
// Create Custom Activity Type
|
||||
const { id: activityType } = await this.closeCom.customActivity.type.create({
|
||||
name: "Cal.com Activity",
|
||||
description: "Bookings in your Cal.com account",
|
||||
});
|
||||
// Create Custom Activity Fields
|
||||
const [attendee, dateTime, timezone, organizer, additionalNotes] = await Promise.all(
|
||||
calComCustomActivityFields.map(async ([name, type, required, multiple]) => {
|
||||
const creation = await this.closeCom.customField.activity.create({
|
||||
custom_activity_type_id: activityType,
|
||||
name,
|
||||
type,
|
||||
required,
|
||||
accepts_multiple_values: multiple,
|
||||
editable_with_roles: [],
|
||||
});
|
||||
return creation.id;
|
||||
})
|
||||
);
|
||||
return {
|
||||
activityType,
|
||||
fields: {
|
||||
attendee,
|
||||
dateTime,
|
||||
timezone,
|
||||
organizer,
|
||||
additionalNotes,
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
getCustomActivityTypeInstanceData = async (event: CalendarEvent): Promise<CloseComCustomActivityCreate> => {
|
||||
// Get Cal.com generic Lead
|
||||
const leadFromCalComId = await this.getCloseComGenericLeadId();
|
||||
// Get Contacts ids
|
||||
const contactsIds = await this.getCloseComContactIds(event, leadFromCalComId);
|
||||
// Get Custom Activity Type id
|
||||
const customActivityTypeAndFieldsIds = await this.getCloseComCustomActivityTypeFieldsIds();
|
||||
// Prepare values for each Custom Activity Fields
|
||||
const customActivityFieldsValue = {
|
||||
attendee: contactsIds.length > 1 ? contactsIds.slice(1) : null,
|
||||
dateTime: event.startTime,
|
||||
timezone: event.attendees[0].timeZone,
|
||||
organizer: contactsIds[0],
|
||||
additionalNotes: event.additionalNotes ?? null,
|
||||
};
|
||||
// Preparing Custom Activity Instance data for Close.com
|
||||
return Object.assign(
|
||||
{},
|
||||
{
|
||||
custom_activity_type_id: customActivityTypeAndFieldsIds.activityType,
|
||||
lead_id: leadFromCalComId,
|
||||
}, // This is to add each field as "custom.FIELD_ID": "value" in the object
|
||||
...Object.keys(customActivityTypeAndFieldsIds.fields).map((fieldKey: string) => {
|
||||
const key = fieldKey as keyof typeof customActivityTypeAndFieldsIds.fields;
|
||||
return {
|
||||
[`custom.${customActivityTypeAndFieldsIds.fields[key]}`]: customActivityFieldsValue[key],
|
||||
};
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
async createEvent(event: CalendarEvent): Promise<NewCalendarEventType> {
|
||||
const customActivityTypeInstanceData = await this.getCustomActivityTypeInstanceData(event);
|
||||
// Create Custom Activity type instance
|
||||
const customActivityTypeInstance = await this.closeCom.activity.custom.create(
|
||||
customActivityTypeInstanceData
|
||||
);
|
||||
return Promise.resolve({
|
||||
uid: customActivityTypeInstance.id,
|
||||
id: customActivityTypeInstance.id,
|
||||
type: this.integrationName,
|
||||
password: "",
|
||||
url: "",
|
||||
additionalInfo: {
|
||||
customActivityTypeInstanceData,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async updateEvent(uid: string, event: CalendarEvent): Promise<any> {
|
||||
return await this.closeComUpdateCustomActivity(uid, event);
|
||||
}
|
||||
|
||||
async deleteEvent(uid: string): Promise<void> {
|
||||
return await this.closeComDeleteCustomActivity(uid);
|
||||
}
|
||||
|
||||
async getAvailability(
|
||||
dateFrom: string,
|
||||
dateTo: string,
|
||||
selectedCalendars: IntegrationCalendar[]
|
||||
): Promise<EventBusyDate[]> {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
async listCalendars(event?: CalendarEvent): Promise<IntegrationCalendar[]> {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as CalendarService } from "./CalendarService";
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"name": "@calcom/closecom",
|
||||
"version": "0.0.0",
|
||||
"main": "./index.ts",
|
||||
"description": "Close is the inside sales CRM of choice for startups and SMBs. Make more calls, send more emails and close more deals starting today.",
|
||||
"scripts": {
|
||||
"test": "jest",
|
||||
"test:coverage": "jest --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*",
|
||||
"@calcom/prisma": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*",
|
||||
"@types/jest": "^26.0.0",
|
||||
"jest": "^26.0.0",
|
||||
"ts-jest": "^26.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useRouter } from "next/router";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Check, X } from "react-feather";
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
import z from "zod";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import showToast from "@calcom/lib/notification";
|
||||
import Button from "@calcom/ui/v2/Button";
|
||||
import { Form, TextField } from "@calcom/ui/v2/form/fields";
|
||||
|
||||
const formSchema = z.object({
|
||||
api_key: z.string(),
|
||||
});
|
||||
|
||||
export default function CloseComSetup() {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const [testPassed, setTestPassed] = useState<boolean | undefined>(undefined);
|
||||
const [testLoading, setTestLoading] = useState<boolean>(false);
|
||||
|
||||
const form = useForm<{
|
||||
api_key: string;
|
||||
}>({
|
||||
resolver: zodResolver(formSchema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
if (testPassed === false) {
|
||||
setTestPassed(undefined);
|
||||
}
|
||||
}, 3000);
|
||||
return () => clearTimeout(timer);
|
||||
}, [testPassed]);
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-gray-200">
|
||||
<div className="m-auto rounded bg-white p-5 md:w-[520px] md:p-10">
|
||||
<div className="flex flex-col space-y-5 md:flex-row md:space-y-0 md:space-x-5">
|
||||
<div>
|
||||
{/* eslint-disable @next/next/no-img-element */}
|
||||
<img
|
||||
src="/api/app-store/closecomothercalendar/icon.svg"
|
||||
alt="Apple Calendar"
|
||||
className="h-12 w-12 max-w-2xl"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-gray-600">{t("provide_api_key")}</h1>
|
||||
|
||||
<div className="mt-1 text-sm">
|
||||
{t("generate_api_key_description")}{" "}
|
||||
<a
|
||||
className="text-indigo-400"
|
||||
href="https://app.close.com/settings/api/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
Close.com
|
||||
</a>
|
||||
. {t("it_stored_encrypted")}
|
||||
</div>
|
||||
<div className="my-2 mt-3">
|
||||
<Form
|
||||
form={form}
|
||||
handleSubmit={async (values) => {
|
||||
const res = await fetch("/api/integrations/closecomothercalendar/add", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(values),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
if (res.ok) {
|
||||
router.push(json.url);
|
||||
} else {
|
||||
showToast(json.message, "error");
|
||||
}
|
||||
}}>
|
||||
<fieldset className="space-y-2" disabled={form.formState.isSubmitting}>
|
||||
<Controller
|
||||
name="api_key"
|
||||
control={form.control}
|
||||
render={({ field: { onBlur, onChange } }) => (
|
||||
<TextField
|
||||
className="my-0"
|
||||
onBlur={onBlur}
|
||||
disabled={testPassed === true}
|
||||
name="api_key"
|
||||
placeholder="api_xyz..."
|
||||
onChange={async (e) => {
|
||||
onChange(e.target.value);
|
||||
form.setValue("api_key", e.target.value);
|
||||
await form.trigger("api_key");
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</fieldset>
|
||||
<div className="mt-5 justify-end space-x-2 sm:mt-4 sm:flex">
|
||||
<Button type="button" color="secondary" onClick={() => router.back()}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
loading={testLoading}
|
||||
disabled={testPassed === true}
|
||||
StartIcon={testPassed !== undefined ? (testPassed ? Check : X) : undefined}
|
||||
className={
|
||||
testPassed !== undefined
|
||||
? testPassed
|
||||
? " !bg-green-100 !text-green-700 hover:bg-green-100"
|
||||
: "!border-red-700 bg-red-100 !text-red-700 hover:bg-red-100"
|
||||
: "secondary"
|
||||
}
|
||||
color={testPassed === true ? "minimal" : "secondary"}
|
||||
onClick={async () => {
|
||||
const check = await form.trigger("api_key");
|
||||
if (!check) return;
|
||||
const api_key = form.getValues("api_key");
|
||||
setTestLoading(true);
|
||||
const res = await fetch("/api/integrations/closecomothercalendar/check", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ api_key }),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status === 200) {
|
||||
setTestPassed(true);
|
||||
} else {
|
||||
setTestPassed(false);
|
||||
}
|
||||
setTestLoading(false);
|
||||
}}>
|
||||
{t(
|
||||
testPassed !== undefined ? (testPassed ? "test_passed" : "test_failed") : "test_api_key"
|
||||
)}
|
||||
</Button>
|
||||
<Button type="submit" loading={form.formState.isSubmitting}>
|
||||
{t("save")}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Toaster position="bottom-right" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<svg viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<path d="M63.9,33.3 L63.9,30.6 C63.9,28.3 63.0,26.2 61.3,24.2 C60.4,23.2 59.2,22.1 57.8,21.2 C57.1,20.7 56.3,20.2 55.4,19.7 C54.7,19.4 54.1,19.0 53.4,18.7 C51.5,17.9 49.5,17.2 47.3,16.6 C47.6,17.5 47.8,18.5 48.0,19.4 C48.2,20.4 48.4,21.4 48.6,22.4 C49.7,22.8 50.9,23.2 51.9,23.7 C52.4,23.9 52.9,24.1 53.4,24.4 C53.6,24.5 53.9,24.6 54.1,24.7 C55.0,25.2 55.7,25.7 56.4,26.2 C59.0,27.9 60.6,29.9 61.1,31.9 C60.6,34.0 59.0,36.0 56.4,37.7 C56.7,38.5 57.0,39.2 57.2,39.9 C57.5,40.8 57.7,41.8 57.8,42.7 C59.2,41.8 60.4,40.7 61.3,39.7 C63.0,37.7 63.9,35.6 63.9,33.3" id="Fill-1" fill="#2F577E"></path>
|
||||
<path d="M24.9,48.9 C23.8,48.0 22.7,47.1 21.7,46.1 C21.9,46.9 22.1,47.8 22.4,48.6 C23.2,48.7 24.1,48.8 24.9,48.9" id="Fill-2" fill="#2F577E"></path>
|
||||
<path d="M30.7,46.6 C31.1,46.9 31.5,47.2 31.9,47.5 C32.8,48.0 33.6,48.6 34.4,49.0 C34.5,49.1 34.6,49.2 34.7,49.2 C36.1,49.2 37.6,49.1 39.0,48.9 C39.8,48.8 40.7,48.7 41.5,48.6 C42.5,48.4 43.5,48.2 44.5,48.0 C45.4,47.8 46.4,47.6 47.3,47.3 C49.5,46.7 51.5,46.0 53.4,45.2 C53.2,44.3 53.0,43.4 52.7,42.5 C52.6,42.3 52.6,42.0 52.5,41.7 C52.3,41.2 52.1,40.7 51.9,40.2 C50.9,40.7 49.7,41.1 48.6,41.5 C47.8,41.8 46.9,42.0 46.1,42.2 C46.0,42.3 45.9,42.3 45.8,42.3 C44.8,42.6 43.9,42.8 42.9,42.9 C42.4,43.0 41.9,43.1 41.4,43.2 C39.9,43.5 38.4,43.6 36.8,43.8 C35.2,43.9 33.6,44.0 31.9,44.0 C30.3,44.0 28.7,43.9 27.1,43.8 C28.2,44.8 29.5,45.7 30.7,46.6" id="Fill-3" fill="#2F577E"></path>
|
||||
<path d="M11.2,21.4 C11.2,21.6 11.3,21.9 11.4,22.2 C11.6,22.7 11.8,23.2 12.0,23.7 C13.0,23.2 14.2,22.8 15.3,22.4 C16.1,22.1 17.0,21.9 17.8,21.7 C17.9,21.6 18.0,21.6 18.1,21.6 C19.1,21.4 20.0,21.1 21.0,21.0 C21.5,20.9 22.0,20.8 22.5,20.7 C24.0,20.5 25.5,20.3 27.1,20.1 C28.7,20.0 30.3,20.0 31.9,20.0 C33.6,20.0 35.2,20.0 36.8,20.1 C35.7,19.1 34.4,18.2 33.2,17.3 C32.8,17.0 32.4,16.7 31.9,16.4 C31.1,15.9 30.3,15.3 29.5,14.9 C29.4,14.8 29.3,14.7 29.2,14.7 C27.7,14.7 26.3,14.9 24.9,15.0 C24.1,15.1 23.2,15.2 22.4,15.3 C21.4,15.5 20.4,15.7 19.4,15.9 C18.5,16.1 17.5,16.3 16.6,16.6 C14.4,17.2 12.4,17.9 10.5,18.7 C10.7,19.6 10.9,20.5 11.2,21.4" id="Fill-4" fill="#2F577E"></path>
|
||||
<path d="M39.0,15.0 C40.1,15.9 41.2,16.8 42.2,17.8 C42.0,17.0 41.8,16.1 41.5,15.3 C40.7,15.2 39.8,15.1 39.0,15.0" id="Fill-5" fill="#2F577E"></path>
|
||||
<path d="M2.8,31.9 C3.3,29.9 4.9,27.9 7.5,26.2 C7.2,25.4 6.9,24.7 6.7,24.0 C6.4,23.1 6.2,22.1 6.0,21.2 C4.7,22.1 3.5,23.2 2.6,24.2 C0.9,26.2 6.7e-14,28.3 6.7e-14,30.6 L6.7e-14,33.3 C6.7e-14,35.6 0.9,37.7 2.6,39.7 C3.5,40.7 4.7,41.7 6.0,42.7 C6.8,43.2 7.6,43.7 8.5,44.2 C9.2,44.5 9.8,44.9 10.5,45.2 C12.4,46.0 14.4,46.7 16.6,47.3 C16.3,46.4 16.1,45.4 15.9,44.5 C15.7,43.5 15.5,42.5 15.3,41.5 C14.2,41.1 13.0,40.7 12.0,40.2 C11.5,40.0 11.0,39.8 10.5,39.5 C10.3,39.4 10.0,39.3 9.8,39.2 C8.9,38.7 8.2,38.2 7.5,37.7 C4.9,36.0 3.3,34.0 2.8,31.9" id="Fill-6" fill="#2F577E"></path>
|
||||
<path d="M39.0,48.9 C37.6,49.0 36.1,49.2 34.7,49.2 C35.4,49.7 36.2,50.1 36.9,50.4 C37.6,49.9 38.3,49.4 39.0,48.9" id="Fill-7" fill="#F4B563"></path>
|
||||
<path d="M55.5,10.3 L53.6,8.4 C52.0,6.8 49.8,5.9 47.2,5.7 C45.8,5.6 44.3,5.8 42.7,6.1 C41.8,6.2 40.8,6.5 39.9,6.7 C39.2,6.9 38.5,7.2 37.7,7.5 C35.9,8.2 33.9,9.1 31.9,10.2 C32.8,10.7 33.6,11.2 34.5,11.8 C35.3,12.3 36.1,12.9 36.9,13.5 C38.0,12.9 39.1,12.4 40.2,12.0 C40.7,11.8 41.2,11.6 41.7,11.4 C42.0,11.3 42.3,11.3 42.5,11.2 C43.4,10.9 44.3,10.7 45.2,10.5 C48.2,10.0 50.8,10.3 52.6,11.3" id="Fill-8" fill="#F4B563"></path>
|
||||
<path d="M52.6,11.3 C53.6,13.1 53.9,15.7 53.4,18.7 C54.1,19.0 54.7,19.4 55.4,19.7 C56.3,20.2 57.1,20.7 57.8,21.2 C58.1,19.6 58.3,18.1 58.2,16.7 C58.0,14.1 57.1,11.9 55.5,10.3 C55.2,9.9 54.9,9.7 54.5,9.4" id="Fill-9" fill="#F4B563"></path>
|
||||
<path d="M41.4,43.2 C41.9,43.1 42.4,43.0 42.9,42.9 C43.9,42.8 44.8,42.6 45.8,42.3 C45.9,42.3 46.0,42.3 46.1,42.2 C47.1,41.2 48.0,40.1 48.9,39.0 C49.4,38.3 50.0,37.6 50.4,36.9 C51.0,36.1 51.6,35.3 52.2,34.5 C52.7,33.6 53.2,32.8 53.7,32.0 C54.8,30.0 55.7,28.0 56.4,26.2 C55.7,25.7 55.0,25.2 54.1,24.7 C53.9,24.6 53.6,24.5 53.4,24.4 C52.9,24.1 52.4,23.9 51.9,23.7 C51.5,24.8 51.0,25.9 50.4,27.0 C50.1,27.7 49.7,28.5 49.2,29.2 C49.2,29.3 49.1,29.4 49.0,29.5 C48.6,30.3 48.0,31.1 47.5,32.0 C47.2,32.4 46.9,32.8 46.6,33.2 C45.7,34.4 44.8,35.7 43.8,36.8 C42.7,38.1 41.6,39.3 40.4,40.4 C39.3,41.6 38.1,42.7 36.8,43.8 C38.4,43.6 39.9,43.5 41.4,43.2" id="Fill-10" fill="#F4B563"></path>
|
||||
<path d="M24.9,15.0 C26.3,14.8 27.7,14.7 29.2,14.7 C28.5,14.2 27.7,13.8 27.0,13.5 C26.3,13.9 25.6,14.5 24.9,15.0" id="Fill-11" fill="#F4B563"></path>
|
||||
<path d="M11.3,52.6 C10.3,50.8 10.0,48.2 10.5,45.2 C9.8,44.9 9.2,44.5 8.5,44.2 C7.6,43.7 6.8,43.2 6.0,42.7 C5.8,44.3 5.6,45.8 5.7,47.2 C5.9,49.8 6.8,52.0 8.4,53.6 L10.3,55.5 C11.9,57.1 14.1,58.0 16.7,58.2 C18.1,58.3 19.6,58.1 21.2,57.9 C22.1,57.7 23.1,57.5 24.0,57.2 C24.7,57.0 25.4,56.7 26.2,56.4 C28.0,55.7 30.0,54.8 31.9,53.7 C31.1,53.2 30.3,52.7 29.4,52.2 C28.6,51.6 27.8,51.1 27.0,50.5 C25.9,51.0 24.8,51.5 23.7,51.9 C23.2,52.1 22.7,52.3 22.2,52.5 C21.9,52.6 21.6,52.7 21.4,52.7 C20.5,53.0 19.6,53.2 18.7,53.4 C15.7,53.9 13.1,53.6 11.3,52.6" id="Fill-12" fill="#F4B563"></path>
|
||||
<path d="M9.8,39.2 C10.0,39.3 10.3,39.4 10.5,39.5 C11.0,39.8 11.5,40.0 12.0,40.2 C12.4,39.1 12.9,38.0 13.5,36.9 C13.8,36.2 14.2,35.4 14.7,34.7 C14.7,34.6 14.8,34.5 14.9,34.4 C15.3,33.6 15.9,32.8 16.4,31.9 C16.7,31.5 17.0,31.1 17.3,30.7 C18.2,29.5 19.1,28.2 20.1,27.1 C21.2,25.8 22.3,24.6 23.5,23.5 C24.6,22.3 25.8,21.2 27.1,20.1 C25.5,20.3 24.0,20.4 22.5,20.7 C22.0,20.8 21.5,20.9 21.0,21.0 C20.0,21.1 19.1,21.4 18.1,21.6 C18.0,21.6 17.9,21.6 17.8,21.7 C16.8,22.7 15.9,23.8 15.0,24.9 C14.5,25.6 13.9,26.3 13.4,27.0 C12.9,27.8 12.3,28.6 11.7,29.4 C11.2,30.3 10.7,31.1 10.2,31.9 C9.1,33.9 8.2,35.9 7.5,37.7 C8.2,38.2 8.9,38.7 9.8,39.2" id="Fill-13" fill="#F4B563"></path>
|
||||
<path d="M15.0,39.0 C14.8,37.6 14.7,36.1 14.7,34.7 C14.2,35.4 13.8,36.2 13.5,36.9 C13.9,37.6 14.5,38.3 15.0,39.0" id="Fill-14" fill="#5CAFCD"></path>
|
||||
<path d="M11.3,11.3 C13.1,10.3 15.7,10.0 18.7,10.5 C19.0,9.8 19.4,9.2 19.7,8.5 C20.2,7.6 20.7,6.8 21.2,6.1 C19.6,5.8 18.1,5.6 16.7,5.7 C14.1,5.9 11.9,6.8 10.3,8.4 C9.9,8.7 9.7,9.0 9.4,9.4 C9.7,9.0 9.9,8.7 10.3,8.4 L8.4,10.3 C6.8,11.9 5.9,14.1 5.7,16.7 C5.6,18.1 5.8,19.6 6.0,21.2 C6.2,22.1 6.4,23.1 6.7,24.0 C6.9,24.7 7.2,25.4 7.5,26.2 C8.2,28.0 9.1,30.0 10.2,32.0 C10.7,31.1 11.2,30.3 11.7,29.4 C12.3,28.6 12.9,27.8 13.4,27.0 C12.9,25.9 12.4,24.8 12.0,23.7 C11.8,23.2 11.6,22.7 11.4,22.2 C11.3,21.9 11.2,21.6 11.2,21.4 C10.9,20.5 10.7,19.6 10.5,18.7 C10.0,15.7 10.3,13.1 11.3,11.3" id="Fill-15" fill="#5CAFCD"></path>
|
||||
<path d="M48.9,24.9 C49.0,26.3 49.2,27.8 49.2,29.2 C49.7,28.5 50.1,27.7 50.4,27.0 C50.0,26.3 49.4,25.6 48.9,24.9" id="Fill-16" fill="#5CAFCD"></path>
|
||||
<path d="M43.2,22.5 C43.1,22.0 43.0,21.5 42.9,21.0 C42.8,20.0 42.5,19.1 42.3,18.1 C42.3,18.0 42.3,17.9 42.2,17.8 C41.2,16.8 40.1,15.9 39.0,15.0 C38.3,14.5 37.6,13.9 36.9,13.5 C36.1,12.9 35.3,12.3 34.5,11.7 C33.6,11.2 32.8,10.7 31.9,10.2 C30.0,9.1 28.0,8.2 26.2,7.5 C25.7,8.2 25.2,8.9 24.7,9.8 C24.6,10.0 24.5,10.3 24.4,10.5 C24.1,11.0 23.9,11.5 23.7,12.0 C24.8,12.4 25.9,12.9 27.0,13.5 C27.7,13.8 28.5,14.2 29.2,14.7 C29.3,14.7 29.4,14.8 29.5,14.9 C30.3,15.3 31.1,15.9 31.9,16.4 C32.4,16.7 32.8,17.0 33.2,17.3 C34.4,18.2 35.7,19.1 36.8,20.1 C38.1,21.2 39.3,22.3 40.4,23.5 C41.6,24.6 42.7,25.8 43.8,27.1 C43.6,25.5 43.4,24.0 43.2,22.5" id="Fill-17" fill="#5CAFCD"></path>
|
||||
<path d="M20.7,41.4 C20.8,41.9 20.9,42.4 21.0,42.9 C21.1,43.9 21.4,44.8 21.6,45.8 C21.6,45.9 21.6,46.0 21.7,46.1 C22.7,47.1 23.8,48.0 24.9,48.9 C25.6,49.4 26.3,50.0 27.0,50.4 C27.8,51.0 28.6,51.6 29.4,52.2 C30.3,52.7 31.1,53.2 31.9,53.7 C33.9,54.8 35.9,55.7 37.7,56.4 C38.2,55.7 38.7,55.0 39.2,54.1 C39.3,53.9 39.4,53.6 39.5,53.4 C39.8,52.9 40.0,52.4 40.2,51.9 C39.1,51.5 38.0,51.0 36.9,50.4 C36.2,50.1 35.4,49.7 34.7,49.2 C34.6,49.2 34.5,49.1 34.4,49.0 C33.6,48.6 32.8,48.0 31.9,47.5 C31.5,47.2 31.1,46.9 30.7,46.6 C29.5,45.7 28.2,44.8 27.1,43.8 C25.8,42.7 24.6,41.6 23.5,40.4 C22.3,39.3 21.2,38.1 20.1,36.8 C20.3,38.4 20.4,39.9 20.7,41.4" id="Fill-18" fill="#5CAFCD"></path>
|
||||
<path d="M53.6,55.5 L55.5,53.6 C57.1,52.0 58.0,49.8 58.2,47.2 C58.3,45.8 58.1,44.3 57.8,42.7 C57.7,41.8 57.5,40.8 57.2,39.9 C57.0,39.2 56.7,38.5 56.4,37.7 C55.7,35.9 54.8,33.9 53.7,32 C53.2,32.8 52.7,33.6 52.2,34.5 C51.6,35.3 51.0,36.1 50.4,36.9 C51.0,38.0 51.5,39.1 51.9,40.2 C52.1,40.7 52.3,41.2 52.5,41.7 C52.6,42.0 52.6,42.3 52.7,42.5 C53.0,43.4 53.2,44.3 53.4,45.2 C53.9,48.2 53.6,50.8 52.6,52.6 C50.8,53.6 48.2,53.9 45.2,53.4 C44.9,54.1 44.5,54.7 44.2,55.4 C43.7,56.3 43.2,57.1 42.7,57.8 C44.3,58.1 45.8,58.3 47.2,58.2 C49.8,58.0 52.0,57.1 53.6,55.5" id="Fill-19" fill="#5CAFCD"></path>
|
||||
<path d="M17.3,30.7 C17.0,31.1 16.7,31.5 16.4,32.0 C15.9,32.8 15.3,33.6 14.9,34.4 C14.8,34.5 14.7,34.6 14.7,34.7 C14.7,36.1 14.8,37.6 15.0,39.0 C15.1,39.8 15.2,40.7 15.3,41.5 C15.5,42.5 15.7,43.5 15.9,44.5 C16.1,45.4 16.3,46.4 16.6,47.3 C17.2,49.5 17.9,51.5 18.7,53.4 C19.6,53.2 20.5,53.0 21.4,52.7 C21.6,52.6 21.9,52.6 22.2,52.5 C22.7,52.3 23.2,52.1 23.7,51.9 C23.2,50.9 22.8,49.7 22.4,48.6 C22.1,47.8 21.9,46.9 21.7,46.1 C21.6,46.0 21.6,45.9 21.6,45.8 C21.4,44.8 21.1,43.9 21.0,42.9 C20.9,42.4 20.8,41.9 20.7,41.4 C20.4,39.9 20.3,38.4 20.1,36.8 C20.0,35.2 19.9,33.6 19.9,32.0 C19.9,30.3 20.0,28.7 20.1,27.1 C19.1,28.2 18.2,29.5 17.3,30.7" id="Fill-20" fill="#47B178"></path>
|
||||
<path d="M48.9,39.0 C48.0,40.1 47.1,41.2 46.1,42.2 C46.9,42.0 47.8,41.8 48.6,41.5 C48.7,40.7 48.8,39.8 48.9,39.0" id="Fill-21" fill="#47B178"></path>
|
||||
<path d="M15.0,24.9 C15.9,23.8 16.8,22.8 17.8,21.7 C17.0,21.9 16.1,22.1 15.3,22.4 C15.2,23.2 15.1,24.1 15.0,24.9" id="Fill-22" fill="#47B178"></path>
|
||||
<path d="M46.6,33.2 C46.9,32.8 47.2,32.4 47.5,32.0 C48.0,31.1 48.6,30.3 49.0,29.5 C49.1,29.4 49.2,29.3 49.2,29.2 C49.2,27.8 49.0,26.3 48.9,24.9 C48.8,24.1 48.7,23.2 48.6,22.4 C48.4,21.4 48.2,20.4 48.0,19.4 C47.8,18.5 47.6,17.5 47.3,16.6 C46.7,14.4 46.0,12.4 45.2,10.5 C44.3,10.7 43.4,10.9 42.5,11.2 C42.3,11.3 42.0,11.3 41.7,11.4 C41.2,11.6 40.7,11.8 40.2,12.0 C40.7,13.0 41.1,14.2 41.5,15.3 C41.8,16.1 42.0,17.0 42.2,17.8 C42.3,17.9 42.3,18.0 42.3,18.1 C42.5,19.1 42.8,20.0 42.9,21.0 C43.0,21.5 43.1,22.0 43.2,22.5 C43.4,24.0 43.6,25.5 43.8,27.1 C43.9,28.7 43.9,30.3 43.9,32.0 C43.9,33.6 43.9,35.2 43.8,36.8 C44.8,35.7 45.7,34.4 46.6,33.2" id="Fill-23" fill="#47B178"></path>
|
||||
<path d="M42.7,6.0 C41.7,4.7 40.7,3.5 39.7,2.6 C37.7,0.9 35.6,0 33.3,0 L30.6,0 C28.3,0 26.2,0.9 24.2,2.6 C23.2,3.5 22.1,4.7 21.2,6.0 C20.7,6.8 20.2,7.6 19.7,8.5 C19.4,9.2 19.0,9.8 18.7,10.5 C17.9,12.4 17.2,14.4 16.6,16.6 C17.5,16.3 18.5,16.1 19.4,15.9 C20.4,15.7 21.4,15.5 22.4,15.3 C22.8,14.2 23.2,13.0 23.7,12.0 C23.9,11.5 24.1,11.0 24.4,10.5 C24.5,10.3 24.6,10.0 24.7,9.8 C25.2,8.9 25.7,8.2 26.2,7.5 C27.9,4.9 29.9,3.3 31.9,2.8 C34.0,3.3 36.0,4.9 37.7,7.5 C38.5,7.2 39.2,6.9 39.9,6.7" id="Fill-24" fill="#47B178"></path>
|
||||
<path d="M30.6,63.9 L33.3,63.9 C35.6,63.9 37.7,63.0 39.7,61.3 C40.7,60.4 41.7,59.2 42.7,57.8 C43.2,57.1 43.7,56.3 44.2,55.4 C44.5,54.7 44.9,54.1 45.2,53.4 C46.0,51.5 46.7,49.5 47.3,47.3 L44.5,48.0 C43.5,48.2 42.5,48.4 41.5,48.6 C41.1,49.7 40.7,50.9 40.2,51.9 C40.0,52.4 39.8,52.9 39.5,53.4 C39.4,53.6 39.3,53.9 39.2,54.1 C38.7,55.0 38.2,55.7 37.7,56.4 C36.0,59.0 34.0,60.6 31.9,61.1 C29.9,60.6 27.9,59.0 26.2,56.4 C25.4,56.7 24.7,57.0 24.0,57.2 L21.2,57.8 C22.1,59.2 23.2,60.4 24.2,61.3 C26.2,63.0 28.3,63.9 30.6,63.9" id="Fill-26" fill="#47B178"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 515 KiB |
@@ -0,0 +1,12 @@
|
||||
jest.mock("@calcom/lib/logger", () => ({
|
||||
debug: jest.fn(),
|
||||
error: jest.fn(),
|
||||
log: jest.fn(),
|
||||
getChildLogger: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("@calcom/lib/crypto", () => ({
|
||||
symmetricDecrypt: () => `{
|
||||
"userApiKey": "test"
|
||||
}`,
|
||||
}));
|
||||
@@ -0,0 +1,293 @@
|
||||
import CloseComCalendarService from "@calcom/closecom/lib/CalendarService";
|
||||
import CloseCom from "@calcom/lib/CloseCom";
|
||||
import { CalendarEvent } from "@calcom/types/Calendar";
|
||||
|
||||
jest.mock("@calcom/lib/CloseCom", () => {
|
||||
return class {
|
||||
constructor() {
|
||||
/* Mock */
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
const mockedCredential = {
|
||||
id: 1,
|
||||
key: "",
|
||||
appId: "",
|
||||
type: "",
|
||||
userId: 1,
|
||||
};
|
||||
|
||||
// getCloseComGenericLeadId
|
||||
test("check generic lead generator: already exists", async () => {
|
||||
CloseCom.prototype.lead = {
|
||||
list: () => ({
|
||||
data: [{ name: "From Cal.com", id: "abc" }],
|
||||
}),
|
||||
} as any;
|
||||
|
||||
const closeComCalendarService = new CloseComCalendarService(mockedCredential);
|
||||
const spy = jest.spyOn(closeComCalendarService, "getCloseComGenericLeadId");
|
||||
const mockedGetCloseComGenericLeadId = spy.getMockImplementation();
|
||||
if (mockedGetCloseComGenericLeadId) {
|
||||
const id = await mockedGetCloseComGenericLeadId();
|
||||
expect(id).toEqual("abc");
|
||||
}
|
||||
});
|
||||
|
||||
// getCloseComGenericLeadId
|
||||
test("check generic lead generator: doesn't exist", async () => {
|
||||
CloseCom.prototype.lead = {
|
||||
list: () => ({
|
||||
data: [],
|
||||
}),
|
||||
create: () => ({ id: "def" }),
|
||||
} as any;
|
||||
|
||||
const closeComCalendarService = new CloseComCalendarService(mockedCredential);
|
||||
const spy = jest.spyOn(closeComCalendarService, "getCloseComGenericLeadId");
|
||||
const mockedGetCloseComGenericLeadId = spy.getMockImplementation();
|
||||
if (mockedGetCloseComGenericLeadId) {
|
||||
const id = await mockedGetCloseComGenericLeadId();
|
||||
expect(id).toEqual("def");
|
||||
}
|
||||
});
|
||||
|
||||
// getCloseComContactIds
|
||||
test("retrieve contact IDs: all exist", async () => {
|
||||
const attendees = [
|
||||
{ email: "test1@example.com", id: "test1" },
|
||||
{ email: "test2@example.com", id: "test2" },
|
||||
];
|
||||
|
||||
const event = {
|
||||
attendees,
|
||||
} as CalendarEvent;
|
||||
|
||||
CloseCom.prototype.contact = {
|
||||
search: () => ({ data: attendees }),
|
||||
} as any;
|
||||
|
||||
const closeComCalendarService = new CloseComCalendarService(mockedCredential);
|
||||
const spy = jest.spyOn(closeComCalendarService, "getCloseComContactIds");
|
||||
const mockedGetCloseComContactIds = spy.getMockImplementation();
|
||||
if (mockedGetCloseComContactIds) {
|
||||
const contactIds = await mockedGetCloseComContactIds(event, "leadId");
|
||||
expect(contactIds).toEqual(["test1", "test2"]);
|
||||
}
|
||||
});
|
||||
|
||||
// getCloseComContactIds
|
||||
test("retrieve contact IDs: some don't exist", async () => {
|
||||
const attendees = [{ email: "test1@example.com", id: "test1" }, { email: "test2@example.com" }];
|
||||
|
||||
const event = {
|
||||
attendees,
|
||||
} as CalendarEvent;
|
||||
|
||||
CloseCom.prototype.contact = {
|
||||
search: () => ({ data: [{ emails: [{ email: "test1@example.com" }], id: "test1" }] }),
|
||||
create: () => ({ id: "test3" }),
|
||||
} as any;
|
||||
|
||||
const closeComCalendarService = new CloseComCalendarService(mockedCredential);
|
||||
const spy = jest.spyOn(closeComCalendarService, "getCloseComContactIds");
|
||||
const mockedGetCloseComContactIds = spy.getMockImplementation();
|
||||
if (mockedGetCloseComContactIds) {
|
||||
const contactIds = await mockedGetCloseComContactIds(event, "leadId");
|
||||
expect(contactIds).toEqual(["test1", "test3"]);
|
||||
}
|
||||
});
|
||||
|
||||
// getCloseComCustomActivityTypeFieldsIds
|
||||
test("retrieve custom fields for custom activity type: type doesn't exist, no field created", async () => {
|
||||
CloseCom.prototype.activity = {
|
||||
type: {
|
||||
get: () => [],
|
||||
},
|
||||
} as any;
|
||||
|
||||
CloseCom.prototype.customActivity = {
|
||||
type: {
|
||||
get: () => ({ data: [] }),
|
||||
create: () => ({ id: "type1" }),
|
||||
},
|
||||
} as any;
|
||||
|
||||
CloseCom.prototype.customField = {
|
||||
activity: {
|
||||
create: (data: { name: string }) => ({ id: `field${data.name.length}${data.name[0]}` }),
|
||||
},
|
||||
} as any;
|
||||
|
||||
const closeComCalendarService = new CloseComCalendarService(mockedCredential);
|
||||
const spy = jest.spyOn(closeComCalendarService, "getCloseComCustomActivityTypeFieldsIds");
|
||||
const mockedGetCloseComCustomActivityTypeFieldsIds = spy.getMockImplementation();
|
||||
if (mockedGetCloseComCustomActivityTypeFieldsIds) {
|
||||
const contactIds = await mockedGetCloseComCustomActivityTypeFieldsIds();
|
||||
expect(contactIds).toEqual({
|
||||
activityType: "type1",
|
||||
fields: {
|
||||
attendee: "field9A",
|
||||
dateTime: "field11D",
|
||||
timezone: "field9T",
|
||||
organizer: "field9O",
|
||||
additionalNotes: "field16A",
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// getCloseComCustomActivityTypeFieldsIds
|
||||
test("retrieve custom fields for custom activity type: type exists, no field created", async () => {
|
||||
CloseCom.prototype.activity = {
|
||||
type: {
|
||||
get: () => [],
|
||||
},
|
||||
} as any;
|
||||
|
||||
CloseCom.prototype.customActivity = {
|
||||
type: {
|
||||
get: () => ({ data: [{ id: "typeX", name: "Cal.com Activity" }] }),
|
||||
},
|
||||
} as any;
|
||||
|
||||
CloseCom.prototype.customField = {
|
||||
activity: {
|
||||
get: () => ({ data: [{ id: "fieldY", custom_activity_type_id: "typeX", name: "Attendees" }] }),
|
||||
create: (data: { name: string }) => ({ id: `field${data.name.length}${data.name[0]}` }),
|
||||
},
|
||||
} as any;
|
||||
|
||||
const closeComCalendarService = new CloseComCalendarService(mockedCredential);
|
||||
const spy = jest.spyOn(closeComCalendarService, "getCloseComCustomActivityTypeFieldsIds");
|
||||
const mockedGetCloseComCustomActivityTypeFieldsIds = spy.getMockImplementation();
|
||||
if (mockedGetCloseComCustomActivityTypeFieldsIds) {
|
||||
const contactIds = await mockedGetCloseComCustomActivityTypeFieldsIds();
|
||||
expect(contactIds).toEqual({
|
||||
activityType: "typeX",
|
||||
fields: {
|
||||
attendee: "fieldY",
|
||||
dateTime: "field11D",
|
||||
timezone: "field9T",
|
||||
organizer: "field9O",
|
||||
additionalNotes: "field16A",
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// getCustomActivityTypeInstanceData
|
||||
test("prepare data to create custom activity type instance: two attendees, no additional notes", async () => {
|
||||
const attendees = [
|
||||
{ email: "test1@example.com", id: "test1", timeZone: "America/Montevideo" },
|
||||
{ email: "test2@example.com" },
|
||||
];
|
||||
|
||||
const now = new Date();
|
||||
|
||||
const event = {
|
||||
attendees,
|
||||
startTime: now.toISOString(),
|
||||
} as CalendarEvent;
|
||||
|
||||
CloseCom.prototype.activity = {
|
||||
type: {
|
||||
get: () => [],
|
||||
},
|
||||
} as any;
|
||||
|
||||
CloseCom.prototype.customActivity = {
|
||||
type: {
|
||||
get: () => ({ data: [] }),
|
||||
create: () => ({ id: "type1" }),
|
||||
},
|
||||
} as any;
|
||||
|
||||
CloseCom.prototype.customField = {
|
||||
activity: {
|
||||
create: (data: { name: string }) => ({ id: `field${data.name.length}${data.name[0]}` }),
|
||||
},
|
||||
} as any;
|
||||
|
||||
CloseCom.prototype.lead = {
|
||||
list: () => ({
|
||||
data: [],
|
||||
}),
|
||||
create: () => ({ id: "def" }),
|
||||
} as any;
|
||||
|
||||
const closeComCalendarService = new CloseComCalendarService(mockedCredential);
|
||||
const spy = jest.spyOn(closeComCalendarService, "getCustomActivityTypeInstanceData");
|
||||
const mockedGetCustomActivityTypeInstanceData = spy.getMockImplementation();
|
||||
if (mockedGetCustomActivityTypeInstanceData) {
|
||||
const data = await mockedGetCustomActivityTypeInstanceData(event);
|
||||
expect(data).toEqual({
|
||||
custom_activity_type_id: "type1",
|
||||
lead_id: "def",
|
||||
"custom.field9A": ["test3"],
|
||||
"custom.field11D": now.toISOString(),
|
||||
"custom.field9T": "America/Montevideo",
|
||||
"custom.field9O": "test1",
|
||||
"custom.field16A": null,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// getCustomActivityTypeInstanceData
|
||||
test("prepare data to create custom activity type instance: one attendees, with additional notes", async () => {
|
||||
const attendees = [{ email: "test1@example.com", id: "test1", timeZone: "America/Montevideo" }];
|
||||
|
||||
const now = new Date();
|
||||
|
||||
const event = {
|
||||
attendees,
|
||||
startTime: now.toISOString(),
|
||||
additionalNotes: "Some comment!",
|
||||
} as CalendarEvent;
|
||||
|
||||
CloseCom.prototype.activity = {
|
||||
type: {
|
||||
get: () => [],
|
||||
},
|
||||
} as any;
|
||||
|
||||
CloseCom.prototype.customActivity = {
|
||||
type: {
|
||||
get: () => ({ data: [] }),
|
||||
create: () => ({ id: "type1" }),
|
||||
},
|
||||
} as any;
|
||||
|
||||
CloseCom.prototype.customField = {
|
||||
activity: {
|
||||
create: (data: { name: string }) => ({ id: `field${data.name.length}${data.name[0]}` }),
|
||||
},
|
||||
} as any;
|
||||
|
||||
CloseCom.prototype.lead = {
|
||||
list: () => ({
|
||||
data: [{ name: "From Cal.com", id: "abc" }],
|
||||
}),
|
||||
} as any;
|
||||
|
||||
const closeComCalendarService = new CloseComCalendarService(mockedCredential);
|
||||
const spy = jest.spyOn(closeComCalendarService, "getCustomActivityTypeInstanceData");
|
||||
const mockedGetCustomActivityTypeInstanceData = spy.getMockImplementation();
|
||||
if (mockedGetCustomActivityTypeInstanceData) {
|
||||
const data = await mockedGetCustomActivityTypeInstanceData(event);
|
||||
expect(data).toEqual({
|
||||
custom_activity_type_id: "type1",
|
||||
lead_id: "abc",
|
||||
"custom.field9A": null,
|
||||
"custom.field11D": now.toISOString(),
|
||||
"custom.field9T": "America/Montevideo",
|
||||
"custom.field9O": "test1",
|
||||
"custom.field16A": "Some comment!",
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
// import * as example from "./_example";
|
||||
import * as applecalendar from "./applecalendar";
|
||||
import * as caldavcalendar from "./caldavcalendar";
|
||||
import * as closecomothercalendar from "./closecomothercalendar";
|
||||
import * as dailyvideo from "./dailyvideo";
|
||||
import * as exchange2013calendar from "./exchange2013calendar";
|
||||
import * as exchange2016calendar from "./exchange2016calendar";
|
||||
@@ -26,6 +27,7 @@ const appStore = {
|
||||
// example,
|
||||
applecalendar,
|
||||
caldavcalendar,
|
||||
closecomothercalendar,
|
||||
dailyvideo,
|
||||
googlecalendar,
|
||||
googlevideo,
|
||||
|
||||
+281
-43
@@ -1,7 +1,22 @@
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { Person } from "@calcom/types/Calendar";
|
||||
|
||||
export type CloseComLead = {
|
||||
companyName?: string;
|
||||
contactName?: string;
|
||||
contactEmail?: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
export type CloseComLeadCreateResult = {
|
||||
status_id: string;
|
||||
status_label: string;
|
||||
display_name: string;
|
||||
addresses: { [key: string]: any }[];
|
||||
name: string;
|
||||
contacts: { [key: string]: any }[];
|
||||
[key: CloseComCustomActivityCustomField<string>]: string;
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type CloseComStatus = {
|
||||
@@ -10,66 +25,245 @@ export type CloseComStatus = {
|
||||
label: string;
|
||||
};
|
||||
|
||||
export default class CloseCom {
|
||||
private apiUrl = "https://api.close.com/api/v1/";
|
||||
private apiKey: string | undefined = undefined;
|
||||
export type CloseComCustomActivityTypeCreate = {
|
||||
name: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
constructor() {
|
||||
if (!process.env.CLOSECOM_API_KEY) throw Error("Close.com Api Key not present");
|
||||
this.apiKey = process.env.CLOSECOM_API_KEY;
|
||||
export type CloseComContactSearch = {
|
||||
data: {
|
||||
__object_type: "contact";
|
||||
emails: {
|
||||
email: string;
|
||||
type: string;
|
||||
}[];
|
||||
id: string;
|
||||
lead_id: string;
|
||||
name: string;
|
||||
}[];
|
||||
cursor: null;
|
||||
};
|
||||
|
||||
export type CloseComCustomActivityTypeGet = {
|
||||
data: {
|
||||
api_create_only: boolean;
|
||||
created_by: string;
|
||||
date_created: string;
|
||||
date_updated: string;
|
||||
description: string;
|
||||
editable_with_roles: string[];
|
||||
fields: CloseComCustomActivityFieldGet["data"][number][];
|
||||
id: string;
|
||||
name: string;
|
||||
organization_id: string;
|
||||
updated_by: string;
|
||||
}[];
|
||||
cursor: null;
|
||||
};
|
||||
|
||||
export type CloseComCustomActivityFieldCreate = {
|
||||
custom_activity_type_id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
required: boolean;
|
||||
accepts_multiple_values: boolean;
|
||||
editable_with_roles: string[];
|
||||
};
|
||||
|
||||
export type CloseComCustomActivityFieldGet = {
|
||||
data: {
|
||||
custom_activity_type_id: string;
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
type: string;
|
||||
required: boolean;
|
||||
accepts_multiple_values: boolean;
|
||||
editable_with_roles: string[];
|
||||
created_by: string;
|
||||
updated_by: string;
|
||||
date_created: string;
|
||||
date_updated: string;
|
||||
organization_id: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type CloseComCustomActivityCreate = {
|
||||
custom_activity_type_id: string;
|
||||
lead_id: string;
|
||||
[key: CloseComCustomActivityCustomField<string>]: string;
|
||||
};
|
||||
|
||||
export type typeCloseComCustomActivityGet = {
|
||||
organization_id: string;
|
||||
contact_id: any;
|
||||
date_updated: string;
|
||||
user_name: string;
|
||||
created_by_name: "Bruce Wayne";
|
||||
id: string;
|
||||
created_by: string;
|
||||
status: string;
|
||||
user_id: string;
|
||||
users: any[];
|
||||
lead_id: string;
|
||||
_type: string;
|
||||
updated_by: string;
|
||||
custom_activity_type_id: string;
|
||||
date_created: string;
|
||||
updated_by_name: string;
|
||||
[key: CloseComCustomActivityCustomField<string>]: string;
|
||||
};
|
||||
|
||||
type CloseComCustomActivityCustomField<T extends string> = `custom.${T}`;
|
||||
|
||||
const environmentApiKey = process.env.CLOSECOM_API_KEY || "";
|
||||
|
||||
/**
|
||||
* This class to instance communicating to Close.com APIs requires an API Key.
|
||||
*
|
||||
* You can either pass to the constructor an API Key or have one defined as an
|
||||
* environment variable in case the communication to Close.com is just for
|
||||
* one account only, not configurable by any user at any moment.
|
||||
*/
|
||||
export default class CloseCom {
|
||||
private apiUrl = "https://api.close.com/api/v1";
|
||||
private apiKey: string | undefined = undefined;
|
||||
private log: typeof logger;
|
||||
|
||||
constructor(providedApiKey = "") {
|
||||
if (!providedApiKey && !environmentApiKey) throw Error("Close.com Api Key not present");
|
||||
this.apiKey = providedApiKey || environmentApiKey;
|
||||
this.log = logger.getChildLogger({ prefix: [`[[lib] close.com`] });
|
||||
}
|
||||
|
||||
public static lead(): [CloseCom, CloseComLead] {
|
||||
return [new this(), {} as CloseComLead];
|
||||
}
|
||||
|
||||
public me = async () => {
|
||||
return this._get({ urlPath: "/me/" });
|
||||
};
|
||||
|
||||
public contact = {
|
||||
search: async ({ emails }: { emails: string[] }) => {
|
||||
search: async ({ emails }: { emails: string[] }): Promise<CloseComContactSearch> => {
|
||||
return this._post({
|
||||
url: `${this.apiUrl}/data/search/`,
|
||||
data: closeComQueries.contact.getContactSearchQuery(emails),
|
||||
urlPath: "/data/search/",
|
||||
data: closeComQueries.contact.search(emails),
|
||||
});
|
||||
},
|
||||
create: async (data: {
|
||||
attendee: Person;
|
||||
leadId: string;
|
||||
}): Promise<CloseComContactSearch["data"][number]> => {
|
||||
return this._post({ urlPath: "/contact/", data: closeComQueries.contact.create(data) });
|
||||
},
|
||||
};
|
||||
|
||||
public lead = {
|
||||
status: async () => {
|
||||
return this._get({ url: `${this.apiUrl}/status/lead/` });
|
||||
list: async ({
|
||||
query,
|
||||
}: {
|
||||
query: { [key: string]: any };
|
||||
}): Promise<{ data: { [key: string]: any }[] }> => {
|
||||
return this._get({ urlPath: "/lead", query });
|
||||
},
|
||||
create: async (data: CloseComLead) => {
|
||||
status: async () => {
|
||||
return this._get({ urlPath: `/status/lead/` });
|
||||
},
|
||||
create: async (data: CloseComLead): Promise<CloseComLeadCreateResult> => {
|
||||
return this._post({
|
||||
url: `${this.apiUrl}/lead/`,
|
||||
data: closeComQueries.lead.getCreateLeadQuery(data),
|
||||
urlPath: "/lead/",
|
||||
data: closeComQueries.lead.create(data),
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
private _get = async ({ url, ...rest }: { url: string }) => {
|
||||
return await this._request({ url, method: "get", ...rest });
|
||||
public customActivity = {
|
||||
type: {
|
||||
create: async (
|
||||
data: CloseComCustomActivityTypeCreate
|
||||
): Promise<CloseComCustomActivityTypeGet["data"][number]> => {
|
||||
return this._post({
|
||||
urlPath: "/custom_activity",
|
||||
data: closeComQueries.customActivity.type.create(data),
|
||||
});
|
||||
},
|
||||
get: async (): Promise<CloseComCustomActivityTypeGet> => {
|
||||
return this._get({ urlPath: "/custom_activity" });
|
||||
},
|
||||
},
|
||||
};
|
||||
private _post = async ({ url, data, ...rest }: { url: string; data: Record<string, unknown> }) => {
|
||||
return this._request({ url, method: "post", data, ...rest });
|
||||
|
||||
public customField = {
|
||||
activity: {
|
||||
create: async (
|
||||
data: CloseComCustomActivityFieldCreate
|
||||
): Promise<CloseComCustomActivityFieldGet["data"][number]> => {
|
||||
return this._post({ urlPath: "/custom_field/activity/", data });
|
||||
},
|
||||
get: async ({ query }: { query: { [key: string]: any } }): Promise<CloseComCustomActivityFieldGet> => {
|
||||
return this._get({ urlPath: "/custom_field/activity/", query });
|
||||
},
|
||||
},
|
||||
};
|
||||
private _delete = async ({ url, ...rest }: { url: string }) => {
|
||||
return this._request({ url, method: "delete", ...rest });
|
||||
|
||||
public activity = {
|
||||
custom: {
|
||||
create: async (
|
||||
data: CloseComCustomActivityCreate
|
||||
): Promise<CloseComCustomActivityTypeGet["data"][number]> => {
|
||||
return this._post({ urlPath: "/activity/custom/", data });
|
||||
},
|
||||
delete: async (uuid: string) => {
|
||||
return this._delete({ urlPath: `/activity/custom/${uuid}/` });
|
||||
},
|
||||
update: async (
|
||||
uuid: string,
|
||||
data: Partial<CloseComCustomActivityCreate>
|
||||
): Promise<CloseComCustomActivityTypeGet["data"][number]> => {
|
||||
return this._put({ urlPath: `/activity/custom/${uuid}/`, data });
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
private _get = async ({ urlPath, query }: { urlPath: string; query?: { [key: string]: any } }) => {
|
||||
return await this._request({ urlPath, method: "get", query });
|
||||
};
|
||||
private _post = async ({ urlPath, data }: { urlPath: string; data: Record<string, unknown> }) => {
|
||||
return this._request({ urlPath, method: "post", data });
|
||||
};
|
||||
private _put = async ({ urlPath, data }: { urlPath: string; data: Record<string, unknown> }) => {
|
||||
return this._request({ urlPath, method: "put", data });
|
||||
};
|
||||
private _delete = async ({ urlPath }: { urlPath: string }) => {
|
||||
return this._request({ urlPath, method: "delete" });
|
||||
};
|
||||
private _request = async ({
|
||||
url,
|
||||
urlPath,
|
||||
data,
|
||||
...rest
|
||||
method,
|
||||
query,
|
||||
}: {
|
||||
url: string;
|
||||
urlPath: string;
|
||||
method: string;
|
||||
query?: { [key: string]: any };
|
||||
data?: Record<string, unknown>;
|
||||
}) => {
|
||||
this.log.debug(method, urlPath, query, data);
|
||||
const credentials = Buffer.from(`${this.apiKey}:`).toString("base64");
|
||||
const headers = {
|
||||
Authorization: `Basic ${credentials}`,
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
return await fetch(url, { headers, body: JSON.stringify(data), ...rest }).then(async (response) => {
|
||||
const queryString = query ? `?${new URLSearchParams(query).toString()}` : "";
|
||||
return await fetch(`${this.apiUrl}${urlPath}${queryString}`, {
|
||||
headers,
|
||||
method,
|
||||
body: JSON.stringify(data),
|
||||
}).then(async (response) => {
|
||||
if (!response.ok) {
|
||||
const message = `An error has occured: ${response.status}`;
|
||||
const message = `[Close.com app] An error has occured: ${response.status}`;
|
||||
this.log.error(await response.json());
|
||||
throw new Error(message);
|
||||
}
|
||||
return await response.json();
|
||||
@@ -79,11 +273,11 @@ export default class CloseCom {
|
||||
|
||||
export const closeComQueries = {
|
||||
contact: {
|
||||
getContactSearchQuery(contactEmails: string[]) {
|
||||
search(contactEmails: string[]) {
|
||||
return {
|
||||
limit: null,
|
||||
_fields: {
|
||||
contact: ["id", "name"],
|
||||
contact: ["id", "name", "emails"],
|
||||
},
|
||||
query: {
|
||||
negate: false,
|
||||
@@ -130,24 +324,68 @@ export const closeComQueries = {
|
||||
sort: [],
|
||||
};
|
||||
},
|
||||
},
|
||||
lead: {
|
||||
getCreateLeadQuery({ companyName, contactEmail, contactName }: CloseComLead) {
|
||||
create(data: { attendee: Person; leadId: string }) {
|
||||
return {
|
||||
name: companyName,
|
||||
contacts: [
|
||||
{
|
||||
name: contactName,
|
||||
email: contactEmail,
|
||||
emails: [
|
||||
{
|
||||
type: "office",
|
||||
email: contactEmail,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
lead_id: data.leadId,
|
||||
name: data.attendee.name ?? data.attendee.email,
|
||||
emails: [{ email: data.attendee.email, type: "office" }],
|
||||
};
|
||||
},
|
||||
},
|
||||
lead: {
|
||||
create({ companyName, contactEmail, contactName, description }: CloseComLead) {
|
||||
return {
|
||||
name: companyName,
|
||||
...(description ? { description } : {}),
|
||||
...(contactEmail && contactName
|
||||
? {
|
||||
contacts: [
|
||||
{
|
||||
name: contactName,
|
||||
email: contactEmail,
|
||||
emails: [
|
||||
{
|
||||
type: "office",
|
||||
email: contactEmail,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
},
|
||||
},
|
||||
customActivity: {
|
||||
type: {
|
||||
create({ name, description }: CloseComCustomActivityTypeCreate) {
|
||||
return {
|
||||
name: name,
|
||||
description: description,
|
||||
api_create_only: false,
|
||||
editable_with_roles: ["admin"],
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
customField: {
|
||||
activity: {
|
||||
create({
|
||||
custom_activity_type_id,
|
||||
name,
|
||||
type,
|
||||
required,
|
||||
accepts_multiple_values,
|
||||
}: CloseComCustomActivityFieldCreate) {
|
||||
return {
|
||||
custom_activity_type_id,
|
||||
name,
|
||||
type,
|
||||
required,
|
||||
accepts_multiple_values,
|
||||
editable_with_roles: [],
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -22,6 +22,7 @@ export function deriveAppDictKeyFromType(appType: string, dict: Record<string, u
|
||||
}
|
||||
|
||||
// Transform as last resort removing all underscores, applies to `hubspot_other_calendar` to be `hubsporothercalendar`
|
||||
// and `closecom_other_calendar` to be `closecomothercalendar`
|
||||
const appTypeVariant3 = appType.replace(/_/g, "");
|
||||
handlers = dict[appTypeVariant3];
|
||||
if (handlers) {
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { Prisma } from "@prisma/client";
|
||||
import dotEnv from "dotenv";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
import prisma from ".";
|
||||
|
||||
dotEnv.config({ path: "../../.env.appStore" });
|
||||
|
||||
export const seededForm = {
|
||||
id: "948ae412-d995-4865-875a-48302588de03",
|
||||
name: "Seeded Form - Pro",
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
require("dotenv").config({ path: "../../.env.appStore" });
|
||||
|
||||
async function seedAppData() {
|
||||
const form = await prisma.app_RoutingForms_Form.findUnique({
|
||||
where: {
|
||||
@@ -175,7 +175,9 @@ export default async function main() {
|
||||
await createApp("exchange2013-calendar", "exchange2013calendar", ["calendar"], "exchange2013_calendar");
|
||||
await createApp("exchange2016-calendar", "exchange2016calendar", ["calendar"], "exchange2016_calendar");
|
||||
try {
|
||||
const { client_secret, client_id, redirect_uris } = JSON.parse(process.env.GOOGLE_API_CREDENTIALS).web;
|
||||
const { client_secret, client_id, redirect_uris } = JSON.parse(
|
||||
process.env.GOOGLE_API_CREDENTIALS || ""
|
||||
).web;
|
||||
await createApp("google-calendar", "googlecalendar", ["calendar"], "google_calendar", {
|
||||
client_id,
|
||||
client_secret,
|
||||
@@ -238,6 +240,8 @@ export default async function main() {
|
||||
client_secret: process.env.HUBSPOT_CLIENT_SECRET,
|
||||
});
|
||||
}
|
||||
// No need to check if environment variable is present, the API Key is set up by the user, not the system
|
||||
await createApp("closecom", "closecomothercalendar", ["other"], "closecom_other_calendar");
|
||||
await createApp("wipe-my-cal", "wipemycalother", ["other"], "wipemycal_other");
|
||||
if (process.env.GIPHY_API_KEY) {
|
||||
await createApp("giphy", "giphy", ["other"], "giphy_other", {
|
||||
|
||||
@@ -82,7 +82,7 @@ export const Button = forwardRef<HTMLAnchorElement | HTMLButtonElement, ButtonPr
|
||||
ref: forwardedRef,
|
||||
className: classNames(
|
||||
// base styles independent what type of button it is
|
||||
"inline-flex justify-center items-center text-sm font-medium",
|
||||
"inline-flex justify-center items-center text-sm font-medium relative",
|
||||
// different styles depending on size
|
||||
size === "base" && "h-9 px-4 py-2.5 rounded-md ",
|
||||
size === "lg" && "h-[36px] px-4 py-2.5 rounded-md",
|
||||
|
||||
@@ -6101,6 +6101,14 @@
|
||||
dependencies:
|
||||
"@types/istanbul-lib-report" "*"
|
||||
|
||||
"@types/jest@^26.0.0":
|
||||
version "26.0.24"
|
||||
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.24.tgz#943d11976b16739185913a1936e0de0c4a7d595a"
|
||||
integrity sha512-E/X5Vib8BWqZNRlDxj9vYXhsDwPYbPINqKF9BsnSoon4RQ0D9moEuLD8txgyypFLH7J4+Lho9Nr/c8H0Fi+17w==
|
||||
dependencies:
|
||||
jest-diff "^26.0.0"
|
||||
pretty-format "^26.0.0"
|
||||
|
||||
"@types/jest@^27.5.1":
|
||||
version "27.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-27.5.1.tgz#2c8b6dc6ff85c33bcd07d0b62cb3d19ddfdb3ab9"
|
||||
@@ -14497,7 +14505,7 @@ jest-config@^28.1.3:
|
||||
slash "^3.0.0"
|
||||
strip-json-comments "^3.1.1"
|
||||
|
||||
jest-diff@^26.6.2:
|
||||
jest-diff@^26.0.0, jest-diff@^26.6.2:
|
||||
version "26.6.2"
|
||||
resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-26.6.2.tgz#1aa7468b52c3a68d7d5c5fdcdfcd5e49bd164394"
|
||||
integrity sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==
|
||||
@@ -17363,7 +17371,7 @@ next-api-middleware@^1.0.1:
|
||||
dependencies:
|
||||
debug "^4.3.2"
|
||||
|
||||
next-auth@^4.10.3:
|
||||
next-auth@^4.10.3, next-auth@^4.9.0:
|
||||
version "4.10.3"
|
||||
resolved "https://registry.yarnpkg.com/next-auth/-/next-auth-4.10.3.tgz#0a952dd5004fd2ac2ba414c990922cf9b33951a3"
|
||||
integrity sha512-7zc4aXYc/EEln7Pkcsn21V1IevaTZsMLJwapfbnKA4+JY0+jFzWbt5p/ljugesGIrN4VOZhpZIw50EaFZyghJQ==
|
||||
@@ -19066,7 +19074,7 @@ pretty-error@^4.0.0:
|
||||
lodash "^4.17.20"
|
||||
renderkid "^3.0.0"
|
||||
|
||||
pretty-format@^26.6.2:
|
||||
pretty-format@^26.0.0, pretty-format@^26.6.2:
|
||||
version "26.6.2"
|
||||
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.6.2.tgz#e35c2705f14cb7fe2fe94fa078345b444120fc93"
|
||||
integrity sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==
|
||||
|
||||
Reference in New Issue
Block a user