Refacto workflow folders (#9302)

- Create separated folders for sections
- Add components
- Add utils and clean old ones
- Add constants
- Rename search variables folder and components

Next steps:
- clean hooks
- clean states
This commit is contained in:
Thomas Trompette
2024-12-31 17:08:14 +01:00
committed by GitHub
parent d4d8883794
commit 9e74ffae52
109 changed files with 195 additions and 840 deletions

View File

@ -0,0 +1,16 @@
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { workflowIdState } from '@/workflow/states/workflowIdState';
import { RightDrawerWorkflowSelectTriggerTypeContent } from '@/workflow/workflow-trigger/components/RightDrawerWorkflowSelectTriggerTypeContent';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-ui';
export const RightDrawerWorkflowSelectTriggerType = () => {
const workflowId = useRecoilValue(workflowIdState);
const workflow = useWorkflowWithCurrentVersion(workflowId);
if (!isDefined(workflow)) {
return null;
}
return <RightDrawerWorkflowSelectTriggerTypeContent workflow={workflow} />;
};

View File

@ -0,0 +1,59 @@
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
import { useUpdateWorkflowVersionTrigger } from '@/workflow/hooks/useUpdateWorkflowVersionTrigger';
import { workflowSelectedNodeState } from '@/workflow/states/workflowSelectedNodeState';
import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow';
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
import { TRIGGER_TYPES } from '@/workflow/workflow-trigger/constants/TriggerTypes';
import { getTriggerDefaultDefinition } from '@/workflow/workflow-trigger/utils/getTriggerDefaultDefinition';
import styled from '@emotion/styled';
import { useSetRecoilState } from 'recoil';
import { MenuItem } from 'twenty-ui';
const StyledActionListContainer = styled.div`
display: flex;
flex-direction: column;
height: 100%;
overflow-y: auto;
padding-block: ${({ theme }) => theme.spacing(1)};
padding-inline: ${({ theme }) => theme.spacing(2)};
`;
export const RightDrawerWorkflowSelectTriggerTypeContent = ({
workflow,
}: {
workflow: WorkflowWithCurrentVersion;
}) => {
const { updateTrigger } = useUpdateWorkflowVersionTrigger({ workflow });
const { activeObjectMetadataItems } = useFilteredObjectMetadataItems();
const { openRightDrawer } = useRightDrawer();
const setWorkflowSelectedNode = useSetRecoilState(workflowSelectedNodeState);
return (
<StyledActionListContainer>
{TRIGGER_TYPES.map((action) => (
<MenuItem
key={action.type}
LeftIcon={action.icon}
text={action.name}
onClick={async () => {
await updateTrigger(
getTriggerDefaultDefinition({
type: action.type,
activeObjectMetadataItems,
}),
);
setWorkflowSelectedNode(TRIGGER_STEP_ID);
openRightDrawer(RightDrawerPages.WorkflowStepEdit);
}}
/>
))}
</StyledActionListContainer>
);
};

View File

@ -0,0 +1,151 @@
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { Select, SelectOption } from '@/ui/input/components/Select';
import { WorkflowDatabaseEventTrigger } from '@/workflow/types/Workflow';
import { splitWorkflowTriggerEventName } from '@/workflow/utils/splitWorkflowTriggerEventName';
import { WorkflowStepBody } from '@/workflow/workflow-step/components/WorkflowStepBody';
import { WorkflowStepHeader } from '@/workflow/workflow-step/components/WorkflowStepHeader';
import { OBJECT_EVENT_TRIGGERS } from '@/workflow/workflow-trigger/constants/ObjectEventTriggers';
import { useTheme } from '@emotion/react';
import { IconPlaylistAdd, isDefined } from 'twenty-ui';
type WorkflowEditTriggerDatabaseEventFormProps = {
trigger: WorkflowDatabaseEventTrigger;
triggerOptions:
| {
readonly: true;
onTriggerUpdate?: undefined;
}
| {
readonly?: false;
onTriggerUpdate: (trigger: WorkflowDatabaseEventTrigger) => void;
};
};
export const WorkflowEditTriggerDatabaseEventForm = ({
trigger,
triggerOptions,
}: WorkflowEditTriggerDatabaseEventFormProps) => {
const theme = useTheme();
const { activeObjectMetadataItems } = useFilteredObjectMetadataItems();
const triggerEvent = isDefined(trigger)
? splitWorkflowTriggerEventName(trigger.settings.eventName)
: undefined;
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(
(availableEvent) => availableEvent.value === triggerEvent.event,
)
: undefined;
const headerTitle = isDefined(trigger.name)
? trigger.name
: isDefined(recordTypeMetadata) && isDefined(selectedEvent)
? `When a ${recordTypeMetadata.labelSingular} is ${selectedEvent.label}`
: '-';
const headerType = isDefined(selectedEvent)
? `Trigger · Record is ${selectedEvent.label}`
: '-';
return (
<>
<WorkflowStepHeader
onTitleChange={(newName: string) => {
if (triggerOptions.readonly === true) {
return;
}
triggerOptions.onTriggerUpdate({
...trigger,
name: newName,
});
}}
Icon={IconPlaylistAdd}
iconColor={theme.font.color.tertiary}
initialTitle={headerTitle}
headerType={headerType}
/>
<WorkflowStepBody>
<Select
dropdownId="workflow-edit-trigger-record-type"
label="Record Type"
fullWidth
disabled={triggerOptions.readonly}
value={triggerEvent?.objectType}
emptyOption={{ label: 'Select an option', value: '' }}
options={availableMetadata}
onChange={(updatedRecordType) => {
if (triggerOptions.readonly === true) {
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: {},
},
},
);
}}
/>
</WorkflowStepBody>
</>
);
};

