Add workflow email action (#7279)

- Add the SAVE_EMAIL action. This action requires more setting
parameters than the Serverless Function action.
- Changed the way we computed the workflow diagram. It now preserves
some properties, like the `selected` property. That's necessary to not
close the right drawer when the workflow back-end data change.
- Added the possibility to set a label to a TextArea. This uses a
`<label>` HTML element and the `useId()` hook to create an id linking
the label with the input.
This commit is contained in:
Baptiste Devessier
2024-10-01 14:22:14 +02:00
committed by GitHub
parent 0d570caff5
commit cde255a031
19 changed files with 512 additions and 106 deletions

View File

@ -72,6 +72,7 @@ describe('generateWorkflowDiagram', () => {
for (const [index, step] of steps.entries()) {
expect(stepNodes[index].data).toEqual({
nodeType: 'action',
actionType: 'CODE',
label: step.name,
});
}

View File

@ -0,0 +1,72 @@
import { WorkflowDiagram } from '@/workflow/types/WorkflowDiagram';
import { mergeWorkflowDiagrams } from '../mergeWorkflowDiagrams';
it('Preserves the properties defined in the previous version but not in the next one', () => {
const previousDiagram: WorkflowDiagram = {
nodes: [
{
data: { nodeType: 'action', label: '', actionType: 'CODE' },
id: '1',
position: { x: 0, y: 0 },
selected: true,
},
],
edges: [],
};
const nextDiagram: WorkflowDiagram = {
nodes: [
{
data: { nodeType: 'action', label: '', actionType: 'CODE' },
id: '1',
position: { x: 0, y: 0 },
},
],
edges: [],
};
expect(mergeWorkflowDiagrams(previousDiagram, nextDiagram)).toEqual({
nodes: [
{
data: { nodeType: 'action', label: '', actionType: 'CODE' },
id: '1',
position: { x: 0, y: 0 },
selected: true,
},
],
edges: [],
});
});
it('Replaces duplicated properties with the next value', () => {
const previousDiagram: WorkflowDiagram = {
nodes: [
{
data: { nodeType: 'action', label: '', actionType: 'CODE' },
id: '1',
position: { x: 0, y: 0 },
},
],
edges: [],
};
const nextDiagram: WorkflowDiagram = {
nodes: [
{
data: { nodeType: 'action', label: '2', actionType: 'CODE' },
id: '1',
position: { x: 0, y: 0 },
},
],
edges: [],
};
expect(mergeWorkflowDiagrams(previousDiagram, nextDiagram)).toEqual({
nodes: [
{
data: { nodeType: 'action', label: '2', actionType: 'CODE' },
id: '1',
position: { x: 0, y: 0 },
},
],
edges: [],
});
});

View File

@ -0,0 +1,3 @@
export const assertUnreachable = (x: never, errorMessage?: string): never => {
throw new Error(errorMessage ?? "Didn't expect to get here.");
};

View File

@ -33,6 +33,7 @@ export const generateWorkflowDiagram = ({
id: nodeId,
data: {
nodeType: 'action',
actionType: step.type,
label: step.name,
},
position: {

View File

@ -26,6 +26,27 @@ export const getStepDefaultDefinition = (
},
};
}
case 'SEND_EMAIL': {
return {
id: newStepId,
name: 'Send Email',
type: 'SEND_EMAIL',
valid: false,
settings: {
subject: 'hello',
title: 'hello',
template: '{{title}}',
errorHandlingOptions: {
continueOnFailure: {
value: false,
},
retryOnFailure: {
value: false,
},
},
},
};
}
default: {
throw new Error(`Unknown type: ${type}`);
}

View File

@ -0,0 +1,33 @@
import {
WorkflowDiagram,
WorkflowDiagramNode,
} from '@/workflow/types/WorkflowDiagram';
const nodePropertiesToPreserve: Array<keyof WorkflowDiagramNode> = ['selected'];
export const mergeWorkflowDiagrams = (
previousDiagram: WorkflowDiagram,
nextDiagram: WorkflowDiagram,
): WorkflowDiagram => {
const lastNodes = nextDiagram.nodes.map((nextNode) => {
const previousNode = previousDiagram.nodes.find(
(previousNode) => previousNode.id === nextNode.id,
);
const nodeWithPreservedProperties = nodePropertiesToPreserve.reduce(
(nodeToSet, propertyToPreserve) => {
return Object.assign(nodeToSet, {
[propertyToPreserve]: previousNode?.[propertyToPreserve],
});
},
{} as Partial<WorkflowDiagramNode>,
);
return Object.assign(nodeWithPreservedProperties, nextNode);
});
return {
nodes: lastNodes,
edges: nextDiagram.edges,
};
};

View File

@ -1,14 +1,14 @@
import { WorkflowStep } from '@/workflow/types/Workflow';
import { findStepPositionOrThrow } from '@/workflow/utils/findStepPositionOrThrow';
export const replaceStep = ({
export const replaceStep = <T extends WorkflowStep>({
steps: stepsInitial,
stepId,
stepToReplace,
}: {
steps: Array<WorkflowStep>;
stepId: string;
stepToReplace: Partial<Omit<WorkflowStep, 'id'>>;
stepToReplace: Partial<Omit<T, 'id'>>;
}) => {
const steps = structuredClone(stepsInitial);