400 workflows webhooks trigger (#11041)
https://github.com/user-attachments/assets/dc0ece22-4d87-417f-b9e1-a11c3fd52ce8
This commit is contained in:
@ -14,10 +14,11 @@ export const useTestWorkflowSingleRecordAction: ActionHookWithoutObjectMetadataI
|
||||
|
||||
const shouldBeRegistered =
|
||||
isDefined(workflowWithCurrentVersion?.currentVersion?.trigger) &&
|
||||
workflowWithCurrentVersion.currentVersion.trigger.type === 'MANUAL' &&
|
||||
!isDefined(
|
||||
workflowWithCurrentVersion.currentVersion.trigger.settings.objectType,
|
||||
);
|
||||
((workflowWithCurrentVersion.currentVersion.trigger.type === 'MANUAL' &&
|
||||
!isDefined(
|
||||
workflowWithCurrentVersion.currentVersion.trigger.settings.objectType,
|
||||
)) ||
|
||||
workflowWithCurrentVersion.currentVersion.trigger.type === 'WEBHOOK');
|
||||
|
||||
const onClick = () => {
|
||||
if (!shouldBeRegistered) {
|
||||
|
||||
@ -7,6 +7,7 @@ import {
|
||||
IconRobot,
|
||||
IconSettingsAutomation,
|
||||
IconUserCircle,
|
||||
IconWebhook,
|
||||
} from 'twenty-ui';
|
||||
|
||||
export const getActorSourceMultiSelectOptions = (
|
||||
@ -53,6 +54,13 @@ export const getActorSourceMultiSelectOptions = (
|
||||
AvatarIcon: IconSettingsAutomation,
|
||||
isIconInverted: true,
|
||||
},
|
||||
{
|
||||
id: 'WEBHOOK',
|
||||
name: 'Webhook',
|
||||
isSelected: selectedSourceNames.includes('WEBHOOK'),
|
||||
AvatarIcon: IconWebhook,
|
||||
isIconInverted: true,
|
||||
},
|
||||
{
|
||||
id: 'SYSTEM',
|
||||
name: 'System',
|
||||
|
||||
@ -286,15 +286,18 @@ const FieldActorSourceSchema = z.union([
|
||||
z.literal('MANUAL'),
|
||||
z.literal('SYSTEM'),
|
||||
z.literal('WORKFLOW'),
|
||||
z.literal('WEBHOOK'),
|
||||
]);
|
||||
|
||||
export const FieldActorValueSchema = z.object({
|
||||
source: FieldActorSourceSchema,
|
||||
workspaceMemberId: z.string().nullable(),
|
||||
name: z.string(),
|
||||
context: z.object({
|
||||
provider: z.nativeEnum(ConnectedAccountProvider).optional(),
|
||||
}),
|
||||
context: z
|
||||
.object({
|
||||
provider: z.nativeEnum(ConnectedAccountProvider).optional(),
|
||||
})
|
||||
.nullable(),
|
||||
});
|
||||
export type FieldActorValue = z.infer<typeof FieldActorValueSchema>;
|
||||
|
||||
|
||||
@ -14,6 +14,7 @@ import {
|
||||
IconMicrosoftOutlook,
|
||||
IconRobot,
|
||||
IconSettingsAutomation,
|
||||
IconWebhook,
|
||||
} from 'twenty-ui';
|
||||
|
||||
type ActorDisplayProps = Partial<FieldActorValue> & {
|
||||
@ -54,6 +55,8 @@ export const ActorDisplay = ({
|
||||
return IconRobot;
|
||||
case 'WORKFLOW':
|
||||
return IconSettingsAutomation;
|
||||
case 'WEBHOOK':
|
||||
return IconWebhook;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@ -65,6 +65,7 @@ const StyledInput = styled.input<
|
||||
Pick<
|
||||
TextInputV2ComponentProps,
|
||||
| 'LeftIcon'
|
||||
| 'RightIcon'
|
||||
| 'error'
|
||||
| 'sizeVariant'
|
||||
| 'width'
|
||||
@ -112,9 +113,16 @@ const StyledInput = styled.input<
|
||||
: LeftIcon
|
||||
? `calc(${theme.spacing(3)} + 16px)`
|
||||
: theme.spacing(2)};
|
||||
padding-right: ${({ theme, RightIcon, autoGrow }) =>
|
||||
autoGrow
|
||||
? theme.spacing(1)
|
||||
: RightIcon
|
||||
? `calc(${theme.spacing(3)} + 16px)`
|
||||
: theme.spacing(2)};
|
||||
width: ${({ theme, width }) =>
|
||||
width ? `calc(${width}px + ${theme.spacing(0.5)})` : '100%'};
|
||||
max-width: ${({ autoGrow }) => (autoGrow ? '100%' : 'none')};
|
||||
text-overflow: ellipsis;
|
||||
&::placeholder,
|
||||
&::-webkit-input-placeholder {
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
@ -126,6 +134,10 @@ const StyledInput = styled.input<
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
}
|
||||
|
||||
&[readonly] {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
${({ theme, error }) => {
|
||||
return `
|
||||
@ -165,7 +177,10 @@ const StyledTrailingIconContainer = styled.div<
|
||||
margin: auto 0;
|
||||
`;
|
||||
|
||||
const StyledTrailingIcon = styled.div<{ isFocused?: boolean }>`
|
||||
const StyledTrailingIcon = styled.div<{
|
||||
isFocused?: boolean;
|
||||
onClick?: () => void;
|
||||
}>`
|
||||
align-items: center;
|
||||
color: ${({ theme, isFocused }) =>
|
||||
isFocused ? theme.font.color.secondary : theme.font.color.light};
|
||||
@ -189,6 +204,7 @@ export type TextInputV2ComponentProps = Omit<
|
||||
error?: string;
|
||||
noErrorHelper?: boolean;
|
||||
RightIcon?: IconComponent;
|
||||
onRightIconClick?: () => void;
|
||||
LeftIcon?: IconComponent;
|
||||
autoGrow?: boolean;
|
||||
onKeyDown?: (event: React.KeyboardEvent<HTMLInputElement>) => void;
|
||||
@ -224,8 +240,10 @@ const TextInputV2Component = forwardRef<
|
||||
autoFocus,
|
||||
placeholder,
|
||||
disabled,
|
||||
readOnly,
|
||||
tabIndex,
|
||||
RightIcon,
|
||||
onRightIconClick,
|
||||
LeftIcon,
|
||||
autoComplete,
|
||||
maxLength,
|
||||
@ -302,10 +320,12 @@ const TextInputV2Component = forwardRef<
|
||||
{...{
|
||||
autoFocus,
|
||||
disabled,
|
||||
readOnly,
|
||||
placeholder,
|
||||
required,
|
||||
value,
|
||||
LeftIcon,
|
||||
RightIcon,
|
||||
maxLength,
|
||||
error,
|
||||
sizeVariant,
|
||||
@ -337,7 +357,9 @@ const TextInputV2Component = forwardRef<
|
||||
</StyledTrailingIcon>
|
||||
)}
|
||||
{!error && type !== INPUT_TYPE_PASSWORD && !!RightIcon && (
|
||||
<StyledTrailingIcon>
|
||||
<StyledTrailingIcon
|
||||
onClick={onRightIconClick ? onRightIconClick : undefined}
|
||||
>
|
||||
<RightIcon size={theme.icon.size.md} />
|
||||
</StyledTrailingIcon>
|
||||
)}
|
||||
|
||||
@ -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 =
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -15,4 +15,9 @@ export const OTHER_TRIGGER_TYPES: Array<{
|
||||
type: 'CRON',
|
||||
icon: 'IconClock',
|
||||
},
|
||||
{
|
||||
defaultLabel: 'Webhook',
|
||||
type: 'WEBHOOK',
|
||||
icon: 'IconWebhook',
|
||||
},
|
||||
];
|
||||
|
||||
@ -58,6 +58,15 @@ export const getTriggerDefaultDefinition = ({
|
||||
},
|
||||
};
|
||||
}
|
||||
case 'WEBHOOK': {
|
||||
return {
|
||||
type,
|
||||
name: defaultLabel,
|
||||
settings: {
|
||||
outputSchema: {},
|
||||
},
|
||||
};
|
||||
}
|
||||
default: {
|
||||
return assertUnreachable(type, `Unknown type: ${type}`);
|
||||
}
|
||||
|
||||
@ -9,6 +9,9 @@ export const getTriggerIcon = (
|
||||
| {
|
||||
type: 'CRON';
|
||||
}
|
||||
| {
|
||||
type: 'WEBHOOK';
|
||||
}
|
||||
| {
|
||||
type: 'DATABASE_EVENT';
|
||||
eventName: string;
|
||||
|
||||
@ -9,6 +9,9 @@ export const getTriggerDefaultLabel = (
|
||||
| {
|
||||
type: 'CRON';
|
||||
}
|
||||
| {
|
||||
type: 'WEBHOOK';
|
||||
}
|
||||
| {
|
||||
type: 'DATABASE_EVENT';
|
||||
eventName: string;
|
||||
|
||||
@ -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';
|
||||
|
||||
Reference in New Issue
Block a user