View File

@ -0,0 +1,117 @@
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { Select, SelectOption } from '@/ui/input/components/Select';
import {
WorkflowManualTrigger,
WorkflowManualTriggerAvailability,
} from '@/workflow/types/Workflow';
import { WorkflowStepBody } from '@/workflow/workflow-step/components/WorkflowStepBody';
import { WorkflowStepHeader } from '@/workflow/workflow-step/components/WorkflowStepHeader';
import { MANUAL_TRIGGER_AVAILABILITY_OPTIONS } from '@/workflow/workflow-trigger/constants/ManualTriggerAvailabilityOptions';
import { getManualTriggerDefaultSettings } from '@/workflow/workflow-trigger/utils/getManualTriggerDefaultSettings';
import { useTheme } from '@emotion/react';
import { IconHandMove, isDefined, useIcons } from 'twenty-ui';
type WorkflowEditTriggerManualFormProps = {
trigger: WorkflowManualTrigger;
triggerOptions:
| {
readonly: true;
onTriggerUpdate?: undefined;
}
| {
readonly?: false;
onTriggerUpdate: (trigger: WorkflowManualTrigger) => void;
};
};
export const WorkflowEditTriggerManualForm = ({
trigger,
triggerOptions,
}: WorkflowEditTriggerManualFormProps) => {
const theme = useTheme();
const { getIcon } = useIcons();
const { activeObjectMetadataItems } = useFilteredObjectMetadataItems();
const availableMetadata: Array<SelectOption<string>> =
activeObjectMetadataItems.map((item) => ({
label: item.labelPlural,
value: item.nameSingular,
Icon: getIcon(item.icon),
}));
const manualTriggerAvailability: WorkflowManualTriggerAvailability =
isDefined(trigger.settings.objectType)
? 'WHEN_RECORD_SELECTED'
: 'EVERYWHERE';
const headerTitle = isDefined(trigger.name) ? trigger.name : 'Manual Trigger';
return (
<>
<WorkflowStepHeader
onTitleChange={(newName: string) => {
if (triggerOptions.readonly === true) {
return;
}
triggerOptions.onTriggerUpdate({
...trigger,
name: newName,
});
}}
Icon={IconHandMove}
iconColor={theme.font.color.tertiary}
initialTitle={headerTitle}
headerType="Trigger · Manual"
/>
<WorkflowStepBody>
<Select
dropdownId="workflow-edit-manual-trigger-availability"
label="Available"
fullWidth
disabled={triggerOptions.readonly}
value={manualTriggerAvailability}
options={MANUAL_TRIGGER_AVAILABILITY_OPTIONS}
onChange={(updatedTriggerType) => {
if (triggerOptions.readonly === true) {
return;
}
triggerOptions.onTriggerUpdate({
...trigger,
settings: getManualTriggerDefaultSettings({
availability: updatedTriggerType,
activeObjectMetadataItems,
}),
});
}}
/>
{manualTriggerAvailability === 'WHEN_RECORD_SELECTED' ? (
<Select
dropdownId="workflow-edit-manual-trigger-object"
label="Object"
fullWidth
value={trigger.settings.objectType}
options={availableMetadata}
disabled={triggerOptions.readonly}
onChange={(updatedObject) => {
if (triggerOptions.readonly === true) {
return;
}
triggerOptions.onTriggerUpdate({
...trigger,
settings: {
objectType: updatedObject,
outputSchema: {},
},
});
}}
/>
) : null}
</WorkflowStepBody>
</>
);
};

