Allow deleting any branch via keyboard (#13897)
The heart of the fixing is making edges selectable and deletable in `packages/twenty-front/src/modules/workflow/workflow-diagram/utils/transformFilterNodesAsEdges.ts`. ## When Branches are enabled https://github.com/user-attachments/assets/7477c556-d593-4235-8312-e830847192ca ## When Branches are disabled https://github.com/user-attachments/assets/445c98b7-2370-4d60-b2e8-e47b05e8d8a2
This commit is contained in:
+5
@@ -30,6 +30,9 @@ export const useRunWorkflowRunOpeningInCommandMenuSideEffects = () => {
|
||||
const isWorkflowFilteringEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_WORKFLOW_FILTERING_ENABLED,
|
||||
);
|
||||
const isWorkflowBranchEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_WORKFLOW_BRANCH_ENABLED,
|
||||
);
|
||||
|
||||
const runWorkflowRunOpeningInCommandMenuSideEffects = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
@@ -63,6 +66,7 @@ export const useRunWorkflowRunOpeningInCommandMenuSideEffects = () => {
|
||||
stepInfos: workflowRunRecord.state.stepInfos,
|
||||
trigger: workflowRunRecord.state.flow.trigger,
|
||||
isWorkflowFilteringEnabled,
|
||||
isWorkflowBranchEnabled,
|
||||
});
|
||||
|
||||
if (!isDefined(stepToOpenByDefault)) {
|
||||
@@ -133,6 +137,7 @@ export const useRunWorkflowRunOpeningInCommandMenuSideEffects = () => {
|
||||
apolloCoreClient.cache,
|
||||
objectPermissionsByObjectMetadataId,
|
||||
isWorkflowFilteringEnabled,
|
||||
isWorkflowBranchEnabled,
|
||||
openWorkflowRunViewStepInCommandMenu,
|
||||
getIcon,
|
||||
],
|
||||
|
||||
+23
-11
@@ -38,6 +38,7 @@ import {
|
||||
type NodeChange,
|
||||
type NodeProps,
|
||||
type OnBeforeDelete,
|
||||
type OnDelete,
|
||||
type OnNodeDrag,
|
||||
} from '@xyflow/react';
|
||||
import '@xyflow/react/dist/style.css';
|
||||
@@ -417,18 +418,30 @@ export const WorkflowDiagramCanvasBase = ({
|
||||
const onBeforeDelete: OnBeforeDelete<
|
||||
WorkflowDiagramNode,
|
||||
WorkflowDiagramEdge
|
||||
> = async (diagram) => {
|
||||
if (
|
||||
diagram.nodes.length === 0 // We don't call deleteEdge when node diagram deletion is called
|
||||
) {
|
||||
for (const edge of diagram.edges) {
|
||||
onDeleteEdge?.(edge);
|
||||
}
|
||||
return diagram;
|
||||
> = async ({ nodes, edges }) => {
|
||||
if (!isWorkflowBranchEnabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (nodes.length === 0 && edges.length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const onDelete: OnDelete<WorkflowDiagramNode, WorkflowDiagramEdge> = async ({
|
||||
edges,
|
||||
}) => {
|
||||
if (!isWorkflowBranchEnabled || !isDefined(onDeleteEdge)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const edge of edges) {
|
||||
onDeleteEdge(edge);
|
||||
}
|
||||
};
|
||||
|
||||
const onPaneContextMenu = useCallback(
|
||||
(event: MouseEvent | React.MouseEvent<Element, MouseEvent>) => {
|
||||
event.preventDefault();
|
||||
@@ -474,9 +487,8 @@ export const WorkflowDiagramCanvasBase = ({
|
||||
onEdgesChange={handleEdgesChange}
|
||||
onConnect={isWorkflowBranchEnabled ? onConnect : undefined}
|
||||
onNodeDragStop={isWorkflowBranchEnabled ? onNodeDragStop : undefined}
|
||||
onBeforeDelete={
|
||||
isWorkflowBranchEnabled ? onBeforeDelete : async () => false
|
||||
}
|
||||
onBeforeDelete={onBeforeDelete}
|
||||
onDelete={onDelete}
|
||||
selectNodesOnDrag={false}
|
||||
proOptions={{ hideAttribution: true }}
|
||||
multiSelectionKeyCode={null}
|
||||
|
||||
+5
@@ -68,6 +68,9 @@ export const WorkflowRunVisualizerEffect = ({
|
||||
const isWorkflowFilteringEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_WORKFLOW_FILTERING_ENABLED,
|
||||
);
|
||||
const isWorkflowBranchEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_WORKFLOW_BRANCH_ENABLED,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setWorkflowRunId(workflowRunId);
|
||||
@@ -120,6 +123,7 @@ export const WorkflowRunVisualizerEffect = ({
|
||||
steps: workflowRunState.flow.steps,
|
||||
stepInfos: workflowRunState.stepInfos,
|
||||
isWorkflowFilteringEnabled,
|
||||
isWorkflowBranchEnabled,
|
||||
});
|
||||
|
||||
if (workflowDiagramStatus !== 'done') {
|
||||
@@ -190,6 +194,7 @@ export const WorkflowRunVisualizerEffect = ({
|
||||
[
|
||||
flowState,
|
||||
getIcon,
|
||||
isWorkflowBranchEnabled,
|
||||
isWorkflowFilteringEnabled,
|
||||
openWorkflowRunViewStepInCommandMenu,
|
||||
workflowDiagramState,
|
||||
|
||||
+5
@@ -105,6 +105,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
steps,
|
||||
stepInfos,
|
||||
isWorkflowFilteringEnabled: true,
|
||||
isWorkflowBranchEnabled: true,
|
||||
});
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
@@ -334,6 +335,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
steps,
|
||||
stepInfos,
|
||||
isWorkflowFilteringEnabled: true,
|
||||
isWorkflowBranchEnabled: true,
|
||||
});
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
@@ -563,6 +565,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
steps,
|
||||
stepInfos,
|
||||
isWorkflowFilteringEnabled: true,
|
||||
isWorkflowBranchEnabled: true,
|
||||
});
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
@@ -811,6 +814,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
steps,
|
||||
stepInfos,
|
||||
isWorkflowFilteringEnabled: true,
|
||||
isWorkflowBranchEnabled: true,
|
||||
});
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
@@ -1039,6 +1043,7 @@ describe('generateWorkflowRunDiagram', () => {
|
||||
steps,
|
||||
stepInfos,
|
||||
isWorkflowFilteringEnabled: true,
|
||||
isWorkflowBranchEnabled: true,
|
||||
});
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
|
||||
+95
-4
@@ -46,6 +46,7 @@ describe('transformFilterNodesAsEdges', () => {
|
||||
nodes: diagram.nodes,
|
||||
edges: diagram.edges,
|
||||
defaultFilterEdgeType: 'filter--editable',
|
||||
isWorkflowBranchEnabled: true,
|
||||
});
|
||||
|
||||
expect(result.nodes).toEqual(diagram.nodes);
|
||||
@@ -112,6 +113,7 @@ describe('transformFilterNodesAsEdges', () => {
|
||||
nodes: diagram.nodes,
|
||||
edges: diagram.edges,
|
||||
defaultFilterEdgeType: 'filter--editable',
|
||||
isWorkflowBranchEnabled: true,
|
||||
});
|
||||
|
||||
// Should only have nodes A and C
|
||||
@@ -149,7 +151,8 @@ describe('transformFilterNodesAsEdges', () => {
|
||||
type: 'filter--editable',
|
||||
source: 'A',
|
||||
target: 'C',
|
||||
deletable: false,
|
||||
selectable: true,
|
||||
deletable: true,
|
||||
data: {
|
||||
edgeType: 'filter',
|
||||
stepId: 'B',
|
||||
@@ -256,6 +259,7 @@ describe('transformFilterNodesAsEdges', () => {
|
||||
nodes: diagram.nodes,
|
||||
edges: diagram.edges,
|
||||
defaultFilterEdgeType: 'filter--editable',
|
||||
isWorkflowBranchEnabled: true,
|
||||
});
|
||||
|
||||
// Should only have nodes A, C, and D
|
||||
@@ -275,7 +279,8 @@ describe('transformFilterNodesAsEdges', () => {
|
||||
type: 'filter--editable',
|
||||
source: 'A',
|
||||
target: 'C',
|
||||
deletable: false,
|
||||
selectable: true,
|
||||
deletable: true,
|
||||
data: {
|
||||
edgeType: 'filter',
|
||||
name: 'Filter B1',
|
||||
@@ -293,7 +298,8 @@ describe('transformFilterNodesAsEdges', () => {
|
||||
type: 'filter--editable',
|
||||
source: 'C',
|
||||
target: 'D',
|
||||
deletable: false,
|
||||
selectable: true,
|
||||
deletable: true,
|
||||
data: {
|
||||
edgeType: 'filter',
|
||||
name: 'Filter B2',
|
||||
@@ -346,6 +352,7 @@ describe('transformFilterNodesAsEdges', () => {
|
||||
nodes: diagram.nodes,
|
||||
edges: diagram.edges,
|
||||
defaultFilterEdgeType: 'filter--editable',
|
||||
isWorkflowBranchEnabled: true,
|
||||
});
|
||||
|
||||
// Should only have node A (filter node B is removed)
|
||||
@@ -428,6 +435,7 @@ describe('transformFilterNodesAsEdges', () => {
|
||||
nodes: diagram.nodes,
|
||||
edges: diagram.edges,
|
||||
defaultFilterEdgeType: 'filter--editable',
|
||||
isWorkflowBranchEnabled: true,
|
||||
});
|
||||
|
||||
// Should have trigger and C nodes
|
||||
@@ -465,7 +473,8 @@ describe('transformFilterNodesAsEdges', () => {
|
||||
type: 'filter--editable',
|
||||
source: 'trigger',
|
||||
target: 'C',
|
||||
deletable: false,
|
||||
selectable: true,
|
||||
deletable: true,
|
||||
data: {
|
||||
edgeType: 'filter',
|
||||
name: 'Filter B',
|
||||
@@ -476,4 +485,86 @@ describe('transformFilterNodesAsEdges', () => {
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should set selectable and deletable to false when isWorkflowBranchEnabled is false', () => {
|
||||
const diagram: WorkflowDiagram = {
|
||||
nodes: [
|
||||
{
|
||||
id: 'A',
|
||||
data: {
|
||||
nodeType: 'action',
|
||||
actionType: 'CODE',
|
||||
name: 'Step A',
|
||||
hasNextStepIds: true,
|
||||
position: { x: 0, y: 0 },
|
||||
stepId: 'A',
|
||||
},
|
||||
position: { x: 0, y: 0 },
|
||||
},
|
||||
{
|
||||
id: 'B',
|
||||
data: {
|
||||
nodeType: 'action',
|
||||
actionType: 'FILTER',
|
||||
name: 'Filter B',
|
||||
hasNextStepIds: true,
|
||||
position: { x: 0, y: 150 },
|
||||
stepId: 'B',
|
||||
},
|
||||
position: { x: 0, y: 150 },
|
||||
},
|
||||
{
|
||||
id: 'C',
|
||||
data: {
|
||||
nodeType: 'action',
|
||||
actionType: 'SEND_EMAIL',
|
||||
name: 'Step C',
|
||||
hasNextStepIds: false,
|
||||
position: { x: 0, y: 300 },
|
||||
stepId: 'C',
|
||||
},
|
||||
position: { x: 0, y: 300 },
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
id: 'A-B',
|
||||
source: 'A',
|
||||
target: 'B',
|
||||
data: { edgeType: 'default' },
|
||||
},
|
||||
{
|
||||
id: 'B-C',
|
||||
source: 'B',
|
||||
target: 'C',
|
||||
data: { edgeType: 'default' },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = transformFilterNodesAsEdges({
|
||||
nodes: diagram.nodes,
|
||||
edges: diagram.edges,
|
||||
defaultFilterEdgeType: 'filter--editable',
|
||||
isWorkflowBranchEnabled: false,
|
||||
});
|
||||
|
||||
// Should have one edge with filter data
|
||||
expect(result.edges).toHaveLength(1);
|
||||
expect(result.edges[0]).toEqual({
|
||||
id: 'A-C-filter-B',
|
||||
type: 'filter--editable',
|
||||
source: 'A',
|
||||
target: 'C',
|
||||
selectable: false,
|
||||
deletable: false,
|
||||
data: {
|
||||
edgeType: 'filter',
|
||||
stepId: 'B',
|
||||
name: 'Filter B',
|
||||
runStatus: undefined,
|
||||
filterSettings: {},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+3
@@ -20,11 +20,13 @@ export const generateWorkflowRunDiagram = ({
|
||||
steps,
|
||||
stepInfos,
|
||||
isWorkflowFilteringEnabled,
|
||||
isWorkflowBranchEnabled,
|
||||
}: {
|
||||
trigger: WorkflowTrigger;
|
||||
steps: Array<WorkflowStep>;
|
||||
stepInfos: WorkflowRunStepInfos | undefined;
|
||||
isWorkflowFilteringEnabled: boolean;
|
||||
isWorkflowBranchEnabled: boolean;
|
||||
}): {
|
||||
diagram: WorkflowRunDiagram;
|
||||
stepToOpenByDefault:
|
||||
@@ -119,6 +121,7 @@ export const generateWorkflowRunDiagram = ({
|
||||
nodes: workflowRunDiagramNodes,
|
||||
edges: workflowRunDiagramEdges,
|
||||
defaultFilterEdgeType: 'filter--run',
|
||||
isWorkflowBranchEnabled,
|
||||
}),
|
||||
stepToOpenByDefault,
|
||||
};
|
||||
|
||||
+2
-1
@@ -4,8 +4,8 @@ import {
|
||||
type WorkflowDiagramEdgeType,
|
||||
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
|
||||
import { generateWorkflowDiagram } from '@/workflow/workflow-diagram/utils/generateWorkflowDiagram';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { transformFilterNodesAsEdges } from '@/workflow/workflow-diagram/utils/transformFilterNodesAsEdges';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const EMPTY_DIAGRAM: WorkflowDiagram = {
|
||||
nodes: [],
|
||||
@@ -57,5 +57,6 @@ export const getWorkflowVersionDiagram = ({
|
||||
nodes: diagram.nodes,
|
||||
edges: diagram.edges,
|
||||
defaultFilterEdgeType: isEditable ? 'filter--editable' : 'filter--readonly',
|
||||
isWorkflowBranchEnabled: isWorkflowBranchEnabled === true,
|
||||
});
|
||||
};
|
||||
|
||||
+4
-1
@@ -12,10 +12,12 @@ export const transformFilterNodesAsEdges = <
|
||||
nodes,
|
||||
edges,
|
||||
defaultFilterEdgeType,
|
||||
isWorkflowBranchEnabled,
|
||||
}: {
|
||||
nodes: T[];
|
||||
edges: U[];
|
||||
defaultFilterEdgeType: WorkflowDiagramEdgeType;
|
||||
isWorkflowBranchEnabled: boolean;
|
||||
}): { nodes: T[]; edges: U[] } => {
|
||||
const filterNodes = nodes.filter(
|
||||
(node) =>
|
||||
@@ -53,7 +55,8 @@ export const transformFilterNodesAsEdges = <
|
||||
type: defaultFilterEdgeType,
|
||||
id: `${incomingEdge.source}-${outgoingEdge.target}-filter-${filterNode.id}`,
|
||||
target: outgoingEdge.target,
|
||||
deletable: false,
|
||||
selectable: isWorkflowBranchEnabled === true,
|
||||
deletable: isWorkflowBranchEnabled === true,
|
||||
data: {
|
||||
...incomingEdge.data,
|
||||
edgeType: 'filter',
|
||||
|
||||
Reference in New Issue
Block a user