Implement workflow loops default state (#14297)
https://github.com/user-attachments/assets/9176ae58-f253-469c-b5b6-98d77909db53
This commit is contained in:
committed by
GitHub
parent
4d6ef9bc34
commit
7b23f5ac91
+5
-3
@@ -2,10 +2,8 @@ import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/ho
|
||||
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
|
||||
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||
import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState';
|
||||
import { WorkflowDiagramBlankEdge } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramBlankEdge';
|
||||
import { WorkflowDiagramCanvasBase } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasBase';
|
||||
import { WorkflowDiagramCanvasEditableEffect } from '@/workflow/workflow-diagram/components/WorkflowDiagramCanvasEditableEffect';
|
||||
import { WorkflowDiagramDefaultEdgeEditable } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramDefaultEdgeEditable';
|
||||
import { workflowDiagramComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramComponentState';
|
||||
import { workflowDiagramRightClickMenuPositionState } from '@/workflow/workflow-diagram/states/workflowDiagramRightClickMenuPositionState';
|
||||
import {
|
||||
@@ -13,8 +11,11 @@ import {
|
||||
type WorkflowDiagramNode,
|
||||
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { getWorkflowVersionStatusTagProps } from '@/workflow/workflow-diagram/utils/getWorkflowVersionStatusTagProps';
|
||||
import { WorkflowDiagramEmptyTriggerEditable } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramEmptyTriggerEditable';
|
||||
import { WorkflowDiagramBlankEdge } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramBlankEdge';
|
||||
import { WorkflowDiagramDefaultEdgeEditable } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramDefaultEdgeEditable';
|
||||
import { WorkflowDiagramFilterEdgeEditable } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramFilterEdgeEditable';
|
||||
import { WorkflowDiagramEmptyTriggerEditable } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramEmptyTriggerEditable';
|
||||
import { WorkflowDiagramIteratorEmptyActionEditable } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramIteratorEmptyActionEditable';
|
||||
import { WorkflowDiagramStepNodeEditable } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeEditable';
|
||||
import { useCreateEdge } from '@/workflow/workflow-steps/hooks/useCreateEdge';
|
||||
import { useDeleteEdge } from '@/workflow/workflow-steps/hooks/useDeleteEdge';
|
||||
@@ -121,6 +122,7 @@ export const WorkflowDiagramCanvasEditable = () => {
|
||||
nodeTypes={{
|
||||
default: WorkflowDiagramStepNodeEditable,
|
||||
'empty-trigger': WorkflowDiagramEmptyTriggerEditable,
|
||||
'iterator-empty-action': WorkflowDiagramIteratorEmptyActionEditable,
|
||||
}}
|
||||
edgeTypes={{
|
||||
blank: WorkflowDiagramBlankEdge,
|
||||
|
||||
+10
@@ -19,12 +19,19 @@ const StyledVerticalLineContainer = styled.div`
|
||||
width: 56px;
|
||||
`;
|
||||
|
||||
const StyledLabelContainer = styled.div`
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
`;
|
||||
|
||||
type WorkflowDiagramCreateStepElementProps = {
|
||||
data: WorkflowDiagramStepNodeData;
|
||||
Label?: React.ReactNode;
|
||||
};
|
||||
|
||||
export const WorkflowDiagramCreateStepElement = ({
|
||||
data,
|
||||
Label,
|
||||
}: WorkflowDiagramCreateStepElementProps) => {
|
||||
const { startNodeCreation } = useStartNodeCreation();
|
||||
|
||||
@@ -44,6 +51,9 @@ export const WorkflowDiagramCreateStepElement = ({
|
||||
<StyledVerticalLineContainer>
|
||||
<WorkflowDiagramConnector />
|
||||
</StyledVerticalLineContainer>
|
||||
|
||||
{Label && <StyledLabelContainer>{Label}</StyledLabelContainer>}
|
||||
|
||||
<IconButton
|
||||
Icon={IconPlus}
|
||||
size="small"
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@ export const WorkflowDiagramEffect = () => {
|
||||
|
||||
const nextWorkflowDiagram = getWorkflowVersionDiagram({
|
||||
workflowVersion: currentVersion,
|
||||
isEditable: true,
|
||||
workflowContext: 'workflow',
|
||||
});
|
||||
|
||||
let mergedWorkflowDiagram = nextWorkflowDiagram;
|
||||
|
||||
+1
-1
@@ -60,7 +60,7 @@ export const WorkflowVersionVisualizerEffect = ({
|
||||
|
||||
const nextWorkflowDiagram = getWorkflowVersionDiagram({
|
||||
workflowVersion,
|
||||
isEditable: false,
|
||||
workflowContext: 'workflow-version',
|
||||
});
|
||||
|
||||
setWorkflowDiagram(nextWorkflowDiagram);
|
||||
|
||||
+2
@@ -6,6 +6,7 @@ import { fn } from '@storybook/test';
|
||||
import '@xyflow/react/dist/style.css';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
import { CatalogDecorator, type CatalogStory } from 'twenty-ui/testing';
|
||||
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
||||
import { ReactflowDecorator } from '~/testing/decorators/ReactflowDecorator';
|
||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||
import { WorkflowDiagramStepNodeEditableContent } from '../../workflow-nodes/components/WorkflowDiagramStepNodeEditableContent';
|
||||
@@ -16,6 +17,7 @@ const meta: Meta<typeof WorkflowDiagramStepNodeEditableContent> = {
|
||||
parameters: {
|
||||
msw: graphqlMocks,
|
||||
},
|
||||
decorators: [I18nFrontDecorator],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export type WorkflowContext = 'workflow' | 'workflow-version' | 'workflow-run';
|
||||
+28
-3
@@ -3,8 +3,10 @@ import {
|
||||
type WorkflowRunStepStatus,
|
||||
type WorkflowTriggerType,
|
||||
} from '@/workflow/types/Workflow';
|
||||
import { type WorkflowDiagramIteratorEmptyActionNodeData } from '@/workflow/workflow-diagram/workflow-iterator/types/WorkflowDiagramIteratorEmptyActionNodeData';
|
||||
import { type FilterSettings } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilter';
|
||||
import { type Edge, type Node } from '@xyflow/react';
|
||||
import { type MessageDescriptor } from '@lingui/core';
|
||||
import { type Edge, type Node, type Position } from '@xyflow/react';
|
||||
import { type StepStatus } from 'twenty-shared/workflow';
|
||||
|
||||
export type WorkflowDiagramStepNode = Node<WorkflowDiagramStepNodeData>;
|
||||
@@ -23,6 +25,14 @@ export type WorkflowDiagram = {
|
||||
edges: Array<WorkflowDiagramEdge>;
|
||||
};
|
||||
|
||||
export type WorkflowDiagramNodeRightHandleOptions = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type WorkflowDiagramNodeDefaultHandleOptions = {
|
||||
label?: MessageDescriptor;
|
||||
};
|
||||
|
||||
export type WorkflowDiagramStepNodeData =
|
||||
| {
|
||||
nodeType: 'trigger';
|
||||
@@ -32,6 +42,8 @@ export type WorkflowDiagramStepNodeData =
|
||||
runStatus?: WorkflowRunStepStatus;
|
||||
hasNextStepIds: boolean;
|
||||
stepId: string;
|
||||
defaultHandleOptions?: WorkflowDiagramNodeDefaultHandleOptions;
|
||||
rightHandleOptions?: WorkflowDiagramNodeRightHandleOptions;
|
||||
position: {
|
||||
x: number;
|
||||
y: number;
|
||||
@@ -44,6 +56,8 @@ export type WorkflowDiagramStepNodeData =
|
||||
runStatus?: WorkflowRunStepStatus;
|
||||
hasNextStepIds: boolean;
|
||||
stepId: string;
|
||||
defaultHandleOptions?: WorkflowDiagramNodeDefaultHandleOptions;
|
||||
rightHandleOptions?: WorkflowDiagramNodeRightHandleOptions;
|
||||
position: {
|
||||
x: number;
|
||||
y: number;
|
||||
@@ -67,13 +81,19 @@ export type WorkflowDiagramEmptyTriggerNodeData = {
|
||||
|
||||
export type WorkflowDiagramNodeData =
|
||||
| WorkflowDiagramStepNodeData
|
||||
| WorkflowDiagramEmptyTriggerNodeData;
|
||||
| WorkflowDiagramEmptyTriggerNodeData
|
||||
| WorkflowDiagramIteratorEmptyActionNodeData;
|
||||
|
||||
export type WorkflowRunDiagramNodeData = Exclude<
|
||||
WorkflowDiagramStepNodeData,
|
||||
'runStatus'
|
||||
> & { runStatus: WorkflowRunStepStatus };
|
||||
|
||||
export type WorkflowDiagramEdgeLabelOptions = {
|
||||
position: Position;
|
||||
label: MessageDescriptor;
|
||||
};
|
||||
|
||||
export type WorkflowDiagramFilterEdgeData = {
|
||||
edgeType: 'filter';
|
||||
stepId: string;
|
||||
@@ -81,18 +101,23 @@ export type WorkflowDiagramFilterEdgeData = {
|
||||
name: string;
|
||||
runStatus?: WorkflowRunStepStatus;
|
||||
edgeExecutionStatus?: StepStatus;
|
||||
labelOptions?: WorkflowDiagramEdgeLabelOptions;
|
||||
};
|
||||
|
||||
export type WorkflowDiagramDefaultEdgeData = {
|
||||
edgeType: 'default';
|
||||
edgeExecutionStatus?: StepStatus;
|
||||
labelOptions?: WorkflowDiagramEdgeLabelOptions;
|
||||
};
|
||||
|
||||
export type WorkflowDiagramEdgeData =
|
||||
| WorkflowDiagramFilterEdgeData
|
||||
| WorkflowDiagramDefaultEdgeData;
|
||||
|
||||
export type WorkflowDiagramNodeType = 'default' | 'empty-trigger';
|
||||
export type WorkflowDiagramNodeType =
|
||||
| 'default'
|
||||
| 'empty-trigger'
|
||||
| 'iterator-empty-action';
|
||||
|
||||
export type WorkflowDiagramEdgeType =
|
||||
| 'blank'
|
||||
|
||||
+5
-5
@@ -19,7 +19,7 @@ describe('generateWorkflowDiagram', () => {
|
||||
const result = generateWorkflowDiagram({
|
||||
trigger,
|
||||
steps,
|
||||
defaultEdgeType: 'empty-filter--editable',
|
||||
workflowContext: 'workflow',
|
||||
});
|
||||
|
||||
expect(result.nodes).toHaveLength(1);
|
||||
@@ -86,7 +86,7 @@ describe('generateWorkflowDiagram', () => {
|
||||
const result = generateWorkflowDiagram({
|
||||
trigger,
|
||||
steps,
|
||||
defaultEdgeType: 'empty-filter--editable',
|
||||
workflowContext: 'workflow',
|
||||
});
|
||||
|
||||
expect(result.nodes).toHaveLength(steps.length + 1); // All steps + trigger
|
||||
@@ -165,7 +165,7 @@ describe('generateWorkflowDiagram', () => {
|
||||
const result = generateWorkflowDiagram({
|
||||
trigger,
|
||||
steps,
|
||||
defaultEdgeType: 'empty-filter--editable',
|
||||
workflowContext: 'workflow',
|
||||
});
|
||||
|
||||
expect(result.edges.length).toEqual(2);
|
||||
@@ -232,7 +232,7 @@ describe('generateWorkflowDiagram', () => {
|
||||
const result = generateWorkflowDiagram({
|
||||
trigger,
|
||||
steps,
|
||||
defaultEdgeType: 'empty-filter--editable',
|
||||
workflowContext: 'workflow',
|
||||
});
|
||||
|
||||
expect(result.edges.length).toEqual(2);
|
||||
@@ -318,7 +318,7 @@ describe('generateWorkflowDiagram', () => {
|
||||
const result = generateWorkflowDiagram({
|
||||
trigger,
|
||||
steps,
|
||||
defaultEdgeType: 'empty-filter--editable',
|
||||
workflowContext: 'workflow',
|
||||
});
|
||||
|
||||
expect(result.edges.length).toEqual(4);
|
||||
|
||||
+4
-4
@@ -9,7 +9,7 @@ describe('getWorkflowVersionDiagram', () => {
|
||||
it('returns an empty diagram if the provided workflow version', () => {
|
||||
const result = getWorkflowVersionDiagram({
|
||||
workflowVersion: undefined,
|
||||
isEditable: true,
|
||||
workflowContext: 'workflow',
|
||||
});
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
@@ -33,7 +33,7 @@ describe('getWorkflowVersionDiagram', () => {
|
||||
updatedAt: '',
|
||||
workflowId: '',
|
||||
},
|
||||
isEditable: true,
|
||||
workflowContext: 'workflow',
|
||||
});
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
@@ -77,7 +77,7 @@ describe('getWorkflowVersionDiagram', () => {
|
||||
updatedAt: '',
|
||||
workflowId: '',
|
||||
},
|
||||
isEditable: true,
|
||||
workflowContext: 'workflow',
|
||||
});
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
@@ -145,7 +145,7 @@ describe('getWorkflowVersionDiagram', () => {
|
||||
updatedAt: '',
|
||||
workflowId: '',
|
||||
},
|
||||
isEditable: true,
|
||||
workflowContext: 'workflow',
|
||||
});
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
|
||||
+5
-5
@@ -45,7 +45,7 @@ describe('transformFilterNodesAsEdges', () => {
|
||||
const result = transformFilterNodesAsEdges({
|
||||
nodes: diagram.nodes,
|
||||
edges: diagram.edges,
|
||||
defaultFilterEdgeType: 'filter--editable',
|
||||
workflowContext: 'workflow',
|
||||
});
|
||||
|
||||
expect(result.nodes).toEqual(diagram.nodes);
|
||||
@@ -111,7 +111,7 @@ describe('transformFilterNodesAsEdges', () => {
|
||||
const result = transformFilterNodesAsEdges({
|
||||
nodes: diagram.nodes,
|
||||
edges: diagram.edges,
|
||||
defaultFilterEdgeType: 'filter--editable',
|
||||
workflowContext: 'workflow',
|
||||
});
|
||||
|
||||
// Should only have nodes A and C
|
||||
@@ -256,7 +256,7 @@ describe('transformFilterNodesAsEdges', () => {
|
||||
const result = transformFilterNodesAsEdges({
|
||||
nodes: diagram.nodes,
|
||||
edges: diagram.edges,
|
||||
defaultFilterEdgeType: 'filter--editable',
|
||||
workflowContext: 'workflow',
|
||||
});
|
||||
|
||||
// Should only have nodes A, C, and D
|
||||
@@ -348,7 +348,7 @@ describe('transformFilterNodesAsEdges', () => {
|
||||
const result = transformFilterNodesAsEdges({
|
||||
nodes: diagram.nodes,
|
||||
edges: diagram.edges,
|
||||
defaultFilterEdgeType: 'filter--editable',
|
||||
workflowContext: 'workflow',
|
||||
});
|
||||
|
||||
// Should only have node A (filter node B is removed)
|
||||
@@ -430,7 +430,7 @@ describe('transformFilterNodesAsEdges', () => {
|
||||
const result = transformFilterNodesAsEdges({
|
||||
nodes: diagram.nodes,
|
||||
edges: diagram.edges,
|
||||
defaultFilterEdgeType: 'filter--editable',
|
||||
workflowContext: 'workflow',
|
||||
});
|
||||
|
||||
// Should have trigger and C nodes
|
||||
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
import { type WorkflowStep } from '@/workflow/types/Workflow';
|
||||
import { type WorkflowContext } from '@/workflow/workflow-diagram/types/WorkflowContext';
|
||||
import {
|
||||
type WorkflowDiagramEdge,
|
||||
type WorkflowDiagramNode,
|
||||
type WorkflowDiagramStepNodeData,
|
||||
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { getEdgeTypeBetweenTwoNodes } from '@/workflow/workflow-diagram/utils/getEdgeTypeBetweenTwoNodes';
|
||||
import { WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION } from '@/workflow/workflow-diagram/workflow-edges/constants/WorkflowVisualizerEdgeDefaultConfiguration';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
export const generateNodesAndEdgesForDefaultNode = ({
|
||||
step,
|
||||
xPos,
|
||||
yPos,
|
||||
nodes,
|
||||
edges,
|
||||
workflowContext,
|
||||
}: {
|
||||
step: WorkflowStep;
|
||||
yPos: number;
|
||||
xPos: number;
|
||||
nodes: readonly WorkflowDiagramNode[];
|
||||
edges: readonly WorkflowDiagramEdge[];
|
||||
workflowContext: WorkflowContext;
|
||||
}): {
|
||||
nodes: Array<WorkflowDiagramNode>;
|
||||
edges: Array<WorkflowDiagramEdge>;
|
||||
} => {
|
||||
const edgeTypeBetweenTwoNodes = getEdgeTypeBetweenTwoNodes({
|
||||
workflowContext,
|
||||
});
|
||||
|
||||
const updatedNodes = [...nodes];
|
||||
const updatedEdges = [...edges];
|
||||
|
||||
updatedNodes.push({
|
||||
id: step.id,
|
||||
data: {
|
||||
nodeType: 'action',
|
||||
actionType: step.type,
|
||||
name: step.name,
|
||||
hasNextStepIds:
|
||||
isDefined(step.nextStepIds) && step.nextStepIds.length > 0,
|
||||
stepId: step.id,
|
||||
position: step.position ?? {
|
||||
x: xPos,
|
||||
y: yPos,
|
||||
},
|
||||
} satisfies WorkflowDiagramStepNodeData,
|
||||
position: step.position ?? {
|
||||
x: xPos,
|
||||
y: yPos,
|
||||
},
|
||||
});
|
||||
|
||||
step.nextStepIds?.forEach((child) => {
|
||||
updatedEdges.push({
|
||||
...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION,
|
||||
type: edgeTypeBetweenTwoNodes,
|
||||
id: v4(),
|
||||
source: step.id,
|
||||
target: child,
|
||||
...(edgeTypeBetweenTwoNodes.includes('editable')
|
||||
? { deletable: true, selectable: true }
|
||||
: {}),
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
nodes: updatedNodes,
|
||||
edges: updatedEdges,
|
||||
};
|
||||
};
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
import { type WorkflowStep } from '@/workflow/types/Workflow';
|
||||
import { type WorkflowContext } from '@/workflow/workflow-diagram/types/WorkflowContext';
|
||||
import {
|
||||
type WorkflowDiagramEdge,
|
||||
type WorkflowDiagramNode,
|
||||
type WorkflowDiagramStepNodeData,
|
||||
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { getEdgeTypeBetweenTwoNodes } from '@/workflow/workflow-diagram/utils/getEdgeTypeBetweenTwoNodes';
|
||||
import { WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION } from '@/workflow/workflow-diagram/workflow-edges/constants/WorkflowVisualizerEdgeDefaultConfiguration';
|
||||
import { type WorkflowDiagramIteratorEmptyActionNodeData } from '@/workflow/workflow-diagram/workflow-iterator/types/WorkflowDiagramIteratorEmptyActionNodeData';
|
||||
import { WORKFLOW_DIAGRAM_ITERATOR_NODE_LOOP_HANDLE_ID } from '@/workflow/workflow-diagram/workflow-nodes/constants/WorkflowDiagramIteratorNodeLoopHandleId';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { isNonEmptyArray, isNonEmptyString } from '@sniptt/guards';
|
||||
import { Position } from '@xyflow/react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
export const generateNodesAndEdgesForIteratorNode = ({
|
||||
step,
|
||||
xPos,
|
||||
yPos,
|
||||
nodes,
|
||||
edges,
|
||||
workflowContext,
|
||||
}: {
|
||||
step: WorkflowStep & { type: 'ITERATOR' };
|
||||
yPos: number;
|
||||
xPos: number;
|
||||
nodes: readonly WorkflowDiagramNode[];
|
||||
edges: readonly WorkflowDiagramEdge[];
|
||||
workflowContext: WorkflowContext;
|
||||
}): {
|
||||
nodes: Array<WorkflowDiagramNode>;
|
||||
edges: Array<WorkflowDiagramEdge>;
|
||||
} => {
|
||||
const edgeTypeBetweenTwoNodes = getEdgeTypeBetweenTwoNodes({
|
||||
workflowContext,
|
||||
});
|
||||
|
||||
const updatedNodes = [...nodes];
|
||||
const updatedEdges = [...edges];
|
||||
|
||||
const iteratorNode: WorkflowDiagramNode = {
|
||||
id: step.id,
|
||||
data: {
|
||||
nodeType: 'action',
|
||||
actionType: step.type,
|
||||
name: step.name,
|
||||
hasNextStepIds:
|
||||
isDefined(step.nextStepIds) && step.nextStepIds.length > 0,
|
||||
stepId: step.id,
|
||||
position: step.position ?? {
|
||||
x: xPos,
|
||||
y: yPos,
|
||||
},
|
||||
defaultHandleOptions: {
|
||||
label: msg`completed`,
|
||||
},
|
||||
rightHandleOptions: {
|
||||
id: WORKFLOW_DIAGRAM_ITERATOR_NODE_LOOP_HANDLE_ID,
|
||||
},
|
||||
} satisfies WorkflowDiagramStepNodeData,
|
||||
position: step.position ?? {
|
||||
x: xPos,
|
||||
y: yPos,
|
||||
},
|
||||
};
|
||||
|
||||
updatedNodes.push(iteratorNode);
|
||||
|
||||
const initialLoopStepIds = Array.isArray(
|
||||
step.settings.input.initialLoopStepIds,
|
||||
)
|
||||
? step.settings.input.initialLoopStepIds
|
||||
: isNonEmptyString(step.settings.input.initialLoopStepIds)
|
||||
? [step.settings.input.initialLoopStepIds]
|
||||
: [];
|
||||
|
||||
if (isNonEmptyArray(initialLoopStepIds)) {
|
||||
for (const initialLoopStepId of initialLoopStepIds) {
|
||||
updatedEdges.push({
|
||||
...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION,
|
||||
type: edgeTypeBetweenTwoNodes,
|
||||
id: v4(),
|
||||
source: step.id,
|
||||
sourceHandle: WORKFLOW_DIAGRAM_ITERATOR_NODE_LOOP_HANDLE_ID,
|
||||
target: initialLoopStepId,
|
||||
...(edgeTypeBetweenTwoNodes.includes('editable')
|
||||
? { deletable: true, selectable: true }
|
||||
: {}),
|
||||
data: {
|
||||
...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION.data,
|
||||
labelOptions: {
|
||||
position: Position.Right,
|
||||
label: msg`loop`,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const emptyNodeId = `${step.id}-empty-loop`;
|
||||
|
||||
updatedNodes.push({
|
||||
id: emptyNodeId,
|
||||
type: 'iterator-empty-action',
|
||||
data: {
|
||||
nodeType: 'iterator-empty-action',
|
||||
parentIteratorStepId: step.id,
|
||||
position: {
|
||||
x: iteratorNode.position.x + 175,
|
||||
y: iteratorNode.position.y + 75,
|
||||
},
|
||||
} satisfies WorkflowDiagramIteratorEmptyActionNodeData,
|
||||
position: {
|
||||
x: iteratorNode.position.x + 175,
|
||||
y: iteratorNode.position.y + 75,
|
||||
},
|
||||
draggable: false,
|
||||
});
|
||||
|
||||
updatedEdges.push({
|
||||
...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION,
|
||||
type: edgeTypeBetweenTwoNodes,
|
||||
id: v4(),
|
||||
source: step.id,
|
||||
sourceHandle: WORKFLOW_DIAGRAM_ITERATOR_NODE_LOOP_HANDLE_ID,
|
||||
target: emptyNodeId,
|
||||
...(edgeTypeBetweenTwoNodes.includes('editable')
|
||||
? { deletable: true, selectable: true }
|
||||
: {}),
|
||||
data: {
|
||||
...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION.data,
|
||||
labelOptions: {
|
||||
position: Position.Right,
|
||||
label: msg`loop`,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
updatedEdges.push({
|
||||
...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION,
|
||||
type: edgeTypeBetweenTwoNodes,
|
||||
id: v4(),
|
||||
source: emptyNodeId,
|
||||
target: step.id,
|
||||
...(edgeTypeBetweenTwoNodes.includes('editable')
|
||||
? { deletable: true, selectable: true }
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
|
||||
step.nextStepIds?.forEach((child) => {
|
||||
updatedEdges.push({
|
||||
...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION,
|
||||
type: edgeTypeBetweenTwoNodes,
|
||||
id: v4(),
|
||||
source: step.id,
|
||||
target: child,
|
||||
...(edgeTypeBetweenTwoNodes.includes('editable')
|
||||
? { deletable: true, selectable: true }
|
||||
: {}),
|
||||
data: {
|
||||
...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION.data,
|
||||
labelOptions: {
|
||||
position: Position.Bottom,
|
||||
label: msg`completed`,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
nodes: updatedNodes,
|
||||
edges: updatedEdges,
|
||||
};
|
||||
};
|
||||
+54
-50
@@ -4,17 +4,18 @@ import {
|
||||
} from '@/workflow/types/Workflow';
|
||||
import { FIRST_NODE_POSITION } from '@/workflow/workflow-diagram/constants/FirstNodePosition';
|
||||
import { VERTICAL_DISTANCE_BETWEEN_TWO_NODES } from '@/workflow/workflow-diagram/constants/VerticalDistanceBetweenTwoNodes';
|
||||
import { WORKFLOW_DIAGRAM_EMPTY_TRIGGER_NODE_DEFINITION } from '@/workflow/workflow-diagram/constants/WorkflowDiagramEmptyTriggerNodeDefinition';
|
||||
import { type WorkflowContext } from '@/workflow/workflow-diagram/types/WorkflowContext';
|
||||
import {
|
||||
type WorkflowDiagram,
|
||||
type WorkflowDiagramEdge,
|
||||
type WorkflowDiagramEdgeType,
|
||||
type WorkflowDiagramNode,
|
||||
type WorkflowDiagramStepNodeData,
|
||||
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { generateNodesAndEdgesForDefaultNode } from '@/workflow/workflow-diagram/utils/generateNodesAndEdgesForDefaultNode';
|
||||
import { generateNodesAndEdgesForIteratorNode } from '@/workflow/workflow-diagram/utils/generateNodesAndEdgesForIteratorNode';
|
||||
import { getEdgeTypeBetweenTwoNodes } from '@/workflow/workflow-diagram/utils/getEdgeTypeBetweenTwoNodes';
|
||||
import { getWorkflowDiagramTriggerNode } from '@/workflow/workflow-diagram/utils/getWorkflowDiagramTriggerNode';
|
||||
import { WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION } from '@/workflow/workflow-diagram/workflow-edges/constants/WorkflowVisualizerEdgeDefaultConfiguration';
|
||||
|
||||
import { WORKFLOW_DIAGRAM_EMPTY_TRIGGER_NODE_DEFINITION } from '@/workflow/workflow-diagram/constants/WorkflowDiagramEmptyTriggerNodeDefinition';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { TRIGGER_STEP_ID } from 'twenty-shared/workflow';
|
||||
import { v4 } from 'uuid';
|
||||
@@ -22,14 +23,18 @@ import { v4 } from 'uuid';
|
||||
export const generateWorkflowDiagram = ({
|
||||
trigger,
|
||||
steps,
|
||||
defaultEdgeType,
|
||||
workflowContext,
|
||||
}: {
|
||||
trigger: WorkflowTrigger | undefined;
|
||||
steps: Array<WorkflowStep>;
|
||||
defaultEdgeType: WorkflowDiagramEdgeType;
|
||||
workflowContext: WorkflowContext;
|
||||
}): WorkflowDiagram => {
|
||||
const nodes: Array<WorkflowDiagramNode> = [];
|
||||
const edges: Array<WorkflowDiagramEdge> = [];
|
||||
let nodes: Array<WorkflowDiagramNode> = [];
|
||||
let edges: Array<WorkflowDiagramEdge> = [];
|
||||
|
||||
const edgeTypeBetweenTwoNodes = getEdgeTypeBetweenTwoNodes({
|
||||
workflowContext,
|
||||
});
|
||||
|
||||
if (isDefined(trigger)) {
|
||||
nodes.push(getWorkflowDiagramTriggerNode({ trigger }));
|
||||
@@ -37,60 +42,59 @@ export const generateWorkflowDiagram = ({
|
||||
nodes.push(WORKFLOW_DIAGRAM_EMPTY_TRIGGER_NODE_DEFINITION);
|
||||
}
|
||||
|
||||
let levelYPos = FIRST_NODE_POSITION.y;
|
||||
|
||||
const xPos = FIRST_NODE_POSITION.x;
|
||||
|
||||
for (const step of steps) {
|
||||
levelYPos += VERTICAL_DISTANCE_BETWEEN_TWO_NODES;
|
||||
|
||||
nodes.push({
|
||||
id: step.id,
|
||||
data: {
|
||||
nodeType: 'action',
|
||||
actionType: step.type,
|
||||
name: step.name,
|
||||
hasNextStepIds:
|
||||
isDefined(step.nextStepIds) && step.nextStepIds.length > 0,
|
||||
stepId: step.id,
|
||||
position: step.position ?? {
|
||||
x: xPos,
|
||||
y: levelYPos,
|
||||
},
|
||||
} satisfies WorkflowDiagramStepNodeData,
|
||||
position: step.position ?? {
|
||||
x: xPos,
|
||||
y: levelYPos,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
for (const stepLinkToTriggerId of trigger?.nextStepIds ?? []) {
|
||||
edges.push({
|
||||
...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION,
|
||||
type: defaultEdgeType,
|
||||
type: edgeTypeBetweenTwoNodes,
|
||||
id: v4(),
|
||||
source: TRIGGER_STEP_ID,
|
||||
target: stepLinkToTriggerId,
|
||||
...(defaultEdgeType.includes('editable')
|
||||
...(edgeTypeBetweenTwoNodes.includes('editable')
|
||||
? { deletable: true, selectable: true }
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
|
||||
const xPos = FIRST_NODE_POSITION.x;
|
||||
let levelYPos = FIRST_NODE_POSITION.y;
|
||||
|
||||
for (const step of steps) {
|
||||
step.nextStepIds?.forEach((child) => {
|
||||
edges.push({
|
||||
...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION,
|
||||
type: defaultEdgeType,
|
||||
id: v4(),
|
||||
source: step.id,
|
||||
target: child,
|
||||
...(defaultEdgeType.includes('editable')
|
||||
? { deletable: true, selectable: true }
|
||||
: {}),
|
||||
});
|
||||
});
|
||||
levelYPos += VERTICAL_DISTANCE_BETWEEN_TWO_NODES;
|
||||
|
||||
switch (step.type) {
|
||||
case 'ITERATOR': {
|
||||
const { nodes: iteratorNodes, edges: iteratorEdges } =
|
||||
generateNodesAndEdgesForIteratorNode({
|
||||
step,
|
||||
xPos,
|
||||
yPos: levelYPos,
|
||||
nodes,
|
||||
edges,
|
||||
workflowContext,
|
||||
});
|
||||
|
||||
nodes = iteratorNodes;
|
||||
edges = iteratorEdges;
|
||||
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
const { nodes: defaultNodes, edges: defaultEdges } =
|
||||
generateNodesAndEdgesForDefaultNode({
|
||||
step,
|
||||
xPos,
|
||||
yPos: levelYPos,
|
||||
nodes,
|
||||
edges,
|
||||
workflowContext,
|
||||
});
|
||||
|
||||
nodes = defaultNodes;
|
||||
edges = defaultEdges;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
+2
-2
@@ -42,7 +42,7 @@ export const generateWorkflowRunDiagram = ({
|
||||
const workflowDiagram = generateWorkflowDiagram({
|
||||
trigger,
|
||||
steps,
|
||||
defaultEdgeType: 'empty-filter--readonly',
|
||||
workflowContext: 'workflow-run',
|
||||
});
|
||||
|
||||
const workflowRunDiagramNodes: WorkflowRunDiagramNode[] =
|
||||
@@ -104,7 +104,7 @@ export const generateWorkflowRunDiagram = ({
|
||||
diagram: transformFilterNodesAsEdges({
|
||||
nodes: workflowRunDiagramNodes,
|
||||
edges: workflowRunDiagramEdges,
|
||||
defaultFilterEdgeType: 'filter--run',
|
||||
workflowContext: 'workflow-run',
|
||||
}),
|
||||
stepToOpenByDefault,
|
||||
};
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { type WorkflowContext } from '@/workflow/workflow-diagram/types/WorkflowContext';
|
||||
import { type WorkflowDiagramEdgeType } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
|
||||
export const getEdgeTypeBetweenTwoNodes = ({
|
||||
workflowContext,
|
||||
}: {
|
||||
workflowContext: WorkflowContext;
|
||||
}): WorkflowDiagramEdgeType => {
|
||||
switch (workflowContext) {
|
||||
case 'workflow': {
|
||||
return 'empty-filter--editable';
|
||||
}
|
||||
case 'workflow-version':
|
||||
case 'workflow-run': {
|
||||
return 'empty-filter--readonly';
|
||||
}
|
||||
}
|
||||
};
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { type WorkflowContext } from '@/workflow/workflow-diagram/types/WorkflowContext';
|
||||
import { type WorkflowDiagramEdgeType } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
|
||||
export const getFilterEdgeType = ({
|
||||
workflowContext,
|
||||
}: {
|
||||
workflowContext: WorkflowContext;
|
||||
}): WorkflowDiagramEdgeType => {
|
||||
switch (workflowContext) {
|
||||
case 'workflow': {
|
||||
return 'filter--editable';
|
||||
}
|
||||
case 'workflow-version': {
|
||||
return 'filter--readonly';
|
||||
}
|
||||
case 'workflow-run': {
|
||||
return 'filter--run';
|
||||
}
|
||||
}
|
||||
};
|
||||
+6
-18
@@ -1,8 +1,6 @@
|
||||
import { type WorkflowVersion } from '@/workflow/types/Workflow';
|
||||
import {
|
||||
type WorkflowDiagram,
|
||||
type WorkflowDiagramEdgeType,
|
||||
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { type WorkflowContext } from '@/workflow/workflow-diagram/types/WorkflowContext';
|
||||
import { type WorkflowDiagram } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { generateWorkflowDiagram } from '@/workflow/workflow-diagram/utils/generateWorkflowDiagram';
|
||||
import { transformFilterNodesAsEdges } from '@/workflow/workflow-diagram/utils/transformFilterNodesAsEdges';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
@@ -12,20 +10,12 @@ const EMPTY_DIAGRAM: WorkflowDiagram = {
|
||||
edges: [],
|
||||
};
|
||||
|
||||
const getEdgeTypeToCreateByDefault = ({
|
||||
isEditable,
|
||||
}: {
|
||||
isEditable: boolean;
|
||||
}): WorkflowDiagramEdgeType => {
|
||||
return isEditable ? 'empty-filter--editable' : 'empty-filter--readonly';
|
||||
};
|
||||
|
||||
export const getWorkflowVersionDiagram = ({
|
||||
workflowVersion,
|
||||
isEditable,
|
||||
workflowContext,
|
||||
}: {
|
||||
workflowVersion: WorkflowVersion | undefined;
|
||||
isEditable: boolean;
|
||||
workflowContext: WorkflowContext;
|
||||
}): WorkflowDiagram => {
|
||||
if (!isDefined(workflowVersion)) {
|
||||
return EMPTY_DIAGRAM;
|
||||
@@ -34,14 +24,12 @@ export const getWorkflowVersionDiagram = ({
|
||||
const diagram = generateWorkflowDiagram({
|
||||
trigger: workflowVersion.trigger ?? undefined,
|
||||
steps: workflowVersion.steps ?? [],
|
||||
defaultEdgeType: getEdgeTypeToCreateByDefault({
|
||||
isEditable,
|
||||
}),
|
||||
workflowContext,
|
||||
});
|
||||
|
||||
return transformFilterNodesAsEdges({
|
||||
nodes: diagram.nodes,
|
||||
edges: diagram.edges,
|
||||
defaultFilterEdgeType: isEditable ? 'filter--editable' : 'filter--readonly',
|
||||
workflowContext,
|
||||
});
|
||||
};
|
||||
|
||||
+7
-4
@@ -1,8 +1,9 @@
|
||||
import { type WorkflowContext } from '@/workflow/workflow-diagram/types/WorkflowContext';
|
||||
import {
|
||||
type WorkflowDiagramEdge,
|
||||
type WorkflowDiagramEdgeType,
|
||||
type WorkflowDiagramNode,
|
||||
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { getFilterEdgeType } from '@/workflow/workflow-diagram/utils/getFilterEdgeType';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const transformFilterNodesAsEdges = <
|
||||
@@ -11,11 +12,11 @@ export const transformFilterNodesAsEdges = <
|
||||
>({
|
||||
nodes,
|
||||
edges,
|
||||
defaultFilterEdgeType,
|
||||
workflowContext,
|
||||
}: {
|
||||
nodes: T[];
|
||||
edges: U[];
|
||||
defaultFilterEdgeType: WorkflowDiagramEdgeType;
|
||||
workflowContext: WorkflowContext;
|
||||
}): { nodes: T[]; edges: U[] } => {
|
||||
const filterNodes = nodes.filter(
|
||||
(node) =>
|
||||
@@ -48,9 +49,11 @@ export const transformFilterNodesAsEdges = <
|
||||
throw new Error('Expected the filter node to be of action type');
|
||||
}
|
||||
|
||||
const filterEdgeType = getFilterEdgeType({ workflowContext });
|
||||
|
||||
const newEdge: U = {
|
||||
...incomingEdge,
|
||||
type: defaultFilterEdgeType,
|
||||
type: filterEdgeType,
|
||||
id: `${incomingEdge.source}-${outgoingEdge.target}-filter-${filterNode.id}`,
|
||||
target: outgoingEdge.target,
|
||||
selectable: true,
|
||||
|
||||
+16
@@ -8,12 +8,15 @@ import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStart
|
||||
import { type WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { WorkflowDiagramBaseEdge } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramBaseEdge';
|
||||
import { WorkflowDiagramEdgeButtonGroup } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeButtonGroup';
|
||||
import { WorkflowDiagramEdgeLabel } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeLabel';
|
||||
import { WorkflowDiagramEdgeLabelContainer } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeLabelContainer';
|
||||
import { WorkflowDiagramEdgeV2Container } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeV2Container';
|
||||
import { WorkflowDiagramEdgeV2VisibilityContainer } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeV2VisibilityContainer';
|
||||
import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/workflow-edges/constants/WorkflowDiagramEdgeOptionsClickOutsideId';
|
||||
import { useEdgeState } from '@/workflow/workflow-diagram/workflow-edges/hooks/useEdgeState';
|
||||
import { useCreateStep } from '@/workflow/workflow-steps/hooks/useCreateStep';
|
||||
import { useDeleteEdge } from '@/workflow/workflow-steps/hooks/useDeleteEdge';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import {
|
||||
EdgeLabelRenderer,
|
||||
type EdgeProps,
|
||||
@@ -35,7 +38,10 @@ export const WorkflowDiagramDefaultEdgeEditable = ({
|
||||
targetY,
|
||||
markerStart,
|
||||
markerEnd,
|
||||
data,
|
||||
}: WorkflowDiagramDefaultEdgeEditableProps) => {
|
||||
const { i18n } = useLingui();
|
||||
|
||||
const { isInRightDrawer } = useContext(ActionMenuContext);
|
||||
|
||||
const { isEdgeHovered } = useEdgeState();
|
||||
@@ -116,6 +122,16 @@ export const WorkflowDiagramDefaultEdgeEditable = ({
|
||||
/>
|
||||
|
||||
<EdgeLabelRenderer>
|
||||
{isDefined(data?.labelOptions) && (
|
||||
<WorkflowDiagramEdgeLabelContainer
|
||||
sourceX={sourceX}
|
||||
sourceY={sourceY}
|
||||
position={data.labelOptions.position}
|
||||
>
|
||||
<WorkflowDiagramEdgeLabel label={i18n._(data.labelOptions.label)} />
|
||||
</WorkflowDiagramEdgeLabelContainer>
|
||||
)}
|
||||
|
||||
<WorkflowDiagramEdgeV2Container
|
||||
data-click-outside-id={WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID}
|
||||
labelX={labelX}
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { Label } from 'twenty-ui/display';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: center;
|
||||
background: ${({ theme }) => theme.background.secondary};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.strong};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
justify-content: center;
|
||||
padding-block: ${({ theme }) => theme.spacing(0.5)};
|
||||
padding-inline: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
export const WorkflowDiagramEdgeLabel = ({ label }: { label: string }) => {
|
||||
return (
|
||||
<StyledContainer>
|
||||
<Label>{label}</Label>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
import { css } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { Position } from '@xyflow/react';
|
||||
|
||||
const StyledWorkflowDiagramEdgeLabelContainer = styled.div<{
|
||||
sourceX: number;
|
||||
sourceY: number;
|
||||
position: Position;
|
||||
}>`
|
||||
position: absolute;
|
||||
width: fit-content;
|
||||
|
||||
${({ position, sourceX, sourceY }) => {
|
||||
switch (position) {
|
||||
case Position.Right: {
|
||||
return css`
|
||||
transform: translate(0%, -50%)
|
||||
translate(${Math.floor(sourceX)}px, ${sourceY}px) translateX(10px);
|
||||
`;
|
||||
}
|
||||
case Position.Bottom: {
|
||||
return css`
|
||||
transform: translate(-50%, 0%)
|
||||
translate(${sourceX}px, ${Math.floor(sourceY)}px) translateY(10px);
|
||||
`;
|
||||
}
|
||||
}
|
||||
}}
|
||||
`;
|
||||
|
||||
export { StyledWorkflowDiagramEdgeLabelContainer as WorkflowDiagramEdgeLabelContainer };
|
||||
+15
-1
@@ -20,6 +20,8 @@ import {
|
||||
import { getWorkflowDiagramColors } from '@/workflow/workflow-diagram/utils/getWorkflowDiagramColors';
|
||||
import { WorkflowDiagramBaseEdge } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramBaseEdge';
|
||||
import { WorkflowDiagramEdgeButtonGroup } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeButtonGroup';
|
||||
import { WorkflowDiagramEdgeLabel } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeLabel';
|
||||
import { WorkflowDiagramEdgeLabelContainer } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeLabelContainer';
|
||||
import { WorkflowDiagramEdgeV2Container } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeV2Container';
|
||||
import { WorkflowDiagramEdgeV2VisibilityContainer } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeV2VisibilityContainer';
|
||||
import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/workflow-edges/constants/WorkflowDiagramEdgeOptionsClickOutsideId';
|
||||
@@ -39,6 +41,7 @@ import {
|
||||
} from '@xyflow/react';
|
||||
import { useContext } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
IconDotsVertical,
|
||||
@@ -94,7 +97,8 @@ export const WorkflowDiagramFilterEdgeEditable = ({
|
||||
}: WorkflowDiagramFilterEdgeEditableProps) => {
|
||||
assertFilterEdgeDataOrThrow(data);
|
||||
|
||||
const { t } = useLingui();
|
||||
const { t, i18n } = useLingui();
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const [edgePath, labelX, labelY] = getBezierPath({
|
||||
@@ -192,6 +196,16 @@ export const WorkflowDiagramFilterEdgeEditable = ({
|
||||
/>
|
||||
|
||||
<EdgeLabelRenderer>
|
||||
{isDefined(data?.labelOptions) && (
|
||||
<WorkflowDiagramEdgeLabelContainer
|
||||
sourceX={sourceX}
|
||||
sourceY={sourceY}
|
||||
position={data.labelOptions.position}
|
||||
>
|
||||
<WorkflowDiagramEdgeLabel label={i18n._(data.labelOptions.label)} />
|
||||
</WorkflowDiagramEdgeLabelContainer>
|
||||
)}
|
||||
|
||||
<WorkflowDiagramEdgeV2Container
|
||||
data-click-outside-id={WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID}
|
||||
labelX={labelX}
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export const useStartIteratorFirstNodeCreation = () => {
|
||||
/**
|
||||
* This function is used in a context where dependencies shouldn't change much.
|
||||
* That's why its wrapped in a `useCallback` hook. Removing memoization might break the app unexpectedly.
|
||||
*/
|
||||
const startIteratorFirstNodeCreation = useCallback(() => {
|
||||
// TODO: to implement
|
||||
}, []);
|
||||
|
||||
// TODO: to implement
|
||||
const isIteratorFirstNodeCreationStarted = () => {
|
||||
return false;
|
||||
};
|
||||
|
||||
return {
|
||||
startIteratorFirstNodeCreation,
|
||||
isIteratorFirstNodeCreationStarted,
|
||||
};
|
||||
};
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
export type WorkflowDiagramIteratorEmptyActionNodeData = {
|
||||
nodeType: 'iterator-empty-action';
|
||||
parentIteratorStepId: string;
|
||||
position: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
};
|
||||
+41
-38
@@ -6,13 +6,6 @@ import { css } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { Handle, Position, type HandleProps } from '@xyflow/react';
|
||||
|
||||
type WorkflowDiagramHandleSourceProps = {
|
||||
selected: boolean;
|
||||
hovered?: boolean;
|
||||
readOnly?: boolean;
|
||||
runStatus?: WorkflowRunStepStatus;
|
||||
};
|
||||
|
||||
const HANDLE_SCALE_ON_HOVER = 1.5;
|
||||
|
||||
const StyledHandle = styled(Handle, {
|
||||
@@ -23,7 +16,7 @@ const StyledHandle = styled(Handle, {
|
||||
prop !== 'runStatus',
|
||||
})<{
|
||||
type: HandleProps['type'];
|
||||
disableHoverEffect: boolean;
|
||||
disableHoverEffect?: boolean;
|
||||
selected: boolean;
|
||||
hovered?: boolean;
|
||||
runStatus?: WorkflowRunStepStatus;
|
||||
@@ -37,7 +30,7 @@ const StyledHandle = styled(Handle, {
|
||||
if (!selected) {
|
||||
return css`
|
||||
background: ${theme.background.primary};
|
||||
border-color: ${hovered && !disableHoverEffect
|
||||
border-color: ${hovered && disableHoverEffect !== true
|
||||
? theme.font.color.light
|
||||
: theme.border.color.strong};
|
||||
`;
|
||||
@@ -56,45 +49,55 @@ const StyledHandle = styled(Handle, {
|
||||
border-color 0.1s;
|
||||
z-index: 1;
|
||||
|
||||
transform: translate(-50%, 50%);
|
||||
transform-origin: bottom left;
|
||||
${({ position }) => {
|
||||
if (position === Position.Right) {
|
||||
return css`
|
||||
transform: translate(50%, -50%);
|
||||
transform-origin: top right;
|
||||
`;
|
||||
}
|
||||
|
||||
return css`
|
||||
transform: translate(-50%, 50%);
|
||||
transform-origin: bottom left;
|
||||
`;
|
||||
}}
|
||||
|
||||
&.connectionindicator {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
${({ disableHoverEffect, theme }) => {
|
||||
if (disableHoverEffect) {
|
||||
return undefined;
|
||||
}
|
||||
&:hover {
|
||||
${({ disableHoverEffect, theme }) => {
|
||||
if (disableHoverEffect === true) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const colors = getWorkflowDiagramColors({ theme });
|
||||
const colors = getWorkflowDiagramColors({ theme });
|
||||
|
||||
return css`
|
||||
&:hover {
|
||||
return css`
|
||||
background: ${colors.selected.background} !important;
|
||||
border-color: ${colors.selected.borderColor} !important;
|
||||
transform: scale(${HANDLE_SCALE_ON_HOVER}) translate(-50%, 50%);
|
||||
`;
|
||||
}}
|
||||
|
||||
${({ disableHoverEffect, position }) => {
|
||||
if (disableHoverEffect === true) {
|
||||
return undefined;
|
||||
}
|
||||
`;
|
||||
}}
|
||||
|
||||
if (position === Position.Right) {
|
||||
return css`
|
||||
transform: scale(${HANDLE_SCALE_ON_HOVER}) translate(50%, -50%);
|
||||
`;
|
||||
}
|
||||
|
||||
return css`
|
||||
transform: scale(${HANDLE_SCALE_ON_HOVER}) translate(-50%, 50%);
|
||||
`;
|
||||
}}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const WorkflowDiagramHandleSource = ({
|
||||
selected,
|
||||
hovered = false,
|
||||
readOnly = false,
|
||||
runStatus,
|
||||
}: WorkflowDiagramHandleSourceProps) => {
|
||||
return (
|
||||
<StyledHandle
|
||||
type="source"
|
||||
position={Position.Bottom}
|
||||
disableHoverEffect={readOnly}
|
||||
selected={selected}
|
||||
hovered={hovered}
|
||||
runStatus={runStatus}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export { StyledHandle as WorkflowDiagramHandleSource };
|
||||
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
import { WORKFLOW_DIAGRAM_STEP_NODE_BASE_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramStepNodeClickOutsideId';
|
||||
import { useEdgeState } from '@/workflow/workflow-diagram/workflow-edges/hooks/useEdgeState';
|
||||
import { useStartIteratorFirstNodeCreation } from '@/workflow/workflow-diagram/workflow-iterator/hooks/useStartIteratorFirstNodeCreation';
|
||||
import { type WorkflowDiagramIteratorEmptyActionNodeData } from '@/workflow/workflow-diagram/workflow-iterator/types/WorkflowDiagramIteratorEmptyActionNodeData';
|
||||
import { WorkflowDiagramHandleSource } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramHandleSource';
|
||||
import { WorkflowDiagramHandleTarget } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramHandleTarget';
|
||||
import { WorkflowNodeContainer } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeContainer';
|
||||
import { WorkflowNodeIconContainer } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeIconContainer';
|
||||
import { WorkflowNodeLabel } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeLabel';
|
||||
import { WorkflowNodeLabelWithCounterPart } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeLabelWithCounterPart';
|
||||
import { WorkflowNodeRightPart } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeRightPart';
|
||||
import { WorkflowNodeTitle } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeTitle';
|
||||
import { useConnectionState } from '@/workflow/workflow-diagram/workflow-nodes/hooks/useConnectionState';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { Position } from '@xyflow/react';
|
||||
import { useState } from 'react';
|
||||
|
||||
export const WorkflowDiagramIteratorEmptyActionEditable = ({
|
||||
id,
|
||||
selected,
|
||||
}: {
|
||||
id: string;
|
||||
selected: boolean;
|
||||
data: WorkflowDiagramIteratorEmptyActionNodeData;
|
||||
}) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
setIsHovered(true);
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setIsHovered(false);
|
||||
};
|
||||
|
||||
const { isConnectable, isSourceConnected } = useConnectionState('action');
|
||||
|
||||
const { isSourceSelected, isSourceHovered } = useEdgeState();
|
||||
|
||||
const { startIteratorFirstNodeCreation } =
|
||||
useStartIteratorFirstNodeCreation();
|
||||
|
||||
const handleClick = () => {
|
||||
startIteratorFirstNodeCreation();
|
||||
};
|
||||
|
||||
return (
|
||||
<WorkflowNodeContainer
|
||||
data-click-outside-id={WORKFLOW_DIAGRAM_STEP_NODE_BASE_CLICK_OUTSIDE_ID}
|
||||
onClick={handleClick}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<WorkflowDiagramHandleTarget isConnectable={false} />
|
||||
|
||||
<WorkflowNodeIconContainer />
|
||||
|
||||
<WorkflowNodeRightPart>
|
||||
<WorkflowNodeLabelWithCounterPart>
|
||||
<WorkflowNodeLabel>{t`Action`}</WorkflowNodeLabel>
|
||||
</WorkflowNodeLabelWithCounterPart>
|
||||
|
||||
<WorkflowNodeTitle>{t`Add an Action`}</WorkflowNodeTitle>
|
||||
</WorkflowNodeRightPart>
|
||||
|
||||
<WorkflowDiagramHandleSource
|
||||
type="source"
|
||||
position={Position.Bottom}
|
||||
disableHoverEffect
|
||||
selected={
|
||||
isSourceSelected(id) ||
|
||||
selected ||
|
||||
isSourceConnected(id) ||
|
||||
(isConnectable(id) && isHovered)
|
||||
}
|
||||
hovered={isSourceHovered(id) || isHovered}
|
||||
/>
|
||||
</WorkflowNodeContainer>
|
||||
);
|
||||
};
|
||||
+35
-2
@@ -2,6 +2,7 @@ import { WorkflowDiagramCreateStepElement } from '@/workflow/workflow-diagram/co
|
||||
import { WORKFLOW_DIAGRAM_STEP_NODE_BASE_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/constants/WorkflowDiagramStepNodeClickOutsideId';
|
||||
import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStartNodeCreation';
|
||||
import { type WorkflowDiagramStepNodeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { WorkflowDiagramEdgeLabel } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeLabel';
|
||||
import { useEdgeState } from '@/workflow/workflow-diagram/workflow-edges/hooks/useEdgeState';
|
||||
import { WorkflowDiagramHandleSource } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramHandleSource';
|
||||
import { WorkflowDiagramHandleTarget } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramHandleTarget';
|
||||
@@ -14,8 +15,10 @@ import { WorkflowNodeRightPart } from '@/workflow/workflow-diagram/workflow-node
|
||||
import { WorkflowNodeTitle } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeTitle';
|
||||
import { useConnectionState } from '@/workflow/workflow-diagram/workflow-nodes/hooks/useConnectionState';
|
||||
import styled from '@emotion/styled';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { Position } from '@xyflow/react';
|
||||
import { useState } from 'react';
|
||||
import { capitalize } from 'twenty-shared/utils';
|
||||
import { capitalize, isDefined } from 'twenty-shared/utils';
|
||||
import { IconTrash } from 'twenty-ui/display';
|
||||
import { FloatingIconButton } from 'twenty-ui/input';
|
||||
|
||||
@@ -56,6 +59,8 @@ export const WorkflowDiagramStepNodeEditableContent = ({
|
||||
onDelete: () => void;
|
||||
onClick?: () => void;
|
||||
}) => {
|
||||
const { i18n } = useLingui();
|
||||
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
@@ -89,6 +94,7 @@ export const WorkflowDiagramStepNodeEditableContent = ({
|
||||
isConnectable={isConnectable(id)}
|
||||
>
|
||||
<WorkflowDiagramHandleTarget isConnectable={isConnectable(id)} />
|
||||
|
||||
<WorkflowNodeIconContainer>
|
||||
<WorkflowDiagramStepNodeIcon data={data} />
|
||||
</WorkflowNodeIconContainer>
|
||||
@@ -123,11 +129,22 @@ export const WorkflowDiagramStepNodeEditableContent = ({
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onClick={handleAddStepButtonContainerClick}
|
||||
>
|
||||
<WorkflowDiagramCreateStepElement data={data} />
|
||||
<WorkflowDiagramCreateStepElement
|
||||
data={data}
|
||||
Label={
|
||||
isDefined(data.defaultHandleOptions?.label) ? (
|
||||
<WorkflowDiagramEdgeLabel
|
||||
label={i18n._(data.defaultHandleOptions.label)}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
</StyledAddStepButtonContainer>
|
||||
)}
|
||||
|
||||
<WorkflowDiagramHandleSource
|
||||
type="source"
|
||||
position={Position.Bottom}
|
||||
selected={
|
||||
isSourceSelected(id) ||
|
||||
selected ||
|
||||
@@ -136,6 +153,22 @@ export const WorkflowDiagramStepNodeEditableContent = ({
|
||||
}
|
||||
hovered={isSourceHovered(id) || isHovered}
|
||||
/>
|
||||
|
||||
{isDefined(data.rightHandleOptions) && (
|
||||
<WorkflowDiagramHandleSource
|
||||
id={data.rightHandleOptions.id}
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
selected={
|
||||
isSourceSelected(id) ||
|
||||
selected ||
|
||||
isSourceConnected(id) ||
|
||||
(isConnectable(id) && isHovered)
|
||||
}
|
||||
// TODO: fix hovered state when multiple source handles
|
||||
hovered={isSourceHovered(id) || isHovered}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+9
-3
@@ -9,6 +9,8 @@ import { WORKFLOW_DIAGRAM_STEP_NODE_BASE_CLICK_OUTSIDE_ID } from '@/workflow/wor
|
||||
import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState';
|
||||
import { type WorkflowDiagramStepNodeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { getWorkflowNodeIconKey } from '@/workflow/workflow-diagram/utils/getWorkflowNodeIconKey';
|
||||
import { WorkflowDiagramHandleSource } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramHandleSource';
|
||||
import { WorkflowDiagramHandleTarget } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramHandleTarget';
|
||||
import { WorkflowDiagramStepNodeIcon } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeIcon';
|
||||
import { WorkflowNodeContainer } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeContainer';
|
||||
import { WorkflowNodeIconContainer } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeIconContainer';
|
||||
@@ -16,12 +18,11 @@ import { WorkflowNodeLabel } from '@/workflow/workflow-diagram/workflow-nodes/co
|
||||
import { WorkflowNodeLabelWithCounterPart } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeLabelWithCounterPart';
|
||||
import { WorkflowNodeRightPart } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeRightPart';
|
||||
import { WorkflowNodeTitle } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeTitle';
|
||||
import { Position } from '@xyflow/react';
|
||||
import { useContext } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { capitalize, isDefined } from 'twenty-shared/utils';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
import { WorkflowDiagramHandleTarget } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramHandleTarget';
|
||||
import { WorkflowDiagramHandleSource } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramHandleSource';
|
||||
|
||||
export const WorkflowDiagramStepNodeReadonly = ({
|
||||
id,
|
||||
@@ -95,7 +96,12 @@ export const WorkflowDiagramStepNodeReadonly = ({
|
||||
</WorkflowNodeRightPart>
|
||||
</WorkflowNodeContainer>
|
||||
|
||||
<WorkflowDiagramHandleSource selected={selected} readOnly />
|
||||
<WorkflowDiagramHandleSource
|
||||
type="source"
|
||||
selected={selected}
|
||||
position={Position.Bottom}
|
||||
disableHoverEffect
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+7
-4
@@ -9,6 +9,8 @@ import { WORKFLOW_DIAGRAM_STEP_NODE_BASE_CLICK_OUTSIDE_ID } from '@/workflow/wor
|
||||
import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState';
|
||||
import { type WorkflowRunDiagramStepNodeData } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { getWorkflowNodeIconKey } from '@/workflow/workflow-diagram/utils/getWorkflowNodeIconKey';
|
||||
import { WorkflowDiagramHandleSource } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramHandleSource';
|
||||
import { WorkflowDiagramHandleTarget } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramHandleTarget';
|
||||
import { WorkflowDiagramStepNodeIcon } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramStepNodeIcon';
|
||||
import { WorkflowNodeContainer } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeContainer';
|
||||
import { WorkflowNodeIconContainer } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeIconContainer';
|
||||
@@ -18,14 +20,13 @@ import { WorkflowNodeRightPart } from '@/workflow/workflow-diagram/workflow-node
|
||||
import { WorkflowNodeTitle } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowNodeTitle';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { Position } from '@xyflow/react';
|
||||
import { useContext } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { capitalize, isDefined } from 'twenty-shared/utils';
|
||||
import { StepStatus } from 'twenty-shared/workflow';
|
||||
import { IconCheck, IconX, useIcons } from 'twenty-ui/display';
|
||||
import { Loader } from 'twenty-ui/feedback';
|
||||
import { WorkflowDiagramHandleTarget } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramHandleTarget';
|
||||
import { WorkflowDiagramHandleSource } from '@/workflow/workflow-diagram/workflow-nodes/components/WorkflowDiagramHandleSource';
|
||||
|
||||
const StyledNodeLabelWithCounterPart = styled(WorkflowNodeLabelWithCounterPart)`
|
||||
column-gap: ${({ theme }) => theme.spacing(2)};
|
||||
@@ -151,9 +152,11 @@ export const WorkflowRunDiagramStepNode = ({
|
||||
</WorkflowNodeContainer>
|
||||
|
||||
<WorkflowDiagramHandleSource
|
||||
runStatus={data.runStatus}
|
||||
type="source"
|
||||
position={Position.Bottom}
|
||||
disableHoverEffect
|
||||
selected={selected}
|
||||
readOnly
|
||||
runStatus={data.runStatus}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
export const WORKFLOW_DIAGRAM_ITERATOR_NODE_LOOP_HANDLE_ID = 'loop';
|
||||
+3
-3
@@ -1,9 +1,9 @@
|
||||
import { type WorkflowActionType } from '@/workflow/types/Workflow';
|
||||
import { getActionHeaderTypeOrThrow } from '@/workflow/workflow-steps/workflow-actions/utils/getActionHeaderTypeOrThrow';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
|
||||
export const useActionHeaderTypeOrThrow = (actionType: WorkflowActionType) => {
|
||||
const { _ } = useLingui();
|
||||
const { i18n } = useLingui();
|
||||
|
||||
return _(getActionHeaderTypeOrThrow(actionType));
|
||||
return i18n._(getActionHeaderTypeOrThrow(actionType));
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user