400 workflows webhooks trigger (#11041)

https://github.com/user-attachments/assets/dc0ece22-4d87-417f-b9e1-a11c3fd52ce8
This commit is contained in:
martmull
2025-03-20 11:12:52 +01:00
committed by GitHub
parent bc94891a27
commit d99f027e8d
30 changed files with 399 additions and 49 deletions

View File

@ -23,6 +23,7 @@ import {
workflowTriggerSchema,
workflowUpdateRecordActionSchema,
workflowUpdateRecordActionSettingsSchema,
workflowWebhookTriggerSchema,
} from '@/workflow/validation-schemas/workflowSchema';
import { z } from 'zod';
@ -76,6 +77,9 @@ export type WorkflowDatabaseEventTrigger = z.infer<
>;
export type WorkflowManualTrigger = z.infer<typeof workflowManualTriggerSchema>;
export type WorkflowCronTrigger = z.infer<typeof workflowCronTriggerSchema>;
export type WorkflowWebhookTrigger = z.infer<
typeof workflowWebhookTriggerSchema
>;
export type WorkflowManualTriggerSettings = WorkflowManualTrigger['settings'];
export type WorkflowManualTriggerAvailability =

View File

@ -203,11 +203,19 @@ export const workflowCronTriggerSchema = baseTriggerSchema.extend({
]),
});
export const workflowWebhookTriggerSchema = baseTriggerSchema.extend({
type: z.literal('WEBHOOK'),
settings: z.object({
outputSchema: z.object({}).passthrough(),
}),
});
// Combined trigger schema
export const workflowTriggerSchema = z.discriminatedUnion('type', [
workflowDatabaseEventTriggerSchema,
workflowManualTriggerSchema,
workflowCronTriggerSchema,
workflowWebhookTriggerSchema,
]);
// Step output schemas

View File

