Update trigger selection design (#9717)

https://github.com/user-attachments/assets/62bc705a-2f69-4ce7-986f-0208154c9965
This commit is contained in:
Thomas Trompette
2025-01-20 10:54:27 +01:00
committed by GitHub
parent 056cb7c66d
commit d50294d39a
13 changed files with 220 additions and 143 deletions

View File

@ -7,9 +7,9 @@ import {
WorkflowDiagramEdge,
WorkflowDiagramNode,
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { DATABASE_TRIGGER_EVENTS } from '@/workflow/workflow-trigger/constants/DatabaseTriggerEvents';
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
import { capitalize } from 'twenty-shared';
import { isDefined } from 'twenty-ui';
import { v4 } from 'uuid';
@ -70,7 +70,10 @@ export const generateWorkflowDiagram = ({
trigger.settings.eventName,
);
triggerLabel = `${capitalize(triggerEvent.objectType)} is ${capitalize(triggerEvent.event)}`;
triggerLabel =
DATABASE_TRIGGER_EVENTS.find(
(event) => event.value === triggerEvent.event,
)?.label ?? '';
break;
}

View File

@ -1,9 +1,9 @@
import { TextInput } from '@/ui/field/input/components/TextInput';
import styled from '@emotion/styled';
import React, { useState } from 'react';
import { useDebouncedCallback } from 'use-debounce';
import { IconComponent } from 'packages/twenty-ui';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconComponent } from 'packages/twenty-ui';
import { useState } from 'react';
import { useDebouncedCallback } from 'use-debounce';
const StyledHeader = styled.div`
background-color: ${({ theme }) => theme.background.secondary};

View File

@ -3,8 +3,9 @@ import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow';
import { workflowSelectedNodeState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeState';
import { DATABASE_TRIGGER_TYPES } from '@/workflow/workflow-trigger/constants/DatabaseTriggerTypes';
import { OTHER_TRIGGER_TYPES } from '@/workflow/workflow-trigger/constants/OtherTriggerTypes';
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
import { TRIGGER_TYPES } from '@/workflow/workflow-trigger/constants/TriggerTypes';
import { useUpdateWorkflowVersionTrigger } from '@/workflow/workflow-trigger/hooks/useUpdateWorkflowVersionTrigger';
import { getTriggerDefaultDefinition } from '@/workflow/workflow-trigger/utils/getTriggerDefaultDefinition';
import styled from '@emotion/styled';
@ -21,6 +22,14 @@ const StyledActionListContainer = styled.div`
padding-inline: ${({ theme }) => theme.spacing(2)};
`;
const StyledSectionTitle = styled.span`
color: ${({ theme }) => theme.font.color.tertiary};
font-size: ${({ theme }) => theme.font.size.sm};
font-weight: ${({ theme }) => theme.font.weight.medium};
padding: ${({ theme }) => theme.spacing(2)} ${({ theme }) => theme.spacing(1)}
${({ theme }) => theme.spacing(1)} ${({ theme }) => theme.spacing(1)};
`;
export const RightDrawerWorkflowSelectTriggerTypeContent = ({
workflow,
}: {
@ -35,14 +44,37 @@ export const RightDrawerWorkflowSelectTriggerTypeContent = ({
return (
<StyledActionListContainer>
{TRIGGER_TYPES.map((action) => (
<StyledSectionTitle>Data</StyledSectionTitle>
{DATABASE_TRIGGER_TYPES.map((action) => (
<MenuItem
key={action.type}
key={action.name}
LeftIcon={action.icon}
text={action.name}
onClick={async () => {
await updateTrigger(
getTriggerDefaultDefinition({
name: action.name,
type: action.type,
activeObjectMetadataItems,
}),
);
setWorkflowSelectedNode(TRIGGER_STEP_ID);
openRightDrawer(RightDrawerPages.WorkflowStepEdit);
}}
/>
))}
<StyledSectionTitle>Others</StyledSectionTitle>
{OTHER_TRIGGER_TYPES.map((action) => (
<MenuItem
key={action.name}
LeftIcon={action.icon}
text={action.name}
onClick={async () => {
await updateTrigger(
getTriggerDefaultDefinition({
name: action.name,
type: action.type,
activeObjectMetadataItems,
}),

View File

@ -4,7 +4,7 @@ import { WorkflowDatabaseEventTrigger } from '@/workflow/types/Workflow';
import { splitWorkflowTriggerEventName } from '@/workflow/utils/splitWorkflowTriggerEventName';
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
import { OBJECT_EVENT_TRIGGERS } from '@/workflow/workflow-trigger/constants/ObjectEventTriggers';
import { DATABASE_TRIGGER_EVENTS } from '@/workflow/workflow-trigger/constants/DatabaseTriggerEvents';
import { useTheme } from '@emotion/react';
import { IconPlaylistAdd, isDefined } from 'twenty-ui';
@ -29,35 +29,30 @@ export const WorkflowEditTriggerDatabaseEventForm = ({
const { activeObjectMetadataItems } = useFilteredObjectMetadataItems();
const triggerEvent = isDefined(trigger)
? splitWorkflowTriggerEventName(trigger.settings.eventName)
: undefined;
const triggerEvent = splitWorkflowTriggerEventName(
trigger.settings.eventName,
);
const availableMetadata: Array<SelectOption<string>> =
activeObjectMetadataItems.map((item) => ({
label: item.labelPlural,
value: item.nameSingular,
}));
const recordTypeMetadata = isDefined(triggerEvent)
? activeObjectMetadataItems.find(
(item) => item.nameSingular === triggerEvent.objectType,
)
: undefined;
const selectedEvent = isDefined(triggerEvent)
? OBJECT_EVENT_TRIGGERS.find(
? DATABASE_TRIGGER_EVENTS.find(
(availableEvent) => availableEvent.value === triggerEvent.event,
)
: undefined;
const headerTitle = isDefined(trigger.name)
? trigger.name
: isDefined(recordTypeMetadata) && isDefined(selectedEvent)
? `When a ${recordTypeMetadata.labelSingular} is ${selectedEvent.label}`
: isDefined(selectedEvent)
? selectedEvent.label
: '-';
const headerType = isDefined(selectedEvent)
? `Trigger · Record is ${selectedEvent.label}`
? `Trigger · ${selectedEvent.label}`
: '-';
return (
@ -92,57 +87,13 @@ export const WorkflowEditTriggerDatabaseEventForm = ({
return;
}
triggerOptions.onTriggerUpdate(
isDefined(trigger) && isDefined(triggerEvent)
? {
...trigger,
settings: {
...trigger.settings,
eventName: `${updatedRecordType}.${triggerEvent.event}`,
},
}
: {
name: headerTitle,
type: 'DATABASE_EVENT',
settings: {
eventName: `${updatedRecordType}.${OBJECT_EVENT_TRIGGERS[0].value}`,
outputSchema: {},
},
},
);
}}
/>
<Select
dropdownId="workflow-edit-trigger-event-type"
label="Event type"
fullWidth
value={triggerEvent?.event}
emptyOption={{ label: 'Select an option', value: '' }}
options={OBJECT_EVENT_TRIGGERS}
disabled={triggerOptions.readonly}
onChange={(updatedEvent) => {
if (triggerOptions.readonly === true) {
return;
}
triggerOptions.onTriggerUpdate(
isDefined(trigger) && isDefined(triggerEvent)
? {
...trigger,
settings: {
...trigger.settings,
eventName: `${triggerEvent.objectType}.${updatedEvent}`,
},
}
: {
name: headerTitle,
type: 'DATABASE_EVENT',
settings: {
eventName: `${availableMetadata?.[0].value}.${updatedEvent}`,
outputSchema: {},
},
},
);
triggerOptions.onTriggerUpdate({
...trigger,
settings: {
...trigger.settings,
eventName: `${updatedRecordType}.${triggerEvent.event}`,
},
});
}}
/>
</WorkflowStepBody>

View File

@ -0,0 +1,18 @@
import { SelectOption } from '@/ui/input/components/Select';
import { DatabaseTriggerName } from '@/workflow/workflow-trigger/constants/DatabaseTriggerName';
export const DATABASE_TRIGGER_EVENTS: Array<SelectOption<string>> = [
{
label: DatabaseTriggerName.RECORD_IS_CREATED,
value: 'created',
},
{
label: DatabaseTriggerName.RECORD_IS_UPDATED,
value: 'updated',
},
{
label: DatabaseTriggerName.RECORD_IS_DELETED,
value: 'deleted',
},
];

View File

@ -0,0 +1,5 @@
export enum DatabaseTriggerName {
RECORD_IS_CREATED = 'Record is Created',
RECORD_IS_UPDATED = 'Record is Updated',
RECORD_IS_DELETED = 'Record is Deleted',
}

View File

@ -0,0 +1,25 @@
import { WorkflowTriggerType } from '@/workflow/types/Workflow';
import { DatabaseTriggerName } from '@/workflow/workflow-trigger/constants/DatabaseTriggerName';
import { IconClick, IconComponent, IconPlus, IconTrash } from 'twenty-ui';
export const DATABASE_TRIGGER_TYPES: Array<{
name: DatabaseTriggerName;
type: WorkflowTriggerType;
icon: IconComponent;
}> = [
{
name: DatabaseTriggerName.RECORD_IS_CREATED,
type: 'DATABASE_EVENT',
icon: IconPlus,
},
{
name: DatabaseTriggerName.RECORD_IS_UPDATED,
type: 'DATABASE_EVENT',
icon: IconClick,
},
{
name: DatabaseTriggerName.RECORD_IS_DELETED,
type: 'DATABASE_EVENT',
icon: IconTrash,
},
];

View File

@ -1,16 +0,0 @@
import { SelectOption } from '@/ui/input/components/Select';
export const OBJECT_EVENT_TRIGGERS: Array<SelectOption<string>> = [
{
label: 'Created',
value: 'created',
},
{
label: 'Updated',
value: 'updated',
},
{
label: 'Deleted',
value: 'deleted',
},
];

View File

@ -1,18 +1,13 @@
import { WorkflowTriggerType } from '@/workflow/types/Workflow';
import { IconComponent, IconSettingsAutomation } from 'twenty-ui';
export const TRIGGER_TYPES: Array<{
export const OTHER_TRIGGER_TYPES: Array<{
name: string;
type: WorkflowTriggerType;
icon: IconComponent;
}> = [
{
name: 'Database Event',
type: 'DATABASE_EVENT',
icon: IconSettingsAutomation,
},
{
name: 'Manual Trigger',
name: 'Launch manually',
type: 'MANUAL',
icon: IconSettingsAutomation,
},

View File

@ -1,50 +1,105 @@
import { DatabaseTriggerName } from '@/workflow/workflow-trigger/constants/DatabaseTriggerName';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
import { getTriggerDefaultDefinition } from '../getTriggerDefaultDefinition';
it('throws if the activeObjectMetadataItems list is empty', () => {
expect(() => {
getTriggerDefaultDefinition({
type: 'DATABASE_EVENT',
activeObjectMetadataItems: [],
});
}).toThrow();
});
it('returns a valid configuration for DATABASE_EVENT trigger type', () => {
expect(
getTriggerDefaultDefinition({
type: 'DATABASE_EVENT',
activeObjectMetadataItems: generatedMockObjectMetadataItems,
}),
).toStrictEqual({
type: 'DATABASE_EVENT',
settings: {
eventName: `${generatedMockObjectMetadataItems[0].nameSingular}.created`,
outputSchema: {},
},
describe('getTriggerDefaultDefinition', () => {
it('throws if the activeObjectMetadataItems list is empty', () => {
expect(() => {
getTriggerDefaultDefinition({
name: DatabaseTriggerName.RECORD_IS_CREATED,
type: 'DATABASE_EVENT',
activeObjectMetadataItems: [],
});
}).toThrow();
});
});
it('returns a valid configuration for MANUAL trigger type', () => {
expect(
getTriggerDefaultDefinition({
it('returns a valid configuration for DATABASE_EVENT trigger type creation', () => {
expect(
getTriggerDefaultDefinition({
name: DatabaseTriggerName.RECORD_IS_CREATED,
type: 'DATABASE_EVENT',
activeObjectMetadataItems: generatedMockObjectMetadataItems,
}),
).toStrictEqual({
type: 'DATABASE_EVENT',
settings: {
eventName: `${generatedMockObjectMetadataItems[0].nameSingular}.created`,
outputSchema: {},
},
});
});
it('returns a valid configuration for DATABASE_EVENT trigger type update', () => {
expect(
getTriggerDefaultDefinition({
name: DatabaseTriggerName.RECORD_IS_UPDATED,
type: 'DATABASE_EVENT',
activeObjectMetadataItems: generatedMockObjectMetadataItems,
}),
).toStrictEqual({
type: 'DATABASE_EVENT',
settings: {
eventName: `${generatedMockObjectMetadataItems[0].nameSingular}.updated`,
outputSchema: {},
},
});
});
it('returns a valid configuration for DATABASE_EVENT trigger type deletion', () => {
expect(
getTriggerDefaultDefinition({
name: DatabaseTriggerName.RECORD_IS_DELETED,
type: 'DATABASE_EVENT',
activeObjectMetadataItems: generatedMockObjectMetadataItems,
}),
).toStrictEqual({
type: 'DATABASE_EVENT',
settings: {
eventName: `${generatedMockObjectMetadataItems[0].nameSingular}.deleted`,
outputSchema: {},
},
});
});
it('returns a valid configuration for DATABASE_EVENT trigger type creation', () => {
expect(
getTriggerDefaultDefinition({
name: DatabaseTriggerName.RECORD_IS_CREATED,
type: 'DATABASE_EVENT',
activeObjectMetadataItems: generatedMockObjectMetadataItems,
}),
).toStrictEqual({
type: 'DATABASE_EVENT',
settings: {
eventName: `${generatedMockObjectMetadataItems[0].nameSingular}.created`,
outputSchema: {},
},
});
});
it('returns a valid configuration for MANUAL trigger type', () => {
expect(
getTriggerDefaultDefinition({
name: 'Launch manually',
type: 'MANUAL',
activeObjectMetadataItems: generatedMockObjectMetadataItems,
}),
).toStrictEqual({
type: 'MANUAL',
activeObjectMetadataItems: generatedMockObjectMetadataItems,
}),
).toStrictEqual({
type: 'MANUAL',
settings: {
objectType: generatedMockObjectMetadataItems[0].nameSingular,
outputSchema: {},
},
settings: {
objectType: generatedMockObjectMetadataItems[0].nameSingular,
outputSchema: {},
},
});
});
it('throws when providing an unknown trigger type', () => {
expect(() => {
getTriggerDefaultDefinition({
name: DatabaseTriggerName.RECORD_IS_CREATED,
type: 'unknown' as any,
activeObjectMetadataItems: generatedMockObjectMetadataItems,
});
}).toThrow('Unknown type: unknown');
});
});
it('throws when providing an unknown trigger type', () => {
expect(() => {
getTriggerDefaultDefinition({
type: 'unknown' as any,
activeObjectMetadataItems: generatedMockObjectMetadataItems,
});
}).toThrow('Unknown type: unknown');
});

View File

@ -4,13 +4,15 @@ import {
WorkflowTriggerType,
} from '@/workflow/types/Workflow';
import { assertUnreachable } from '@/workflow/utils/assertUnreachable';
import { OBJECT_EVENT_TRIGGERS } from '@/workflow/workflow-trigger/constants/ObjectEventTriggers';
import { DATABASE_TRIGGER_EVENTS } from '@/workflow/workflow-trigger/constants/DatabaseTriggerEvents';
import { getManualTriggerDefaultSettings } from '@/workflow/workflow-trigger/utils/getManualTriggerDefaultSettings';
export const getTriggerDefaultDefinition = ({
name,
type,
activeObjectMetadataItems,
}: {
name: string;
type: WorkflowTriggerType;
activeObjectMetadataItems: ObjectMetadataItem[];
}): WorkflowTrigger => {
@ -25,7 +27,11 @@ export const getTriggerDefaultDefinition = ({
return {
type,
settings: {
eventName: `${activeObjectMetadataItems[0].nameSingular}.${OBJECT_EVENT_TRIGGERS[0].value}`,
eventName: `${activeObjectMetadataItems[0].nameSingular}.${
DATABASE_TRIGGER_EVENTS.find(
(availableEvent) => availableEvent.label === name,
)?.value
}`,
outputSchema: {},
},
};

View File

@ -10,7 +10,7 @@ it('returns the expected name for a DATABASE_EVENT trigger', () => {
outputSchema: {},
},
}),
).toBe('Company is Created');
).toBe('Record is Created');
});
it('returns the expected name for a MANUAL trigger without a defined objectType', () => {

View File

@ -3,6 +3,7 @@ import {
WorkflowTrigger,
} from '@/workflow/types/Workflow';
import { assertUnreachable } from '@/workflow/utils/assertUnreachable';
import { DATABASE_TRIGGER_EVENTS } from '@/workflow/workflow-trigger/constants/DatabaseTriggerEvents';
import { capitalize } from 'twenty-shared';
import { isDefined } from 'twenty-ui';
@ -24,7 +25,9 @@ export const getTriggerStepName = (trigger: WorkflowTrigger): string => {
const getDatabaseEventTriggerStepName = (
trigger: WorkflowDatabaseEventTrigger,
): string => {
const [object, action] = trigger.settings.eventName.split('.');
const [, action] = trigger.settings.eventName.split('.');
return `${capitalize(object)} is ${capitalize(action)}`;
return (
DATABASE_TRIGGER_EVENTS.find((event) => event.value === action)?.label ?? ''
);
};