View File

@ -0,0 +1,19 @@
import { WorkflowManualTriggerAvailability } from '@/workflow/types/Workflow';
import { IconCheckbox, IconComponent, IconSquare } from 'twenty-ui';
export const MANUAL_TRIGGER_AVAILABILITY_OPTIONS: Array<{
label: string;
value: WorkflowManualTriggerAvailability;
Icon: IconComponent;
}> = [
{
label: 'When record(s) are selected',
value: 'WHEN_RECORD_SELECTED',
Icon: IconCheckbox,
},
{
label: 'When no record(s) are selected',
value: 'EVERYWHERE',
Icon: IconSquare,
},
];

View File

@ -0,0 +1,16 @@
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

@ -0,0 +1 @@
export const TRIGGER_STEP_ID = 'trigger';

View File

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

View File

@ -0,0 +1,26 @@
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
import { getManualTriggerDefaultSettings } from '../getManualTriggerDefaultSettings';
it('returns settings for a manual trigger that can be activated from any where', () => {
expect(
getManualTriggerDefaultSettings({
availability: 'EVERYWHERE',
activeObjectMetadataItems: generatedMockObjectMetadataItems,
}),
).toStrictEqual({
objectType: undefined,
outputSchema: {},
});
});
it('returns settings for a manual trigger that can be activated from any where', () => {
expect(
getManualTriggerDefaultSettings({
availability: 'WHEN_RECORD_SELECTED',
activeObjectMetadataItems: generatedMockObjectMetadataItems,
}),
).toStrictEqual({
objectType: generatedMockObjectMetadataItems[0].nameSingular,
outputSchema: {},
});
});

View File

@ -0,0 +1,50 @@
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: {},
},
});
});
it('returns a valid configuration for MANUAL trigger type', () => {
expect(
getTriggerDefaultDefinition({
type: 'MANUAL',
activeObjectMetadataItems: generatedMockObjectMetadataItems,
}),
).toStrictEqual({
type: 'MANUAL',
settings: {
objectType: generatedMockObjectMetadataItems[0].nameSingular,
outputSchema: {},
},
});
});
it('throws when providing an unknown trigger type', () => {
expect(() => {
getTriggerDefaultDefinition({
type: 'unknown' as any,
activeObjectMetadataItems: generatedMockObjectMetadataItems,
});
}).toThrow('Unknown type: unknown');
});

View File

@ -0,0 +1,31 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import {
WorkflowManualTriggerAvailability,
WorkflowManualTriggerSettings,
} from '@/workflow/types/Workflow';
import { assertUnreachable } from '@/workflow/utils/assertUnreachable';
export const getManualTriggerDefaultSettings = ({
availability,
activeObjectMetadataItems,
}: {
availability: WorkflowManualTriggerAvailability;
activeObjectMetadataItems: ObjectMetadataItem[];
}): WorkflowManualTriggerSettings => {
switch (availability) {
case 'EVERYWHERE': {
return {
objectType: undefined,
outputSchema: {},
};
}
case 'WHEN_RECORD_SELECTED': {
return {
objectType: activeObjectMetadataItems[0].nameSingular,
outputSchema: {},
};
}
}
return assertUnreachable(availability);
};

View File

@ -0,0 +1,46 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import {
WorkflowTrigger,
WorkflowTriggerType,
} from '@/workflow/types/Workflow';
import { assertUnreachable } from '@/workflow/utils/assertUnreachable';
import { OBJECT_EVENT_TRIGGERS } from '@/workflow/workflow-trigger/constants/ObjectEventTriggers';
import { getManualTriggerDefaultSettings } from '@/workflow/workflow-trigger/utils/getManualTriggerDefaultSettings';
export const getTriggerDefaultDefinition = ({
type,
activeObjectMetadataItems,
}: {
type: WorkflowTriggerType;
activeObjectMetadataItems: ObjectMetadataItem[];
}): WorkflowTrigger => {
if (activeObjectMetadataItems.length === 0) {
throw new Error(
'This function need to receive at least one object metadata item to run.',
);
}
switch (type) {
case 'DATABASE_EVENT': {
return {
type,
settings: {
eventName: `${activeObjectMetadataItems[0].nameSingular}.${OBJECT_EVENT_TRIGGERS[0].value}`,
outputSchema: {},
},
};
}
case 'MANUAL': {
return {
type,
settings: getManualTriggerDefaultSettings({
availability: 'WHEN_RECORD_SELECTED',
activeObjectMetadataItems,
}),
};
}
default: {
return assertUnreachable(type, `Unknown type: ${type}`);
}
}
};