@ -27,27 +27,10 @@ export const WorkflowDiagramStepNodeIcon = ({
switch (data.nodeType) {
case 'trigger': {
switch (data.triggerType) {
case 'DATABASE_EVENT': {
return (
<StyledStepNodeLabelIconContainer>
<Icon
size={theme.icon.size.md}
color={theme.font.color.tertiary}
/>
</StyledStepNodeLabelIconContainer>
);
}
case 'MANUAL': {
return (
<StyledStepNodeLabelIconContainer>
<Icon
size={theme.icon.size.md}
color={theme.font.color.tertiary}
/>
</StyledStepNodeLabelIconContainer>
);
}
case 'CRON': {
case 'DATABASE_EVENT':
case 'MANUAL':
case 'CRON':
case 'WEBHOOK': {
return (
<StyledStepNodeLabelIconContainer>
<Icon

View File

@ -33,6 +33,14 @@ export const getWorkflowDiagramTriggerNode = ({
break;
}
case 'WEBHOOK': {
triggerDefaultLabel = 'Webhook';
triggerIcon = getTriggerIcon({
type: 'WEBHOOK',
});
break;
}
case 'DATABASE_EVENT': {
const triggerEvent = splitWorkflowTriggerEventName(
trigger.settings.eventName,

View File

@ -13,6 +13,7 @@ import { WorkflowEditTriggerManualForm } from '@/workflow/workflow-trigger/compo
import { Suspense, lazy } from 'react';
import { isDefined } from 'twenty-shared';
import { RightDrawerSkeletonLoader } from '~/loading/components/RightDrawerSkeletonLoader';
import { WorkflowEditTriggerWebhookForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerWebhookForm';
const WorkflowEditActionServerlessFunction = lazy(() =>
import(
@ -85,6 +86,15 @@ export const WorkflowStepDetail = ({
/>
);
}
case 'WEBHOOK': {
return (
<WorkflowEditTriggerWebhookForm
key={stepId}
trigger={stepDefinition.definition}
triggerOptions={props}
/>
);
}
case 'CRON': {
return (
<WorkflowEditTriggerCronForm

View File

@ -0,0 +1,99 @@
import { WorkflowWebhookTrigger } from '@/workflow/types/Workflow';
import { useTheme } from '@emotion/react';
import { useIcons, IconCopy } from 'twenty-ui';
import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon';
import { isDefined } from 'twenty-shared';
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
import { TextInputV2 } from '@/ui/input/components/TextInputV2';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useDebouncedCallback } from 'use-debounce';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useLingui } from '@lingui/react/macro';
import { useRecoilValue } from 'recoil';
import { workflowIdState } from '@/workflow/states/workflowIdState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { REACT_APP_SERVER_BASE_URL } from '~/config';
type WorkflowEditTriggerWebhookFormProps = {
trigger: WorkflowWebhookTrigger;
triggerOptions:
| {
readonly: true;
onTriggerUpdate?: undefined;
}
| {
readonly?: false;
onTriggerUpdate: (trigger: WorkflowWebhookTrigger) => void;
};
};
export const WorkflowEditTriggerWebhookForm = ({
trigger,
triggerOptions,
}: WorkflowEditTriggerWebhookFormProps) => {
const { enqueueSnackBar } = useSnackBar();
const theme = useTheme();
const { t } = useLingui();
const { getIcon } = useIcons();
const workflowId = useRecoilValue(workflowIdState);
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const headerTitle = isDefined(trigger.name) ? trigger.name : 'Webhook';
const headerIcon = getTriggerIcon({
type: 'WEBHOOK',
});
const webhookUrl = `${REACT_APP_SERVER_BASE_URL}/webhooks/workflows/${currentWorkspace?.id}/${workflowId}`;
const displayWebhookUrl = webhookUrl.replace(/^(https?:\/\/)?(www\.)?/, '');
const copyToClipboard = async () => {
await navigator.clipboard.writeText(webhookUrl);
enqueueSnackBar(t`Copied to clipboard!`, {
variant: SnackBarVariant.Success,
icon: <IconCopy size={theme.icon.size.md} />,
});
};
const copyToClipboardDebounced = useDebouncedCallback(copyToClipboard, 200);
if (!isDefined(currentWorkspace)) {
return <></>;
}
return (
<>
<WorkflowStepHeader
onTitleChange={(newName: string) => {
if (triggerOptions.readonly === true) {
return;
}
triggerOptions.onTriggerUpdate({
...trigger,
name: newName,
});
}}
Icon={getIcon(headerIcon)}
iconColor={theme.font.color.tertiary}
initialTitle={headerTitle}
headerType="Trigger · Webhook"
disabled={triggerOptions.readonly}
/>
<WorkflowStepBody>
<TextInputV2
label="Live URL"
value={displayWebhookUrl}
RightIcon={() => (
<IconCopy
size={theme.icon.size.md}
color={theme.font.color.secondary}
/>
)}
onRightIconClick={copyToClipboardDebounced}
readOnly
/>
</WorkflowStepBody>
</>
);
};

View File

@ -15,4 +15,9 @@ export const OTHER_TRIGGER_TYPES: Array<{
type: 'CRON',
icon: 'IconClock',
},
{
defaultLabel: 'Webhook',
type: 'WEBHOOK',
icon: 'IconWebhook',
},
];

View File

@ -58,6 +58,15 @@ export const getTriggerDefaultDefinition = ({
},
};
}
case 'WEBHOOK': {
return {
type,
name: defaultLabel,
settings: {
outputSchema: {},
},
};
}
default: {
return assertUnreachable(type, `Unknown type: ${type}`);
}

View File

@ -9,6 +9,9 @@ export const getTriggerIcon = (
| {
type: 'CRON';
}
| {
type: 'WEBHOOK';
}
| {
type: 'DATABASE_EVENT';
eventName: string;

View File

@ -9,6 +9,9 @@ export const getTriggerDefaultLabel = (
| {
type: 'CRON';
}
| {
type: 'WEBHOOK';
}
| {
type: 'DATABASE_EVENT';
eventName: string;

View File

@ -12,6 +12,8 @@ export const getTriggerStepName = (trigger: WorkflowTrigger): string => {
return getDatabaseEventTriggerStepName(trigger);
case 'CRON':
return 'On a Schedule';
case 'WEBHOOK':
return 'Webhook';
case 'MANUAL':
if (!isDefined(trigger.settings.objectType)) {
return 'Manual trigger';