feat: create calendar events on Google and Microsoft accounts (#22231)
## Context Twenty can import calendar events and send emails, but cannot create calendar events. This adds calendar event creation on connected **Google** and **Microsoft** accounts, mirroring the existing email-send architecture (`message-outbound-manager`). ## What it adds The capability is exposed three ways, all backed by the same composer → driver → persist pipeline: - **GraphQL mutation** `createCalendarEvent` (metadata API) - **AI agent tool** `create_calendar_event` (flows to MCP automatically), gated by a new `CREATE_CALENDAR_EVENT_TOOL` permission flag - **Workflow builder node** "Create Calendar Event" in the **Core** section, with a full settings form (variable interpolation supported) CalDAV/IMAP is intentionally out of scope for now (different long pole). ## Design notes - **Reuse over reinvention** — the created event is run through the existing inbound formatters (`formatGoogleCalendarEvents` / `formatMicrosoftCalendarEvents`) and persisted immediately via the existing `CalendarSaveEventsService`, so it appears in Twenty right away and is reconciled by the next provider sync (dedup on external id). Persistence is best-effort. - **OAuth scopes** — Google already requests `calendar.events` (read+write), so no change there. Microsoft moves `Calendars.Read` → `Calendars.ReadWrite`; existing Microsoft accounts must re-consent (surfaced as a clear "reconnect" error via a missing-scope check). - **Deliberate invitation semantics** — `sendInvitations` is off by default. When off, the event is created with **no attendees** on either provider, so creating an event never silently emails external people. When on, attendees are attached and notified (Google `sendUpdates: all`, Microsoft's default). This sidesteps Microsoft Graph having no per-request suppression. - **Timezone correctness** — Microsoft Graph interprets `dateTime` as wall-clock in the supplied `timeZone` and ignores the offset, so the absolute instant is converted to its wall-clock form before sending (Google honors the offset directly). Both providers end up scheduling the same instant. - **Conferencing** — optional Google Meet (`conferenceData.createRequest`, with a follow-up `events.get` to resolve the async link) / Microsoft Teams (`isOnlineMeeting`). - Attendees are a comma-separated string everywhere (tool input, GraphQL DTO, workflow input), consistent with `send_email` recipients; the composer parses to its internal list. ## Test plan - **Unit**: 45 tests covering the composer (validation, all-day boundaries, offset enforcement, timezone, scope checks, default-account resolution), both provider drivers, the dispatcher, and the workflow step-log builder. - **Integration**: `createCalendarEvent` on the `/metadata` API fails closed with a structured error for a non-existent account (the auth/ownership/validation path that doesn't require provider mocking). - **Manual**: verified the workflow node appears in the Core section, the settings form renders and round-trips (edit → autosave → reload), and the live mutation returns a structured failure for a bogus account. ## Open question for reviewers The metadata mutation `createCalendarEvent` shares a name with the core schema's auto-generated `createCalendarEvent(data:)` CRUD mutation for the CalendarEvent object — they live on different endpoints (`/metadata` vs `/graphql`) so there's no runtime conflict, but it's a potential point of confusion for API consumers. Happy to rename (e.g. `createCalendarEventOnConnectedAccount`) if preferred. ## Out of scope / follow-ups - CalDAV/IMAP support - Event update/delete and recurrence - Existing Microsoft accounts need re-consent for the widened scope <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22231?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: neo773 <neo773@protonmail.com>
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
export type CalendarEventFormData = {
|
||||
connectedAccountId: string;
|
||||
title: string;
|
||||
description: string;
|
||||
location: string;
|
||||
startsAt: string;
|
||||
endsAt: string;
|
||||
isFullDay: boolean;
|
||||
timeZone: string;
|
||||
attendees: string;
|
||||
sendInvitations: boolean;
|
||||
addConferencing: boolean;
|
||||
};
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
type SingleRecordAvailability,
|
||||
type workflowAiAgentActionSchema,
|
||||
type workflowCodeActionSchema,
|
||||
type workflowCreateCalendarEventActionSchema,
|
||||
type workflowCreateRecordActionSchema,
|
||||
type workflowCronTriggerSchema,
|
||||
type workflowDatabaseEventTriggerSchema,
|
||||
@@ -42,6 +43,9 @@ export type WorkflowSendEmailAction = z.infer<
|
||||
export type WorkflowDraftEmailAction = z.infer<
|
||||
typeof workflowDraftEmailActionSchema
|
||||
>;
|
||||
export type WorkflowCreateCalendarEventAction = z.infer<
|
||||
typeof workflowCreateCalendarEventActionSchema
|
||||
>;
|
||||
export type WorkflowCreateRecordAction = z.infer<
|
||||
typeof workflowCreateRecordActionSchema
|
||||
>;
|
||||
@@ -78,6 +82,7 @@ export type WorkflowAction =
|
||||
| WorkflowLogicFunctionAction
|
||||
| WorkflowSendEmailAction
|
||||
| WorkflowDraftEmailAction
|
||||
| WorkflowCreateCalendarEventAction
|
||||
| WorkflowCreateRecordAction
|
||||
| WorkflowUpdateRecordAction
|
||||
| WorkflowDeleteRecordAction
|
||||
|
||||
+2
-1
@@ -35,7 +35,8 @@ export const WorkflowDiagramStepNodeIcon = ({
|
||||
case 'CODE':
|
||||
case 'HTTP_REQUEST':
|
||||
case 'SEND_EMAIL':
|
||||
case 'DRAFT_EMAIL': {
|
||||
case 'DRAFT_EMAIL':
|
||||
case 'CREATE_CALENDAR_EVENT': {
|
||||
return (
|
||||
<Icon
|
||||
size={theme.icon.size.md}
|
||||
|
||||
+12
@@ -6,6 +6,7 @@ import {
|
||||
import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrThrow';
|
||||
import { WorkflowEditActionAiAgent } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowEditActionAiAgent';
|
||||
import { WorkflowActionCode } from '@/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowActionCode';
|
||||
import { WorkflowEditActionCreateCalendarEvent } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionCreateCalendarEvent';
|
||||
import { WorkflowEditActionCreateRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionCreateRecord';
|
||||
import { WorkflowEditActionDeleteRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionDeleteRecord';
|
||||
import { WorkflowEditActionEmpty } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionEmpty';
|
||||
@@ -140,6 +141,17 @@ export const WorkflowRunStepNodeDetail = ({
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'CREATE_CALENDAR_EVENT': {
|
||||
return (
|
||||
<WorkflowEditActionCreateCalendarEvent
|
||||
key={stepId}
|
||||
action={stepDefinition.definition}
|
||||
actionOptions={{
|
||||
readonly: true,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'CREATE_RECORD': {
|
||||
return (
|
||||
<WorkflowEditActionCreateRecord
|
||||
|
||||
+10
@@ -5,6 +5,7 @@ import {
|
||||
import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrThrow';
|
||||
import { WorkflowEditActionAiAgent } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowEditActionAiAgent';
|
||||
import { WorkflowActionCode } from '@/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowActionCode';
|
||||
import { WorkflowEditActionCreateCalendarEvent } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionCreateCalendarEvent';
|
||||
import { WorkflowEditActionCreateRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionCreateRecord';
|
||||
import { WorkflowEditActionDeleteRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionDeleteRecord';
|
||||
import { WorkflowEditActionEmpty } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionEmpty';
|
||||
@@ -136,6 +137,15 @@ export const WorkflowStepDetail = ({
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'CREATE_CALENDAR_EVENT': {
|
||||
return (
|
||||
<WorkflowEditActionCreateCalendarEvent
|
||||
key={stepId}
|
||||
action={stepDefinition.definition}
|
||||
actionOptions={props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'CREATE_RECORD': {
|
||||
return (
|
||||
<WorkflowEditActionCreateRecord
|
||||
|
||||
+228
@@ -0,0 +1,228 @@
|
||||
import { getMissingCreateCalendarEventScopes } from '@/accounts/utils/hasMissingCreateCalendarEventScopes';
|
||||
import { FormBooleanFieldToggleInput } from '@/object-record/record-field/ui/form-types/components/FormBooleanFieldToggleInput';
|
||||
import { FormDateTimeFieldInput } from '@/object-record/record-field/ui/form-types/components/FormDateTimeFieldInput';
|
||||
import { FormMultiTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormMultiTextFieldInput';
|
||||
import { FormSelectFieldInput } from '@/object-record/record-field/ui/form-types/components/FormSelectFieldInput';
|
||||
import { FormTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormTextFieldInput';
|
||||
import { AVAILABLE_TIMEZONE_OPTIONS } from '@/settings/experience/constants/AvailableTimezoneOptions';
|
||||
import { useMyConnectedAccounts } from '@/settings/accounts/hooks/useMyConnectedAccounts';
|
||||
import { useTriggerApisOAuth } from '@/settings/accounts/hooks/useTriggerApiOAuth';
|
||||
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState';
|
||||
import { type WorkflowCreateCalendarEventAction } from '@/workflow/types/Workflow';
|
||||
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||
import { WorkflowStepFooter } from '@/workflow/workflow-steps/components/WorkflowStepFooter';
|
||||
import { useCalendarEventForm } from '@/workflow/workflow-steps/workflow-actions/hooks/useCalendarEventForm';
|
||||
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useEffect } from 'react';
|
||||
import { ConnectedAccountProvider, SettingsPath } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Callout } from 'twenty-ui/feedback';
|
||||
import { type SelectOption } from 'twenty-ui/input';
|
||||
import { IconPlus } from 'twenty-ui/icon';
|
||||
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
||||
|
||||
const CALENDAR_CAPABLE_PROVIDERS = [
|
||||
ConnectedAccountProvider.GOOGLE,
|
||||
ConnectedAccountProvider.MICROSOFT,
|
||||
];
|
||||
|
||||
type WorkflowEditActionCreateCalendarEventProps = {
|
||||
action: WorkflowCreateCalendarEventAction;
|
||||
actionOptions:
|
||||
| {
|
||||
readonly: true;
|
||||
}
|
||||
| {
|
||||
readonly?: false;
|
||||
onActionUpdate: (action: WorkflowCreateCalendarEventAction) => void;
|
||||
};
|
||||
};
|
||||
|
||||
export const WorkflowEditActionCreateCalendarEvent = ({
|
||||
action,
|
||||
actionOptions,
|
||||
}: WorkflowEditActionCreateCalendarEventProps) => {
|
||||
const { formData, handleFieldChange, saveAction } = useCalendarEventForm({
|
||||
action,
|
||||
onActionUpdate:
|
||||
actionOptions.readonly === true
|
||||
? undefined
|
||||
: actionOptions.onActionUpdate,
|
||||
readonly: actionOptions.readonly === true,
|
||||
});
|
||||
|
||||
const navigate = useNavigateSettings();
|
||||
const { closeSidePanelMenu } = useSidePanelMenu();
|
||||
const { accounts: myAccounts, loading } = useMyConnectedAccounts();
|
||||
const { triggerApisOAuth } = useTriggerApisOAuth();
|
||||
|
||||
const workflowVisualizerWorkflowId = useAtomComponentStateValue(
|
||||
workflowVisualizerWorkflowIdComponentState,
|
||||
);
|
||||
const redirectUrl = `/object/workflow/${workflowVisualizerWorkflowId}`;
|
||||
|
||||
const connectedAccountOptions: SelectOption<string>[] = myAccounts
|
||||
.filter((account) => {
|
||||
if (account.provider === ConnectedAccountProvider.IMAP_SMTP_CALDAV) {
|
||||
return isDefined(account.connectionParameters?.CALDAV);
|
||||
}
|
||||
|
||||
return CALENDAR_CAPABLE_PROVIDERS.includes(account.provider);
|
||||
})
|
||||
.map((account) => ({ label: account.handle, value: account.id }));
|
||||
|
||||
const selectedAccount = myAccounts.find(
|
||||
(account) => account.id === formData.connectedAccountId,
|
||||
);
|
||||
|
||||
const missingScopes =
|
||||
isDefined(selectedAccount) &&
|
||||
getMissingCreateCalendarEventScopes(selectedAccount).length > 0
|
||||
? {
|
||||
provider: selectedAccount.provider,
|
||||
loginHint: selectedAccount.handle,
|
||||
}
|
||||
: null;
|
||||
|
||||
const handleReauthorize = async () => {
|
||||
if (!isDefined(missingScopes)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await triggerApisOAuth(missingScopes.provider, {
|
||||
redirectLocation: redirectUrl,
|
||||
loginHint: missingScopes.loginHint,
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
saveAction.flush();
|
||||
};
|
||||
}, [saveAction]);
|
||||
|
||||
if (loading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<WorkflowStepBody>
|
||||
<FormSelectFieldInput
|
||||
key={`connected-account-${formData.connectedAccountId || 'none'}`}
|
||||
label={t`Account`}
|
||||
hint={t`Google, Microsoft or CalDAV account to create the event on. Leave empty to use the default calendar account.`}
|
||||
defaultValue={formData.connectedAccountId}
|
||||
options={connectedAccountOptions}
|
||||
onChange={(value) =>
|
||||
handleFieldChange('connectedAccountId', value ?? '')
|
||||
}
|
||||
readonly={actionOptions.readonly}
|
||||
callToActionButton={{
|
||||
onClick: () => {
|
||||
closeSidePanelMenu();
|
||||
navigate(SettingsPath.NewAccount);
|
||||
},
|
||||
Icon: IconPlus,
|
||||
text: t`Add account`,
|
||||
}}
|
||||
/>
|
||||
{isDefined(missingScopes) && (
|
||||
<Callout
|
||||
variant={'error'}
|
||||
title={t`Missing calendar permission.`}
|
||||
description={t`This account is connected, but we don't have permission to create calendar events on your behalf yet. You'll be redirected to approve this access.`}
|
||||
action={{
|
||||
label: t`Reauthorize`,
|
||||
onClick: handleReauthorize,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<FormTextFieldInput
|
||||
label={t`Title`}
|
||||
placeholder={t`Enter event title`}
|
||||
readonly={actionOptions.readonly}
|
||||
defaultValue={formData.title}
|
||||
onChange={(value) => handleFieldChange('title', value)}
|
||||
VariablePicker={WorkflowVariablePicker}
|
||||
/>
|
||||
<FormTextFieldInput
|
||||
label={t`Description`}
|
||||
placeholder={t`Enter event description`}
|
||||
multiline
|
||||
readonly={actionOptions.readonly}
|
||||
defaultValue={formData.description}
|
||||
onChange={(value) => handleFieldChange('description', value)}
|
||||
VariablePicker={WorkflowVariablePicker}
|
||||
/>
|
||||
<FormTextFieldInput
|
||||
label={t`Location`}
|
||||
placeholder={t`Enter event location`}
|
||||
readonly={actionOptions.readonly}
|
||||
defaultValue={formData.location}
|
||||
onChange={(value) => handleFieldChange('location', value)}
|
||||
VariablePicker={WorkflowVariablePicker}
|
||||
/>
|
||||
<FormDateTimeFieldInput
|
||||
label={t`Starts at`}
|
||||
placeholder={t`Select a date and time`}
|
||||
readonly={actionOptions.readonly}
|
||||
defaultValue={formData.startsAt}
|
||||
onChange={(value) => handleFieldChange('startsAt', value ?? '')}
|
||||
VariablePicker={WorkflowVariablePicker}
|
||||
/>
|
||||
<FormDateTimeFieldInput
|
||||
label={t`Ends at`}
|
||||
placeholder={t`Select a date and time`}
|
||||
readonly={actionOptions.readonly}
|
||||
defaultValue={formData.endsAt}
|
||||
onChange={(value) => handleFieldChange('endsAt', value ?? '')}
|
||||
VariablePicker={WorkflowVariablePicker}
|
||||
/>
|
||||
<FormSelectFieldInput
|
||||
label={t`Time zone`}
|
||||
defaultValue={formData.timeZone}
|
||||
options={AVAILABLE_TIMEZONE_OPTIONS as SelectOption<string>[]}
|
||||
onChange={(value) => handleFieldChange('timeZone', value ?? '')}
|
||||
readonly={actionOptions.readonly}
|
||||
VariablePicker={WorkflowVariablePicker}
|
||||
/>
|
||||
<FormBooleanFieldToggleInput
|
||||
label={t`All day`}
|
||||
description={t`Create the event as an all-day event`}
|
||||
value={formData.isFullDay}
|
||||
onChange={(value) => handleFieldChange('isFullDay', value)}
|
||||
disabled={actionOptions.readonly}
|
||||
/>
|
||||
<FormMultiTextFieldInput
|
||||
label={t`Attendees`}
|
||||
placeholder={t`Enter emails, comma-separated`}
|
||||
readonly={actionOptions.readonly}
|
||||
defaultValue={formData.attendees}
|
||||
onChange={(value) => handleFieldChange('attendees', value)}
|
||||
VariablePicker={WorkflowVariablePicker}
|
||||
/>
|
||||
<FormBooleanFieldToggleInput
|
||||
label={t`Send invitations`}
|
||||
description={t`Email the attendees an invitation`}
|
||||
hint={t`When off, the event is created with no attendees and nobody is notified.`}
|
||||
value={formData.sendInvitations}
|
||||
onChange={(value) => handleFieldChange('sendInvitations', value)}
|
||||
disabled={actionOptions.readonly}
|
||||
/>
|
||||
<FormBooleanFieldToggleInput
|
||||
label={t`Add conferencing`}
|
||||
description={t`Add a video conferencing link`}
|
||||
hint={t`Generates a Google Meet or Microsoft Teams link depending on the account.`}
|
||||
value={formData.addConferencing}
|
||||
onChange={(value) => handleFieldChange('addConferencing', value)}
|
||||
disabled={actionOptions.readonly}
|
||||
/>
|
||||
</WorkflowStepBody>
|
||||
{!actionOptions.readonly && <WorkflowStepFooter stepId={action.id} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+13
-2
@@ -1,5 +1,6 @@
|
||||
import { type WorkflowActionType } from '@/workflow/types/Workflow';
|
||||
import { CODE_ACTION } from '@/workflow/workflow-steps/workflow-actions/constants/actions/CodeAction';
|
||||
import { CREATE_CALENDAR_EVENT_ACTION } from '@/workflow/workflow-steps/workflow-actions/constants/actions/CreateCalendarEventAction';
|
||||
import { DRAFT_EMAIL_ACTION } from '@/workflow/workflow-steps/workflow-actions/constants/actions/DraftEmailAction';
|
||||
import { HTTP_REQUEST_ACTION } from '@/workflow/workflow-steps/workflow-actions/constants/actions/HttpRequestAction';
|
||||
import { SEND_EMAIL_ACTION } from '@/workflow/workflow-steps/workflow-actions/constants/actions/SendEmailAction';
|
||||
@@ -8,7 +9,17 @@ export const CORE_ACTIONS: Array<{
|
||||
defaultLabel: string;
|
||||
type: Extract<
|
||||
WorkflowActionType,
|
||||
'CODE' | 'SEND_EMAIL' | 'DRAFT_EMAIL' | 'HTTP_REQUEST'
|
||||
| 'CODE'
|
||||
| 'SEND_EMAIL'
|
||||
| 'DRAFT_EMAIL'
|
||||
| 'HTTP_REQUEST'
|
||||
| 'CREATE_CALENDAR_EVENT'
|
||||
>;
|
||||
icon: string;
|
||||
}> = [SEND_EMAIL_ACTION, DRAFT_EMAIL_ACTION, CODE_ACTION, HTTP_REQUEST_ACTION];
|
||||
}> = [
|
||||
SEND_EMAIL_ACTION,
|
||||
DRAFT_EMAIL_ACTION,
|
||||
CREATE_CALENDAR_EVENT_ACTION,
|
||||
CODE_ACTION,
|
||||
HTTP_REQUEST_ACTION,
|
||||
];
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import { type WorkflowActionType } from '@/workflow/types/Workflow';
|
||||
|
||||
export const CREATE_CALENDAR_EVENT_ACTION: {
|
||||
defaultLabel: string;
|
||||
type: Extract<WorkflowActionType, 'CREATE_CALENDAR_EVENT'>;
|
||||
icon: string;
|
||||
} = {
|
||||
defaultLabel: 'Create Calendar Event',
|
||||
type: 'CREATE_CALENDAR_EVENT',
|
||||
icon: 'IconCalendarEvent',
|
||||
};
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
import { type CalendarEventFormData } from '@/workflow/types/CalendarEventFormData';
|
||||
import { type WorkflowCreateCalendarEventAction } from '@/workflow/types/Workflow';
|
||||
import { useState } from 'react';
|
||||
import { type JsonValue } from 'type-fest';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
|
||||
type UseCalendarEventFormParams = {
|
||||
action: WorkflowCreateCalendarEventAction;
|
||||
onActionUpdate?: (action: WorkflowCreateCalendarEventAction) => void;
|
||||
readonly: boolean;
|
||||
};
|
||||
|
||||
export const useCalendarEventForm = ({
|
||||
action,
|
||||
onActionUpdate,
|
||||
readonly,
|
||||
}: UseCalendarEventFormParams) => {
|
||||
const [formData, setFormData] = useState<CalendarEventFormData>(() => {
|
||||
const input = action.settings.input;
|
||||
|
||||
return {
|
||||
connectedAccountId: input.connectedAccountId,
|
||||
title: input.title,
|
||||
description: input.description ?? '',
|
||||
location: input.location ?? '',
|
||||
startsAt: input.startsAt,
|
||||
endsAt: input.endsAt,
|
||||
isFullDay: input.isFullDay,
|
||||
timeZone: input.timeZone ?? '',
|
||||
attendees: input.attendees ?? '',
|
||||
sendInvitations: input.sendInvitations,
|
||||
addConferencing: input.addConferencing,
|
||||
};
|
||||
});
|
||||
|
||||
const saveAction = useDebouncedCallback(
|
||||
(nextFormData: CalendarEventFormData) => {
|
||||
if (readonly) {
|
||||
return;
|
||||
}
|
||||
|
||||
onActionUpdate?.({
|
||||
...action,
|
||||
settings: {
|
||||
...action.settings,
|
||||
input: { ...nextFormData },
|
||||
},
|
||||
});
|
||||
},
|
||||
1_000,
|
||||
);
|
||||
|
||||
const handleFieldChange = (
|
||||
fieldName: keyof CalendarEventFormData,
|
||||
updatedValue: JsonValue,
|
||||
) => {
|
||||
const newFormData: CalendarEventFormData = {
|
||||
...formData,
|
||||
[fieldName]: updatedValue,
|
||||
};
|
||||
|
||||
setFormData(newFormData);
|
||||
saveAction(newFormData);
|
||||
};
|
||||
|
||||
return {
|
||||
formData,
|
||||
handleFieldChange,
|
||||
saveAction,
|
||||
};
|
||||
};
|
||||
+1
@@ -20,6 +20,7 @@ export const getActionIcon = (actionType: WorkflowActionType) => {
|
||||
case 'HTTP_REQUEST':
|
||||
case 'SEND_EMAIL':
|
||||
case 'DRAFT_EMAIL':
|
||||
case 'CREATE_CALENDAR_EVENT':
|
||||
return CORE_ACTIONS.find((item) => item.type === actionType)?.icon;
|
||||
case 'LOGIC_FUNCTION':
|
||||
return 'IconFunction';
|
||||
|
||||
+1
@@ -11,6 +11,7 @@ export const getActionIconColorOrThrow = (
|
||||
case 'HTTP_REQUEST':
|
||||
case 'SEND_EMAIL':
|
||||
case 'DRAFT_EMAIL':
|
||||
case 'CREATE_CALENDAR_EVENT':
|
||||
return themeCssVariables.color.red;
|
||||
case 'CREATE_RECORD':
|
||||
case 'UPDATE_RECORD':
|
||||
|
||||
+29
@@ -230,6 +230,35 @@ export const computeStepOutputSchema = ({
|
||||
};
|
||||
}
|
||||
|
||||
case 'CREATE_CALENDAR_EVENT': {
|
||||
return {
|
||||
success: {
|
||||
isLeaf: true,
|
||||
type: FieldMetadataType.BOOLEAN,
|
||||
label: 'Success',
|
||||
value: true,
|
||||
},
|
||||
iCalUid: {
|
||||
isLeaf: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'iCal UID',
|
||||
value: '',
|
||||
},
|
||||
externalEventId: {
|
||||
isLeaf: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'External Event ID',
|
||||
value: '',
|
||||
},
|
||||
conferenceLink: {
|
||||
isLeaf: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Conference Link',
|
||||
value: '',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case 'FILTER':
|
||||
case 'DELAY':
|
||||
case 'EMPTY': {
|
||||
|
||||
Reference in New Issue
Block a user