feat: Salesforce - on booking cancel, write to event object (#20601)
* Type app options * Refactor `writeToPersonRecord` to `writeToRecord` * Abstract writing to salesforce record options component * Add option to write to event object on cancellation * Add on booking cancel write to event object to schema * Refactor writing to record on booking option to use `<WriteToObjectSettings />` * Pass event object to crm delete method * When cancelling booking write to event object - App data isn't being read * V1 pass app data to crm service * Undo .env commit * Fix passing event to cancel crm cancel event method * Pass event type metadata to `EventManager` in `handleCancelBooking` * Handle writing to event record * Type fix
This commit is contained in:
@@ -3076,5 +3076,7 @@
|
||||
"enable_delegation_credential_description": "Grant Cal.com automatic access to the calendars of all organization members by enabling delegation credential.",
|
||||
"disable_delegation_credential": "Disable Delegation Credential",
|
||||
"disable_delegation_credential_description": "Once delegation credential is disabled, organization members who haven’t connected their calendars will need to do so manually.",
|
||||
"salesforce_on_cancel_write_to_event": "On cancelled booking, write to event record instead of deleting event",
|
||||
"salesforce_on_every_cancellation": "On every cancellation",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import type z from "zod";
|
||||
|
||||
import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext";
|
||||
import AppCard from "@calcom/app-store/_components/AppCard";
|
||||
@@ -16,13 +15,9 @@ import { Select } from "@calcom/ui/components/form";
|
||||
import { Switch } from "@calcom/ui/components/form";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
|
||||
import {
|
||||
SalesforceRecordEnum,
|
||||
WhenToWriteToRecord,
|
||||
SalesforceFieldType,
|
||||
DateFieldTypeData,
|
||||
} from "../lib/enums";
|
||||
import type { appDataSchema, writeToRecordEntrySchema } from "../zod";
|
||||
import { SalesforceRecordEnum } from "../lib/enums";
|
||||
import type { appDataSchema } from "../zod";
|
||||
import WriteToObjectSettings, { BookingActionEnum } from "./components/WriteToObjectSettings";
|
||||
|
||||
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) {
|
||||
const pathname = usePathname();
|
||||
@@ -48,6 +43,8 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
||||
const onBookingWriteToRecordFields = getAppData("onBookingWriteToRecordFields") ?? {};
|
||||
const ignoreGuests = getAppData("ignoreGuests") ?? false;
|
||||
const roundRobinSkipFallbackToLeadOwner = getAppData("roundRobinSkipFallbackToLeadOwner") ?? false;
|
||||
const onCancelWriteToEventRecord = getAppData("onCancelWriteToEventRecord") ?? false;
|
||||
const onCancelWriteToEventRecordFields = getAppData("onCancelWriteToEventRecordFields") ?? {};
|
||||
|
||||
const { t } = useLocale();
|
||||
|
||||
@@ -60,49 +57,6 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
||||
recordOptions.find((option) => option.value === createEventOn) ?? recordOptions[0]
|
||||
);
|
||||
|
||||
const fieldTypeOptions = [
|
||||
{ label: t("text"), value: SalesforceFieldType.TEXT },
|
||||
{ label: t("date"), value: SalesforceFieldType.DATE },
|
||||
{ label: t("phone").charAt(0).toUpperCase() + t("phone").slice(1), value: SalesforceFieldType.PHONE },
|
||||
{ label: t("checkbox"), value: SalesforceFieldType.CHECKBOX },
|
||||
{ label: t("picklist"), value: SalesforceFieldType.PICKLIST },
|
||||
{ label: t("custom"), value: SalesforceFieldType.CUSTOM },
|
||||
];
|
||||
|
||||
const [writeToPersonObjectFieldType, setWriteToPersonObjectFieldType] = useState(fieldTypeOptions[0]);
|
||||
|
||||
const whenToWriteToRecordOptions = [
|
||||
{ label: t("on_every_booking"), value: WhenToWriteToRecord.EVERY_BOOKING },
|
||||
{ label: t("only_if_field_is_empty"), value: WhenToWriteToRecord.FIELD_EMPTY },
|
||||
];
|
||||
|
||||
const [whenToWriteToPersonRecord, setWhenToWriteToPersonRecord] = useState(
|
||||
whenToWriteToRecordOptions.find((option) => option.value === WhenToWriteToRecord.FIELD_EMPTY) ??
|
||||
whenToWriteToRecordOptions[0]
|
||||
);
|
||||
|
||||
const dateFieldValueOptions = [
|
||||
{ label: t("booking_start_date"), value: DateFieldTypeData.BOOKING_START_DATE },
|
||||
{ label: t("booking_created_date"), value: DateFieldTypeData.BOOKING_CREATED_DATE },
|
||||
];
|
||||
|
||||
const checkboxFieldValueOptions = [
|
||||
{ label: t("true"), value: true },
|
||||
{ label: t("false"), value: false },
|
||||
];
|
||||
|
||||
const [dateFieldValue, setDateValue] = useState(dateFieldValueOptions[0]);
|
||||
const [checkboxFieldValue, setCheckboxFieldValue] = useState(checkboxFieldValueOptions[0]);
|
||||
|
||||
const [newOnBookingWriteToPersonObjectField, setNewOnBookingWriteToPersonObjectField] = useState<
|
||||
z.infer<typeof writeToRecordEntrySchema>
|
||||
>({
|
||||
field: "",
|
||||
fieldType: writeToPersonObjectFieldType.value,
|
||||
value: "",
|
||||
whenToWrite: WhenToWriteToRecord.FIELD_EMPTY,
|
||||
});
|
||||
|
||||
const checkOwnerOptions = [
|
||||
{ label: t("contact"), value: SalesforceRecordEnum.CONTACT },
|
||||
{ label: t("salesforce_lead"), value: SalesforceRecordEnum.LEAD },
|
||||
@@ -306,206 +260,16 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<Switch
|
||||
label={t("salesforce_on_booking_write_to_record", { record: createEventOn })}
|
||||
labelOnLeading
|
||||
checked={onBookingWriteToRecord}
|
||||
onCheckedChange={(checked) => {
|
||||
<WriteToObjectSettings
|
||||
bookingAction={BookingActionEnum.ON_BOOKING}
|
||||
optionLabel={t("salesforce_on_booking_write_to_record", { record: createEventOn })}
|
||||
optionEnabled={onBookingWriteToRecord}
|
||||
writeToObjectData={onBookingWriteToRecordFields}
|
||||
optionSwitchOnChange={(checked) => {
|
||||
setAppData("onBookingWriteToRecord", checked);
|
||||
}}
|
||||
updateWriteToObjectData={(data) => setAppData("onBookingWriteToRecordFields", data)}
|
||||
/>
|
||||
{onBookingWriteToRecord ? (
|
||||
<div className="ml-2 mt-2">
|
||||
<div className="grid grid-cols-5 gap-4">
|
||||
<div>{t("field_name")}</div>
|
||||
<div>{t("field_type")}</div>
|
||||
<div>{t("value")}</div>
|
||||
<div>{t("when_to_write")}</div>
|
||||
</div>
|
||||
<div>
|
||||
{Object.keys(onBookingWriteToRecordFields).map((key) => (
|
||||
<div className="mt-2 grid grid-cols-5 gap-4" key={key}>
|
||||
<div>
|
||||
<InputField value={key} readOnly />
|
||||
</div>
|
||||
<div>
|
||||
<Select
|
||||
value={fieldTypeOptions.find(
|
||||
(option) => option.value === onBookingWriteToRecordFields[key].fieldType
|
||||
)}
|
||||
isDisabled={true}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
{onBookingWriteToRecordFields[key].fieldType === SalesforceFieldType.DATE ? (
|
||||
<Select
|
||||
value={dateFieldValueOptions.find(
|
||||
(option) => option.value === onBookingWriteToRecordFields[key].value
|
||||
)}
|
||||
isDisabled={true}
|
||||
/>
|
||||
) : onBookingWriteToRecordFields[key].fieldType === SalesforceFieldType.CHECKBOX ? (
|
||||
<Select
|
||||
value={checkboxFieldValueOptions.find(
|
||||
(option) => option.value === onBookingWriteToRecordFields[key].value
|
||||
)}
|
||||
isDisabled={true}
|
||||
/>
|
||||
) : (
|
||||
<InputField value={onBookingWriteToRecordFields[key].value as string} readOnly />
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Select
|
||||
value={whenToWriteToRecordOptions.find(
|
||||
(option) => option.value === onBookingWriteToRecordFields[key].whenToWrite
|
||||
)}
|
||||
isDisabled={true}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
StartIcon="trash"
|
||||
variant="icon"
|
||||
color="destructive"
|
||||
onClick={() => {
|
||||
const newObject = onBookingWriteToRecordFields;
|
||||
delete onBookingWriteToRecordFields[key];
|
||||
setAppData("onBookingWriteToRecordFields", newObject);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="mt-2 grid grid-cols-5 gap-4">
|
||||
<div>
|
||||
<InputField
|
||||
value={newOnBookingWriteToPersonObjectField.field}
|
||||
onChange={(e) =>
|
||||
setNewOnBookingWriteToPersonObjectField({
|
||||
...newOnBookingWriteToPersonObjectField,
|
||||
field: e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Select
|
||||
options={fieldTypeOptions}
|
||||
value={writeToPersonObjectFieldType}
|
||||
onChange={(e) => {
|
||||
if (e) {
|
||||
setWriteToPersonObjectFieldType(e);
|
||||
setNewOnBookingWriteToPersonObjectField({
|
||||
...newOnBookingWriteToPersonObjectField,
|
||||
fieldType: e.value,
|
||||
...(e.value === SalesforceFieldType.DATE && { value: dateFieldValue.value }),
|
||||
...(e.value === SalesforceFieldType.CHECKBOX && {
|
||||
value: checkboxFieldValue.value,
|
||||
}),
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
{writeToPersonObjectFieldType.value === SalesforceFieldType.DATE ? (
|
||||
<Select
|
||||
options={dateFieldValueOptions}
|
||||
value={dateFieldValue}
|
||||
onChange={(e) => {
|
||||
if (e) {
|
||||
setDateValue(e);
|
||||
setNewOnBookingWriteToPersonObjectField({
|
||||
...newOnBookingWriteToPersonObjectField,
|
||||
value: e.value,
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : writeToPersonObjectFieldType.value === SalesforceFieldType.CHECKBOX ? (
|
||||
<Select
|
||||
options={checkboxFieldValueOptions}
|
||||
value={checkboxFieldValue}
|
||||
onChange={(e) => {
|
||||
if (e) {
|
||||
setCheckboxFieldValue(e);
|
||||
setNewOnBookingWriteToPersonObjectField({
|
||||
...newOnBookingWriteToPersonObjectField,
|
||||
value: e.value,
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<InputField
|
||||
value={newOnBookingWriteToPersonObjectField.value as string}
|
||||
onChange={(e) =>
|
||||
setNewOnBookingWriteToPersonObjectField({
|
||||
...newOnBookingWriteToPersonObjectField,
|
||||
value: e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Select
|
||||
options={whenToWriteToRecordOptions}
|
||||
value={whenToWriteToPersonRecord}
|
||||
onChange={(e) => {
|
||||
if (e) {
|
||||
setWhenToWriteToPersonRecord(e);
|
||||
setNewOnBookingWriteToPersonObjectField({
|
||||
...newOnBookingWriteToPersonObjectField,
|
||||
whenToWrite: e.value,
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
className="mt-2"
|
||||
size="sm"
|
||||
disabled={
|
||||
!(
|
||||
newOnBookingWriteToPersonObjectField.field &&
|
||||
newOnBookingWriteToPersonObjectField.fieldType &&
|
||||
newOnBookingWriteToPersonObjectField.value !== "" &&
|
||||
newOnBookingWriteToPersonObjectField.whenToWrite
|
||||
)
|
||||
}
|
||||
onClick={() => {
|
||||
if (
|
||||
Object.keys(onBookingWriteToRecordFields).includes(
|
||||
newOnBookingWriteToEventObjectField.field.trim()
|
||||
)
|
||||
) {
|
||||
showToast("Field already exists", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
setAppData("onBookingWriteToRecordFields", {
|
||||
...onBookingWriteToRecordFields,
|
||||
[newOnBookingWriteToPersonObjectField.field.trim()]: {
|
||||
fieldType: newOnBookingWriteToPersonObjectField.fieldType,
|
||||
value: newOnBookingWriteToPersonObjectField.value,
|
||||
whenToWrite: newOnBookingWriteToPersonObjectField.whenToWrite,
|
||||
},
|
||||
});
|
||||
setNewOnBookingWriteToPersonObjectField({
|
||||
field: "",
|
||||
fieldType: writeToPersonObjectFieldType.value,
|
||||
value: "",
|
||||
whenToWrite: WhenToWriteToRecord.FIELD_EMPTY,
|
||||
});
|
||||
}}>
|
||||
{t("add_new_field")}
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
@@ -588,6 +352,19 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="mt-4">
|
||||
<WriteToObjectSettings
|
||||
bookingAction={BookingActionEnum.ON_CANCEL}
|
||||
optionLabel={t("salesforce_on_cancel_write_to_event")}
|
||||
optionEnabled={onCancelWriteToEventRecord}
|
||||
writeToObjectData={onCancelWriteToEventRecordFields}
|
||||
optionSwitchOnChange={(checked) => {
|
||||
setAppData("onCancelWriteToEventRecord", checked);
|
||||
}}
|
||||
updateWriteToObjectData={(data) => setAppData("onCancelWriteToEventRecordFields", data)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="ml-2 mt-4">
|
||||
<Switch
|
||||
label="Send no show attendee data to event object"
|
||||
|
||||
@@ -0,0 +1,283 @@
|
||||
import { useState } from "react";
|
||||
import type z from "zod";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { Switch } from "@calcom/ui/components/form";
|
||||
import { InputField } from "@calcom/ui/components/form";
|
||||
import { Select } from "@calcom/ui/components/form";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
|
||||
import { WhenToWriteToRecord, SalesforceFieldType, DateFieldTypeData } from "../../lib/enums";
|
||||
import type { writeToRecordEntrySchema, writeToBookingEntry } from "../../zod";
|
||||
|
||||
export enum BookingActionEnum {
|
||||
ON_BOOKING = "on_booking",
|
||||
ON_CANCEL = "on_cancel",
|
||||
}
|
||||
|
||||
type iWriteToObjectData = Record<string, z.infer<typeof writeToBookingEntry>>;
|
||||
|
||||
const WriteToObjectSettings = ({
|
||||
bookingAction,
|
||||
optionLabel,
|
||||
optionEnabled,
|
||||
optionSwitchOnChange,
|
||||
writeToObjectData,
|
||||
updateWriteToObjectData,
|
||||
}: {
|
||||
bookingAction: BookingActionEnum;
|
||||
optionLabel: string;
|
||||
optionEnabled: boolean;
|
||||
optionSwitchOnChange: (checked: boolean) => void;
|
||||
writeToObjectData: iWriteToObjectData;
|
||||
updateWriteToObjectData: (data: iWriteToObjectData) => void;
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
|
||||
const fieldTypeOptions = [
|
||||
{ label: t("text"), value: SalesforceFieldType.TEXT },
|
||||
{ label: t("date"), value: SalesforceFieldType.DATE },
|
||||
{ label: t("phone").charAt(0).toUpperCase() + t("phone").slice(1), value: SalesforceFieldType.PHONE },
|
||||
{ label: t("checkbox"), value: SalesforceFieldType.CHECKBOX },
|
||||
{ label: t("picklist"), value: SalesforceFieldType.PICKLIST },
|
||||
{ label: t("custom"), value: SalesforceFieldType.CUSTOM },
|
||||
];
|
||||
|
||||
const whenToWriteToRecordOptions = [
|
||||
{ label: t("only_if_field_is_empty"), value: WhenToWriteToRecord.FIELD_EMPTY },
|
||||
...(bookingAction === BookingActionEnum.ON_CANCEL
|
||||
? [{ label: t("salesforce_on_every_cancellation"), value: WhenToWriteToRecord.EVERY_BOOKING }]
|
||||
: [{ label: t("on_every_booking"), value: WhenToWriteToRecord.EVERY_BOOKING }]),
|
||||
];
|
||||
|
||||
const checkboxFieldValueOptions = [
|
||||
{ label: t("true"), value: true },
|
||||
{ label: t("false"), value: false },
|
||||
];
|
||||
|
||||
const dateFieldValueOptions = [
|
||||
...(bookingAction === BookingActionEnum.ON_CANCEL
|
||||
? [{ label: t("booking_cancel_date"), value: DateFieldTypeData.BOOKING_CANCEL_DATE }]
|
||||
: []),
|
||||
{ label: t("booking_start_date"), value: DateFieldTypeData.BOOKING_START_DATE },
|
||||
{ label: t("booking_created_date"), value: DateFieldTypeData.BOOKING_CREATED_DATE },
|
||||
];
|
||||
|
||||
const [fieldTypeSelectedOption, setFieldTypeSelectedOption] = useState(fieldTypeOptions[0]);
|
||||
const [dateFieldSelectedOption, setDateFieldSelectedOption] = useState(dateFieldValueOptions[0]);
|
||||
const [checkboxFieldSelectedOption, setCheckboxFieldSelectedOption] = useState(
|
||||
checkboxFieldValueOptions[0]
|
||||
);
|
||||
const [whenToWriteSelectedOption, setWhenToWriteSelectedOption] = useState(whenToWriteToRecordOptions[0]);
|
||||
const [newOnWriteToRecordEntry, setNewOnWriteToRecordEntry] = useState<
|
||||
z.infer<typeof writeToRecordEntrySchema>
|
||||
>({
|
||||
field: "",
|
||||
fieldType: fieldTypeSelectedOption.value,
|
||||
value: "",
|
||||
whenToWrite: whenToWriteSelectedOption.value,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Switch
|
||||
label={optionLabel}
|
||||
labelOnLeading
|
||||
checked={optionEnabled}
|
||||
onCheckedChange={optionSwitchOnChange}
|
||||
/>
|
||||
{optionEnabled ? (
|
||||
<div className="ml-2 mt-2">
|
||||
<div className="grid grid-cols-5 gap-4">
|
||||
<div>{t("field_name")}</div>
|
||||
<div>{t("field_type")}</div>
|
||||
<div>{t("value")}</div>
|
||||
<div>{t("when_to_write")}</div>
|
||||
</div>
|
||||
<div>
|
||||
{Object.keys(writeToObjectData).map((key) => (
|
||||
<div className="mt-2 grid grid-cols-5 gap-4" key={key}>
|
||||
<div>
|
||||
<InputField value={key} readOnly />
|
||||
</div>
|
||||
<div>
|
||||
<Select
|
||||
value={fieldTypeOptions.find(
|
||||
(option) => option.value === writeToObjectData[key].fieldType
|
||||
)}
|
||||
isDisabled={true}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
{writeToObjectData[key].fieldType === SalesforceFieldType.DATE ? (
|
||||
<Select
|
||||
value={dateFieldValueOptions.find(
|
||||
(option) => option.value === writeToObjectData[key].value
|
||||
)}
|
||||
isDisabled={true}
|
||||
/>
|
||||
) : writeToObjectData[key].fieldType === SalesforceFieldType.CHECKBOX ? (
|
||||
<Select
|
||||
value={checkboxFieldValueOptions.find(
|
||||
(option) => option.value === writeToObjectData[key].value
|
||||
)}
|
||||
isDisabled={true}
|
||||
/>
|
||||
) : (
|
||||
<InputField value={writeToObjectData[key].value as string} readOnly />
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Select
|
||||
value={whenToWriteToRecordOptions.find(
|
||||
(option) => option.value === writeToObjectData[key].whenToWrite
|
||||
)}
|
||||
isDisabled={true}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
StartIcon="trash"
|
||||
variant="icon"
|
||||
color="destructive"
|
||||
onClick={() => {
|
||||
const newObject = writeToObjectData;
|
||||
delete writeToObjectData[key];
|
||||
updateWriteToObjectData(newObject);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="mt-2 grid grid-cols-5 gap-4">
|
||||
<div>
|
||||
<InputField
|
||||
value={newOnWriteToRecordEntry.field}
|
||||
onChange={(e) =>
|
||||
setNewOnWriteToRecordEntry({
|
||||
...newOnWriteToRecordEntry,
|
||||
field: e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Select
|
||||
options={fieldTypeOptions}
|
||||
value={fieldTypeSelectedOption}
|
||||
onChange={(e) => {
|
||||
if (e) {
|
||||
setFieldTypeSelectedOption(e);
|
||||
setNewOnWriteToRecordEntry({
|
||||
...newOnWriteToRecordEntry,
|
||||
fieldType: e.value,
|
||||
...(e.value === SalesforceFieldType.DATE && { value: dateFieldSelectedOption.value }),
|
||||
...(e.value === SalesforceFieldType.CHECKBOX && {
|
||||
value: checkboxFieldSelectedOption.value,
|
||||
}),
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
{newOnWriteToRecordEntry.fieldType === SalesforceFieldType.DATE ? (
|
||||
<Select
|
||||
options={dateFieldValueOptions}
|
||||
value={dateFieldSelectedOption}
|
||||
onChange={(e) => {
|
||||
if (e) {
|
||||
setDateFieldSelectedOption(e);
|
||||
setNewOnWriteToRecordEntry({
|
||||
...newOnWriteToRecordEntry,
|
||||
value: e.value,
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : newOnWriteToRecordEntry.fieldType === SalesforceFieldType.CHECKBOX ? (
|
||||
<Select
|
||||
options={checkboxFieldValueOptions}
|
||||
value={checkboxFieldSelectedOption}
|
||||
onChange={(e) => {
|
||||
if (e) {
|
||||
setCheckboxFieldSelectedOption(e);
|
||||
setNewOnWriteToRecordEntry({
|
||||
...newOnWriteToRecordEntry,
|
||||
value: e.value,
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<InputField
|
||||
value={newOnWriteToRecordEntry.value as string}
|
||||
onChange={(e) =>
|
||||
setNewOnWriteToRecordEntry({
|
||||
...newOnWriteToRecordEntry,
|
||||
value: e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Select
|
||||
options={whenToWriteToRecordOptions}
|
||||
value={whenToWriteSelectedOption}
|
||||
onChange={(e) => {
|
||||
if (e) {
|
||||
setWhenToWriteSelectedOption(e);
|
||||
setNewOnWriteToRecordEntry({
|
||||
...newOnWriteToRecordEntry,
|
||||
whenToWrite: e.value,
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
className="mt-2"
|
||||
size="sm"
|
||||
disabled={
|
||||
!(
|
||||
newOnWriteToRecordEntry.field &&
|
||||
newOnWriteToRecordEntry.fieldType &&
|
||||
newOnWriteToRecordEntry.value !== "" &&
|
||||
newOnWriteToRecordEntry.whenToWrite
|
||||
)
|
||||
}
|
||||
onClick={() => {
|
||||
if (Object.keys(writeToObjectData).includes(newOnWriteToRecordEntry.field.trim())) {
|
||||
showToast("Field already exists", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
updateWriteToObjectData({
|
||||
...writeToObjectData,
|
||||
[newOnWriteToRecordEntry.field.trim()]: {
|
||||
fieldType: newOnWriteToRecordEntry.fieldType,
|
||||
value: newOnWriteToRecordEntry.value,
|
||||
whenToWrite: newOnWriteToRecordEntry.whenToWrite,
|
||||
},
|
||||
});
|
||||
|
||||
setNewOnWriteToRecordEntry({
|
||||
field: "",
|
||||
fieldType: fieldTypeOptions[0].value,
|
||||
value: "",
|
||||
whenToWrite: whenToWriteToRecordOptions[0].value,
|
||||
});
|
||||
}}>
|
||||
{t("add_new_field")}
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default WriteToObjectSettings;
|
||||
@@ -18,7 +18,7 @@ import type { CRM, Contact, CrmEvent } from "@calcom/types/CrmService";
|
||||
import type { ParseRefreshTokenResponse } from "../../_utils/oauth/parseRefreshTokenResponse";
|
||||
import parseRefreshTokenResponse from "../../_utils/oauth/parseRefreshTokenResponse";
|
||||
import { default as appMeta } from "../config.json";
|
||||
import type { writeToRecordDataSchema } from "../zod";
|
||||
import type { writeToRecordDataSchema, appDataSchema, writeToBookingEntry } from "../zod";
|
||||
import {
|
||||
SalesforceRecordEnum,
|
||||
SalesforceFieldType,
|
||||
@@ -75,11 +75,11 @@ export default class SalesforceCRMService implements CRM {
|
||||
private conn!: Promise<Connection>;
|
||||
private log: typeof logger;
|
||||
private calWarnings: string[] = [];
|
||||
private appOptions: any;
|
||||
private appOptions: z.infer<typeof appDataSchema>;
|
||||
private doNotCreateEvent = false;
|
||||
private fallbackToContact = false;
|
||||
|
||||
constructor(credential: CredentialPayload, appOptions: any, testMode = false) {
|
||||
constructor(credential: CredentialPayload, appOptions: z.infer<typeof appDataSchema>, testMode = false) {
|
||||
this.integrationName = "salesforce_other_calendar";
|
||||
if (!testMode) {
|
||||
this.conn = this.getClient(credential).then((c) => c);
|
||||
@@ -255,7 +255,7 @@ export default class SalesforceCRMService implements CRM {
|
||||
});
|
||||
});
|
||||
// Check to see if we also need to change the record owner
|
||||
if (appOptions.onBookingChangeRecordOwner && appOptions.onBookingChangeRecordOwnerName) {
|
||||
if (appOptions?.onBookingChangeRecordOwner && appOptions?.onBookingChangeRecordOwnerName) {
|
||||
if (ownerId) {
|
||||
// TODO: firstContact id is assumed to not be undefined. But current code doesn't check for it.
|
||||
await this.checkRecordOwnerNameFromRecordId(firstContact.id, ownerId);
|
||||
@@ -265,15 +265,16 @@ export default class SalesforceCRMService implements CRM {
|
||||
);
|
||||
}
|
||||
}
|
||||
if (appOptions.onBookingWriteToRecord && appOptions.onBookingWriteToRecordFields) {
|
||||
await this.writeToPersonRecord(
|
||||
if (appOptions?.onBookingWriteToRecord && appOptions?.onBookingWriteToRecordFields) {
|
||||
await this.writeToRecord({
|
||||
// TODO: firstContact id is assumed to not be undefined. But current code doesn't check for it.
|
||||
firstContact.id,
|
||||
event.startTime,
|
||||
event.organizer?.email,
|
||||
event.responses,
|
||||
event?.uid
|
||||
);
|
||||
recordId: firstContact.id,
|
||||
fieldsToWriteTo: appOptions.onBookingWriteToRecordFields,
|
||||
startTime: event.startTime,
|
||||
organizerEmail: event.organizer?.email,
|
||||
calEventResponses: event.responses,
|
||||
bookingUid: event?.uid,
|
||||
});
|
||||
}
|
||||
return createdEvent;
|
||||
};
|
||||
@@ -294,8 +295,27 @@ export default class SalesforceCRMService implements CRM {
|
||||
});
|
||||
};
|
||||
|
||||
private salesforceDeleteEvent = async (uid: string) => {
|
||||
private salesforceDeleteEvent = async (uid: string, event: CalendarEvent) => {
|
||||
const appOptions = this.getAppOptions();
|
||||
const conn = await this.conn;
|
||||
|
||||
if (appOptions?.onCancelWriteToEventRecord) {
|
||||
const fieldsToWriteTo = appOptions?.onCancelWriteToEventRecordFields;
|
||||
|
||||
// If the option is enabled then don't delete the event record
|
||||
if (!fieldsToWriteTo || !Object.keys(fieldsToWriteTo)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return await this.writeToRecord({
|
||||
recordId: uid,
|
||||
fieldsToWriteTo,
|
||||
startTime: event.startTime,
|
||||
organizerEmail: event.organizer?.email,
|
||||
calEventResponses: event.responses,
|
||||
bookingUid: event?.uid,
|
||||
});
|
||||
}
|
||||
return await conn.sobject("Event").delete(uid);
|
||||
};
|
||||
|
||||
@@ -353,13 +373,15 @@ export default class SalesforceCRMService implements CRM {
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteEvent(uid: string) {
|
||||
const deletedEvent = await this.salesforceDeleteEvent(uid);
|
||||
if (deletedEvent.success) {
|
||||
Promise.resolve();
|
||||
} else {
|
||||
Promise.reject({ calError: "Something went wrong when deleting the event in Salesforce" });
|
||||
}
|
||||
public async deleteEvent(uid: string, event: CalendarEvent) {
|
||||
await this.salesforceDeleteEvent(uid, event)
|
||||
.then(() => {
|
||||
return Promise.resolve();
|
||||
})
|
||||
.catch((error) => {
|
||||
this.log.error(`Error canceling event ${uid} with error ${error}`);
|
||||
return Promise.reject({ calError: "Something went wrong when deleting the event in Salesforce" });
|
||||
});
|
||||
}
|
||||
|
||||
async getContacts({
|
||||
@@ -724,7 +746,7 @@ export default class SalesforceCRMService implements CRM {
|
||||
// Update the event with the no show data
|
||||
await conn.sobject("Event").update({
|
||||
Id: event.uid,
|
||||
[sendNoShowAttendeeDataField]: noShowData.noShow,
|
||||
[sendNoShowAttendeeDataField as string]: noShowData.noShow,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1045,84 +1067,91 @@ export default class SalesforceCRMService implements CRM {
|
||||
return this.fallbackToContact;
|
||||
}
|
||||
|
||||
private async writeToPersonRecord(
|
||||
contactId: string,
|
||||
startTime: string,
|
||||
organizerEmail?: string,
|
||||
calEventResponses?: CalEventResponses | null,
|
||||
bookingUid?: string | null
|
||||
) {
|
||||
private async writeToRecord({
|
||||
recordId,
|
||||
startTime,
|
||||
fieldsToWriteTo,
|
||||
organizerEmail,
|
||||
calEventResponses,
|
||||
bookingUid,
|
||||
}: {
|
||||
recordId: string;
|
||||
startTime: string;
|
||||
fieldsToWriteTo: Record<string, z.infer<typeof writeToBookingEntry>>;
|
||||
organizerEmail?: string;
|
||||
calEventResponses?: CalEventResponses | null;
|
||||
bookingUid?: string | null;
|
||||
}) {
|
||||
const conn = await this.conn;
|
||||
const { onBookingWriteToRecordFields = {} } = this.getAppOptions();
|
||||
// Determine record type (Contact or Lead)
|
||||
const personRecordType = this.determineRecordTypeById(contactId);
|
||||
// Determine record type (Contact, Lead, etc)
|
||||
const personRecordType = this.determineRecordTypeById(recordId);
|
||||
// Search the fields and ensure 1. they exist 2. they're the right type
|
||||
const fieldsToWriteOn = Object.keys(onBookingWriteToRecordFields);
|
||||
const fieldsToWriteOn = Object.keys(fieldsToWriteTo);
|
||||
const existingFields = await this.ensureFieldsExistOnObject(fieldsToWriteOn, personRecordType);
|
||||
if (!existingFields.length) {
|
||||
this.log.warn(`No fields found for record type ${personRecordType}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const personRecord = await this.fetchPersonRecord(contactId, existingFields, personRecordType);
|
||||
const personRecord = await this.fetchPersonRecord(recordId, existingFields, personRecordType);
|
||||
if (!personRecord) {
|
||||
this.log.warn(`No personRecord found for contactId ${contactId}`);
|
||||
this.log.warn(`No personRecord found for contactId ${recordId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.log.info(`Writing to recordId ${contactId} on fields ${fieldsToWriteOn}`);
|
||||
this.log.info(`Writing to recordId ${recordId} on fields ${fieldsToWriteOn}`);
|
||||
|
||||
const writeOnRecordBody = await this.buildRecordUpdatePayload({
|
||||
existingFields,
|
||||
personRecord,
|
||||
onBookingWriteToRecordFields,
|
||||
fieldsToWriteTo,
|
||||
startTime,
|
||||
bookingUid,
|
||||
organizerEmail,
|
||||
calEventResponses,
|
||||
contactId,
|
||||
recordId,
|
||||
});
|
||||
|
||||
this.log.info(
|
||||
`Final writeOnRecordBody contains fields ${Object.keys(writeOnRecordBody)} for record ${contactId}`
|
||||
`Final writeOnRecordBody contains fields ${Object.keys(writeOnRecordBody)} for record ${recordId}`
|
||||
);
|
||||
|
||||
// Update the person record
|
||||
await conn
|
||||
.sobject(personRecordType)
|
||||
.update({
|
||||
Id: contactId,
|
||||
Id: recordId,
|
||||
...writeOnRecordBody,
|
||||
})
|
||||
.catch((e) => {
|
||||
this.log.error(`Error updating person record for contactId ${contactId}`, e);
|
||||
this.log.error(`Error updating person record for contactId ${recordId}`, e);
|
||||
});
|
||||
}
|
||||
|
||||
private async buildRecordUpdatePayload({
|
||||
existingFields,
|
||||
personRecord,
|
||||
onBookingWriteToRecordFields,
|
||||
fieldsToWriteTo,
|
||||
startTime,
|
||||
bookingUid,
|
||||
organizerEmail,
|
||||
calEventResponses,
|
||||
contactId,
|
||||
recordId,
|
||||
}: {
|
||||
existingFields: Field[];
|
||||
personRecord: Record<string, any>;
|
||||
onBookingWriteToRecordFields: Record<string, any>;
|
||||
fieldsToWriteTo: Record<string, any>;
|
||||
startTime?: string;
|
||||
bookingUid?: string | null;
|
||||
organizerEmail?: string;
|
||||
calEventResponses?: CalEventResponses | null;
|
||||
contactId: string;
|
||||
recordId: string;
|
||||
}): Promise<Record<string, any>> {
|
||||
const log = logger.getSubLogger({ prefix: [`[buildRecordUpdatePayload] ${contactId}`] });
|
||||
const log = logger.getSubLogger({ prefix: [`[buildRecordUpdatePayload] ${recordId}`] });
|
||||
const writeOnRecordBody: Record<string, any> = {};
|
||||
|
||||
for (const field of existingFields) {
|
||||
const fieldConfig = onBookingWriteToRecordFields[field.name];
|
||||
const fieldConfig = fieldsToWriteTo[field.name];
|
||||
|
||||
if (!fieldConfig) {
|
||||
log.error(`No field config found for field ${field.name}`);
|
||||
@@ -1193,7 +1222,7 @@ export default class SalesforceCRMService implements CRM {
|
||||
salesforceField: field,
|
||||
calEventResponses,
|
||||
bookingUid,
|
||||
contactId,
|
||||
recordId,
|
||||
});
|
||||
if (picklistValue) {
|
||||
writeOnRecordBody[field.name] = picklistValue;
|
||||
@@ -1223,8 +1252,10 @@ export default class SalesforceCRMService implements CRM {
|
||||
|
||||
if (!customFieldInputsEnabled) return {};
|
||||
|
||||
if (!appOptions?.onBookingWriteToEventObjectMap) return {};
|
||||
|
||||
const customFieldInputs = customFieldInputsEnabled
|
||||
? await this.ensureFieldsExistOnObject(Object.keys(appOptions?.onBookingWriteToEventObjectMap), "Event")
|
||||
? await this.ensureFieldsExistOnObject(Object.keys(appOptions.onBookingWriteToEventObjectMap), "Event")
|
||||
: [];
|
||||
|
||||
const confirmedCustomFieldInputs: {
|
||||
@@ -1342,6 +1373,10 @@ export default class SalesforceCRMService implements CRM {
|
||||
this.log.warn(`No uid for booking with organizer ${organizerEmail}`);
|
||||
}
|
||||
|
||||
if (fieldValue === DateFieldTypeData.BOOKING_CANCEL_DATE) {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1350,15 +1385,15 @@ export default class SalesforceCRMService implements CRM {
|
||||
salesforceField,
|
||||
calEventResponses,
|
||||
bookingUid,
|
||||
contactId,
|
||||
recordId,
|
||||
}: {
|
||||
fieldConfigValue: string;
|
||||
salesforceField: Field;
|
||||
calEventResponses?: CalEventResponses | null;
|
||||
bookingUid?: string | null;
|
||||
contactId: string;
|
||||
recordId: string;
|
||||
}) {
|
||||
const log = logger.getSubLogger({ prefix: [`[getPicklistFieldValue] ${contactId}`] });
|
||||
const log = logger.getSubLogger({ prefix: [`[getPicklistFieldValue] ${recordId}`] });
|
||||
|
||||
const picklistOptions = salesforceField.picklistValues;
|
||||
if (!picklistOptions || !picklistOptions.length) {
|
||||
@@ -1498,12 +1533,12 @@ export default class SalesforceCRMService implements CRM {
|
||||
if (!(companyFieldName in onBookingWriteToRecordFields)) return;
|
||||
|
||||
const companyValue = await this.getTextFieldValue({
|
||||
fieldValue: onBookingWriteToRecordFields[companyFieldName].value,
|
||||
fieldValue: onBookingWriteToRecordFields[companyFieldName].value as string,
|
||||
fieldLength: defaultTextValueLength,
|
||||
calEventResponses,
|
||||
});
|
||||
|
||||
if (companyValue === onBookingWriteToRecordFields[companyFieldName]) return;
|
||||
if (companyValue && companyValue === onBookingWriteToRecordFields[companyFieldName].value) return;
|
||||
return companyValue;
|
||||
}
|
||||
|
||||
@@ -1538,8 +1573,8 @@ export default class SalesforceCRMService implements CRM {
|
||||
const writeOnRecordBody = await this.buildRecordUpdatePayload({
|
||||
existingFields,
|
||||
personRecord,
|
||||
onBookingWriteToRecordFields: writeToRecordObject,
|
||||
contactId: personRecord.Id,
|
||||
fieldsToWriteTo: writeToRecordObject,
|
||||
recordId: personRecord.Id,
|
||||
});
|
||||
await conn
|
||||
.sobject(recordType)
|
||||
@@ -1568,6 +1603,8 @@ export default class SalesforceCRMService implements CRM {
|
||||
return SalesforceRecordEnum.ACCOUNT;
|
||||
case "00Q":
|
||||
return SalesforceRecordEnum.LEAD;
|
||||
case "00U":
|
||||
return SalesforceRecordEnum.EVENT;
|
||||
default:
|
||||
this.log.warn(`Unhandled record id type ${id}`);
|
||||
return SalesforceRecordEnum.CONTACT;
|
||||
|
||||
@@ -2,6 +2,7 @@ export enum SalesforceRecordEnum {
|
||||
CONTACT = "Contact",
|
||||
LEAD = "Lead",
|
||||
ACCOUNT = "Account",
|
||||
EVENT = "Event",
|
||||
}
|
||||
|
||||
export enum WhenToWriteToRecord {
|
||||
@@ -23,6 +24,7 @@ export enum SalesforceFieldType {
|
||||
export enum DateFieldTypeData {
|
||||
BOOKING_START_DATE = "booking_start_date",
|
||||
BOOKING_CREATED_DATE = "booking_created_date",
|
||||
BOOKING_CANCEL_DATE = "booking_cancel_date",
|
||||
}
|
||||
|
||||
export enum RoutingReasons {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { z } from "zod";
|
||||
import { eventTypeAppCardZod } from "../eventTypeAppCardZod";
|
||||
import { SalesforceRecordEnum, WhenToWriteToRecord, SalesforceFieldType } from "./lib/enums";
|
||||
|
||||
const writeToBookingEntry = z.object({
|
||||
export const writeToBookingEntry = z.object({
|
||||
value: z.union([z.string(), z.boolean()]),
|
||||
fieldType: z.nativeEnum(SalesforceFieldType),
|
||||
whenToWrite: z.nativeEnum(WhenToWriteToRecord),
|
||||
@@ -56,6 +56,8 @@ export const appDataSchema = eventTypeAppCardZod.extend({
|
||||
onBookingWriteToRecord: z.boolean().optional(),
|
||||
onBookingWriteToRecordFields: z.record(z.string(), writeToBookingEntry).optional(),
|
||||
ignoreGuests: z.boolean().optional(),
|
||||
onCancelWriteToEventRecord: z.boolean().optional(),
|
||||
onCancelWriteToEventRecordFields: z.record(z.string(), writeToBookingEntry).optional(),
|
||||
});
|
||||
|
||||
export const appKeysSchema = z.object({
|
||||
|
||||
@@ -27,7 +27,11 @@ import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { WebhookTriggerEvents } from "@calcom/prisma/enums";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
import { bookingMetadataSchema, EventTypeMetaDataSchema, bookingCancelInput } from "@calcom/prisma/zod-utils";
|
||||
import {
|
||||
bookingMetadataSchema,
|
||||
eventTypeMetaDataSchemaWithTypedApps,
|
||||
bookingCancelInput,
|
||||
} from "@calcom/prisma/zod-utils";
|
||||
import type { EventTypeMetadata } from "@calcom/prisma/zod-utils";
|
||||
import { getAllWorkflowsFromEventType } from "@calcom/trpc/server/routers/viewer/workflows/util";
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
@@ -444,7 +448,7 @@ async function handler(input: CancelBookingInput) {
|
||||
allRemainingBookings
|
||||
);
|
||||
|
||||
const bookingToDeleteEventTypeMetadata = EventTypeMetaDataSchema.parse(
|
||||
const bookingToDeleteEventTypeMetadata = eventTypeMetaDataSchemaWithTypedApps.parse(
|
||||
bookingToDelete.eventType?.metadata || null
|
||||
);
|
||||
|
||||
@@ -453,7 +457,10 @@ async function handler(input: CancelBookingInput) {
|
||||
metadata: bookingToDeleteEventTypeMetadata,
|
||||
});
|
||||
|
||||
const eventManager = new EventManager({ ...bookingToDelete.user, credentials });
|
||||
const eventManager = new EventManager(
|
||||
{ ...bookingToDelete.user, credentials },
|
||||
bookingToDeleteEventTypeMetadata?.apps
|
||||
);
|
||||
|
||||
await eventManager.cancelEvent(evt, bookingToDelete.references, isBookingInRecurringSeries);
|
||||
|
||||
|
||||
@@ -616,7 +616,7 @@ export default class EventManager {
|
||||
|
||||
if (reference.type.includes("_crm") || reference.type.includes("other_calendar")) {
|
||||
crmReferences.push(reference);
|
||||
allPromises.push(this.deleteCRMEvent({ reference }));
|
||||
allPromises.push(this.deleteCRMEvent({ reference, event }));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1100,11 +1100,12 @@ export default class EventManager {
|
||||
return updatedEvents;
|
||||
}
|
||||
|
||||
private async deleteCRMEvent({ reference }: { reference: PartialReference }) {
|
||||
private async deleteCRMEvent({ reference, event }: { reference: PartialReference; event: CalendarEvent }) {
|
||||
const credential = this.crmCredentials.find((cred) => cred.id === reference.credentialId);
|
||||
if (credential) {
|
||||
const crm = new CrmManager(credential);
|
||||
await crm.deleteEvent(reference.uid);
|
||||
const currentAppOption = this.getAppOptionsFromEventMetadata(credential);
|
||||
const crm = new CrmManager(credential, currentAppOption);
|
||||
await crm.deleteEvent(reference.uid, event);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -57,9 +57,9 @@ export default class CrmManager {
|
||||
return await crmService?.updateEvent(uid, event);
|
||||
}
|
||||
|
||||
public async deleteEvent(uid: string) {
|
||||
public async deleteEvent(uid: string, event: CalendarEvent) {
|
||||
const crmService = await this.getCrmService(this.credential);
|
||||
return await crmService?.deleteEvent(uid);
|
||||
return await crmService?.deleteEvent(uid, event);
|
||||
}
|
||||
|
||||
public async getContacts(params: {
|
||||
|
||||
Vendored
+1
-1
@@ -33,7 +33,7 @@ export interface CrmEvent {
|
||||
export interface CRM {
|
||||
createEvent: (event: CalendarEvent, contacts: Contact[]) => Promise<CrmEvent | undefined>;
|
||||
updateEvent: (uid: string, event: CalendarEvent) => Promise<CrmEvent>;
|
||||
deleteEvent: (uid: string) => Promise<void>;
|
||||
deleteEvent: (uid: string, event: CalendarEvent) => Promise<void>;
|
||||
getContacts: ({
|
||||
emails,
|
||||
includeOwner,
|
||||
|
||||
Reference in New Issue
Block a user