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

View File

@ -1,9 +1,9 @@
import { TextInput } from '@/ui/field/input/components/TextInput'; 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 { 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` const StyledHeader = styled.div`
background-color: ${({ theme }) => theme.background.secondary}; 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 { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow'; import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow';
import { workflowSelectedNodeState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeState'; 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_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 { useUpdateWorkflowVersionTrigger } from '@/workflow/workflow-trigger/hooks/useUpdateWorkflowVersionTrigger';
import { getTriggerDefaultDefinition } from '@/workflow/workflow-trigger/utils/getTriggerDefaultDefinition'; import { getTriggerDefaultDefinition } from '@/workflow/workflow-trigger/utils/getTriggerDefaultDefinition';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
@ -21,6 +22,14 @@ const StyledActionListContainer = styled.div`
padding-inline: ${({ theme }) => theme.spacing(2)}; 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 = ({ export const RightDrawerWorkflowSelectTriggerTypeContent = ({
workflow, workflow,
}: { }: {
@ -35,14 +44,37 @@ export const RightDrawerWorkflowSelectTriggerTypeContent = ({
return ( return (
<StyledActionListContainer> <StyledActionListContainer>
{TRIGGER_TYPES.map((action) => ( <StyledSectionTitle>Data</StyledSectionTitle>
{DATABASE_TRIGGER_TYPES.map((action) => (
<MenuItem <MenuItem
key={action.type} key={action.name}
LeftIcon={action.icon} LeftIcon={action.icon}
text={action.name} text={action.name}
onClick={async () => { onClick={async () => {
await updateTrigger( await updateTrigger(
getTriggerDefaultDefinition({ 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, type: action.type,
activeObjectMetadataItems, activeObjectMetadataItems,
}), }),

View File

@ -4,7 +4,7 @@ import { WorkflowDatabaseEventTrigger } from '@/workflow/types/Workflow';
import { splitWorkflowTriggerEventName } from '@/workflow/utils/splitWorkflowTriggerEventName'; import { splitWorkflowTriggerEventName } from '@/workflow/utils/splitWorkflowTriggerEventName';
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody'; import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader'; 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 { useTheme } from '@emotion/react';
import { IconPlaylistAdd, isDefined } from 'twenty-ui'; import { IconPlaylistAdd, isDefined } from 'twenty-ui';
@ -29,35 +29,30 @@ export const WorkflowEditTriggerDatabaseEventForm = ({
const { activeObjectMetadataItems } = useFilteredObjectMetadataItems(); const { activeObjectMetadataItems } = useFilteredObjectMetadataItems();
const triggerEvent = isDefined(trigger) const triggerEvent = splitWorkflowTriggerEventName(
? splitWorkflowTriggerEventName(trigger.settings.eventName) trigger.settings.eventName,
: undefined; );
const availableMetadata: Array<SelectOption<string>> = const availableMetadata: Array<SelectOption<string>> =
activeObjectMetadataItems.map((item) => ({ activeObjectMetadataItems.map((item) => ({
label: item.labelPlural, label: item.labelPlural,
value: item.nameSingular, value: item.nameSingular,
})); }));
const recordTypeMetadata = isDefined(triggerEvent)
? activeObjectMetadataItems.find(
(item) => item.nameSingular === triggerEvent.objectType,
)
: undefined;
const selectedEvent = isDefined(triggerEvent) const selectedEvent = isDefined(triggerEvent)
? OBJECT_EVENT_TRIGGERS.find( ? DATABASE_TRIGGER_EVENTS.find(
(availableEvent) => availableEvent.value === triggerEvent.event, (availableEvent) => availableEvent.value === triggerEvent.event,
) )
: undefined; : undefined;
const headerTitle = isDefined(trigger.name) const headerTitle = isDefined(trigger.name)
? trigger.name ? trigger.name
: isDefined(recordTypeMetadata) && isDefined(selectedEvent) : isDefined(selectedEvent)
? `When a ${recordTypeMetadata.labelSingular} is ${selectedEvent.label}` ? selectedEvent.label
: '-'; : '-';
const headerType = isDefined(selectedEvent) const headerType = isDefined(selectedEvent)
? `Trigger · Record is ${selectedEvent.label}` ? `Trigger · ${selectedEvent.label}`
: '-'; : '-';
return ( return (
@ -92,57 +87,13 @@ export const WorkflowEditTriggerDatabaseEventForm = ({
return; return;
} }
triggerOptions.onTriggerUpdate( triggerOptions.onTriggerUpdate({
isDefined(trigger) && isDefined(triggerEvent) ...trigger,
? { settings: {
...trigger, ...trigger.settings,
settings: { eventName: `${updatedRecordType}.${triggerEvent.event}`,
...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: {},
},
},
);
}} }}
/> />
</WorkflowStepBody> </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 { WorkflowTriggerType } from '@/workflow/types/Workflow';
import { IconComponent, IconSettingsAutomation } from 'twenty-ui'; import { IconComponent, IconSettingsAutomation } from 'twenty-ui';
export const TRIGGER_TYPES: Array<{ export const OTHER_TRIGGER_TYPES: Array<{
name: string; name: string;
type: WorkflowTriggerType; type: WorkflowTriggerType;
icon: IconComponent; icon: IconComponent;
}> = [ }> = [
{ {
name: 'Database Event', name: 'Launch manually',
type: 'DATABASE_EVENT',
icon: IconSettingsAutomation,
},
{
name: 'Manual Trigger',
type: 'MANUAL', type: 'MANUAL',
icon: IconSettingsAutomation, icon: IconSettingsAutomation,
}, },

View File

@ -1,50 +1,105 @@
import { DatabaseTriggerName } from '@/workflow/workflow-trigger/constants/DatabaseTriggerName';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
import { getTriggerDefaultDefinition } from '../getTriggerDefaultDefinition'; import { getTriggerDefaultDefinition } from '../getTriggerDefaultDefinition';
it('throws if the activeObjectMetadataItems list is empty', () => { describe('getTriggerDefaultDefinition', () => {
expect(() => { it('throws if the activeObjectMetadataItems list is empty', () => {
getTriggerDefaultDefinition({ expect(() => {
type: 'DATABASE_EVENT', getTriggerDefaultDefinition({
activeObjectMetadataItems: [], name: DatabaseTriggerName.RECORD_IS_CREATED,
}); type: 'DATABASE_EVENT',
}).toThrow(); 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: {},
},
}); });
});
it('returns a valid configuration for MANUAL trigger type', () => { it('returns a valid configuration for DATABASE_EVENT trigger type creation', () => {
expect( expect(
getTriggerDefaultDefinition({ 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', type: 'MANUAL',
activeObjectMetadataItems: generatedMockObjectMetadataItems, settings: {
}), objectType: generatedMockObjectMetadataItems[0].nameSingular,
).toStrictEqual({ outputSchema: {},
type: 'MANUAL', },
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, WorkflowTriggerType,
} from '@/workflow/types/Workflow'; } from '@/workflow/types/Workflow';
import { assertUnreachable } from '@/workflow/utils/assertUnreachable'; 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'; import { getManualTriggerDefaultSettings } from '@/workflow/workflow-trigger/utils/getManualTriggerDefaultSettings';
export const getTriggerDefaultDefinition = ({ export const getTriggerDefaultDefinition = ({
name,
type, type,
activeObjectMetadataItems, activeObjectMetadataItems,
}: { }: {
name: string;
type: WorkflowTriggerType; type: WorkflowTriggerType;
activeObjectMetadataItems: ObjectMetadataItem[]; activeObjectMetadataItems: ObjectMetadataItem[];
}): WorkflowTrigger => { }): WorkflowTrigger => {
@ -25,7 +27,11 @@ export const getTriggerDefaultDefinition = ({
return { return {
type, type,
settings: { settings: {
eventName: `${activeObjectMetadataItems[0].nameSingular}.${OBJECT_EVENT_TRIGGERS[0].value}`, eventName: `${activeObjectMetadataItems[0].nameSingular}.${
DATABASE_TRIGGER_EVENTS.find(
(availableEvent) => availableEvent.label === name,
)?.value
}`,
outputSchema: {}, outputSchema: {},
}, },
}; };

View File

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

View File

@ -3,6 +3,7 @@ import {
WorkflowTrigger, WorkflowTrigger,
} from '@/workflow/types/Workflow'; } from '@/workflow/types/Workflow';
import { assertUnreachable } from '@/workflow/utils/assertUnreachable'; import { assertUnreachable } from '@/workflow/utils/assertUnreachable';
import { DATABASE_TRIGGER_EVENTS } from '@/workflow/workflow-trigger/constants/DatabaseTriggerEvents';
import { capitalize } from 'twenty-shared'; import { capitalize } from 'twenty-shared';
import { isDefined } from 'twenty-ui'; import { isDefined } from 'twenty-ui';
@ -24,7 +25,9 @@ export const getTriggerStepName = (trigger: WorkflowTrigger): string => {
const getDatabaseEventTriggerStepName = ( const getDatabaseEventTriggerStepName = (
trigger: WorkflowDatabaseEventTrigger, trigger: WorkflowDatabaseEventTrigger,
): string => { ): 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 ?? ''
);
}; };