Skip to content

Commit 168eb8d

Browse files
committed
Allow to move nodes by drag & drop
1 parent 98f824c commit 168eb8d

File tree

36 files changed

+1244
-107
lines changed

36 files changed

+1244
-107
lines changed

backend/Makefile

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,10 +177,16 @@ test-coverage: .check-dev
177177
$(VPYTEST) -n 10 --cov-report term --cov-report html:reports/html --cov=src $(BACKEND_TESTS_DIRS) || exit;
178178

179179
test-builder: .check-dev
180-
$(VPYTEST) tests/baserow/contrib/builder || exit
180+
$(VPYTEST) tests/baserow/contrib/integrations tests/baserow/contrib/builder || exit
181181

182182
test-builder-parallel: .check-dev
183-
$(VPYTEST) tests/baserow/contrib/builder -n 10 || exit
183+
$(VPYTEST) tests/baserow/contrib/integrations tests/baserow/contrib/builder -n 10 || exit
184+
185+
test-automation: .check-dev
186+
$(VPYTEST) tests/baserow/contrib/integrations tests/baserow/contrib/automation || exit
187+
188+
test-automation-parallel: .check-dev
189+
$(VPYTEST) tests/baserow/contrib/integrations tests/baserow/contrib/automation -n 10 || exit
184190

185191
test-regenerate-ci-durations: .check-dev
186192
$(VPYTEST) $(BACKEND_TESTS_DIRS) --store-durations || exit;

backend/src/baserow/contrib/automation/api/nodes/errors.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,9 @@
4848
HTTP_400_BAD_REQUEST,
4949
"Failed to simulate dispatch: {e}",
5050
)
51+
52+
ERROR_AUTOMATION_NODE_NOT_MOVABLE = (
53+
"ERROR_AUTOMATION_NODE_NOT_MOVABLE",
54+
HTTP_400_BAD_REQUEST,
55+
"{e}",
56+
)

backend/src/baserow/contrib/automation/api/nodes/serializers.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,15 @@ class ReplaceAutomationNodeSerializer(serializers.Serializer):
110110
required=True,
111111
help_text="The type of the new automation node",
112112
)
113+
114+
115+
class MoveAutomationNodeSerializer(serializers.Serializer):
116+
previous_node_id = serializers.IntegerField(
117+
required=False,
118+
help_text="The ID of the node that should be before the moved node.",
119+
)
120+
previous_node_output = serializers.CharField(
121+
required=False,
122+
allow_blank=True,
123+
help_text="The output UID of the destination.",
124+
)

backend/src/baserow/contrib/automation/api/nodes/urls.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
AutomationNodesView,
55
AutomationNodeView,
66
DuplicateAutomationNodeView,
7+
MoveAutomationNodeView,
78
OrderAutomationNodesView,
89
ReplaceAutomationNodeView,
910
SimulateDispatchAutomationNodeView,
@@ -42,4 +43,9 @@
4243
SimulateDispatchAutomationNodeView.as_view(),
4344
name="simulate_dispatch",
4445
),
46+
re_path(
47+
r"node/(?P<node_id>[0-9]+)/move/$",
48+
MoveAutomationNodeView.as_view(),
49+
name="move",
50+
),
4551
]

backend/src/baserow/contrib/automation/api/nodes/views.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,15 @@
2121
ERROR_AUTOMATION_NODE_MISCONFIGURED_SERVICE,
2222
ERROR_AUTOMATION_NODE_NOT_DELETABLE,
2323
ERROR_AUTOMATION_NODE_NOT_IN_WORKFLOW,
24+
ERROR_AUTOMATION_NODE_NOT_MOVABLE,
2425
ERROR_AUTOMATION_NODE_NOT_REPLACEABLE,
2526
ERROR_AUTOMATION_NODE_SIMULATE_DISPATCH,
2627
ERROR_AUTOMATION_TRIGGER_NODE_MODIFICATION_DISALLOWED,
2728
)
2829
from baserow.contrib.automation.api.nodes.serializers import (
2930
AutomationNodeSerializer,
3031
CreateAutomationNodeSerializer,
32+
MoveAutomationNodeSerializer,
3133
OrderAutomationNodesSerializer,
3234
ReplaceAutomationNodeSerializer,
3335
UpdateAutomationNodeSerializer,
@@ -39,6 +41,7 @@
3941
CreateAutomationNodeActionType,
4042
DeleteAutomationNodeActionType,
4143
DuplicateAutomationNodeActionType,
44+
MoveAutomationNodeActionType,
4245
OrderAutomationNodesActionType,
4346
ReplaceAutomationNodeActionType,
4447
UpdateAutomationNodeActionType,
@@ -49,6 +52,7 @@
4952
AutomationNodeMisconfiguredService,
5053
AutomationNodeNotDeletable,
5154
AutomationNodeNotInWorkflow,
55+
AutomationNodeNotMovable,
5256
AutomationNodeNotReplaceable,
5357
AutomationNodeSimulateDispatchError,
5458
AutomationTriggerModificationDisallowed,
@@ -439,3 +443,50 @@ def post(self, request, node_id: int):
439443
)
440444

441445
return Response(serializer.data)
446+
447+
448+
class MoveAutomationNodeView(APIView):
449+
permission_classes = (IsAuthenticated,)
450+
451+
@extend_schema(
452+
parameters=[
453+
OpenApiParameter(
454+
name="node_id",
455+
location=OpenApiParameter.PATH,
456+
type=OpenApiTypes.INT,
457+
description="The node that is to be moved.",
458+
),
459+
CLIENT_SESSION_ID_SCHEMA_PARAMETER,
460+
],
461+
tags=[AUTOMATION_NODES_TAG],
462+
operation_id="move_automation_node",
463+
description="Move a node in a workflow to a new position.",
464+
request=MoveAutomationNodeSerializer,
465+
responses={
466+
200: DiscriminatorCustomFieldsMappingSerializer(
467+
automation_node_type_registry, AutomationNodeSerializer
468+
),
469+
400: get_error_schema(["ERROR_AUTOMATION_NODE_NOT_MOVABLE"]),
470+
404: get_error_schema(["ERROR_AUTOMATION_NODE_DOES_NOT_EXIST"]),
471+
},
472+
)
473+
@transaction.atomic
474+
@map_exceptions(
475+
{
476+
AutomationNodeDoesNotExist: ERROR_AUTOMATION_NODE_DOES_NOT_EXIST,
477+
AutomationNodeNotMovable: ERROR_AUTOMATION_NODE_NOT_MOVABLE,
478+
}
479+
)
480+
@validate_body(MoveAutomationNodeSerializer)
481+
def post(self, request, data: Dict, node_id: int):
482+
moved_node = MoveAutomationNodeActionType.do(
483+
request.user,
484+
node_id,
485+
data["previous_node_id"],
486+
data["previous_node_output"],
487+
)
488+
return Response(
489+
automation_node_type_registry.get_serializer(
490+
moved_node, AutomationNodeSerializer
491+
).data
492+
)

backend/src/baserow/contrib/automation/apps.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ def ready(self):
1515
CreateAutomationNodeActionType,
1616
DeleteAutomationNodeActionType,
1717
DuplicateAutomationNodeActionType,
18+
MoveAutomationNodeActionType,
1819
OrderAutomationNodesActionType,
1920
ReplaceAutomationNodeActionType,
2021
UpdateAutomationNodeActionType,
@@ -145,6 +146,7 @@ def ready(self):
145146
action_type_registry.register(OrderAutomationNodesActionType())
146147
action_type_registry.register(DuplicateAutomationNodeActionType())
147148
action_type_registry.register(ReplaceAutomationNodeActionType())
149+
action_type_registry.register(MoveAutomationNodeActionType())
148150

149151
action_scope_registry.register(WorkflowActionScopeType())
150152

backend/src/baserow/contrib/automation/nodes/actions.py

Lines changed: 121 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from dataclasses import dataclass
2-
from typing import Any, List
2+
from typing import Any, List, Optional
33

44
from django.contrib.auth.models import AbstractUser
55
from django.utils.translation import gettext_lazy as _
@@ -10,7 +10,7 @@
1010
)
1111
from baserow.contrib.automation.actions import AUTOMATION_WORKFLOW_CONTEXT
1212
from baserow.contrib.automation.nodes.handler import AutomationNodeHandler
13-
from baserow.contrib.automation.nodes.models import AutomationNode
13+
from baserow.contrib.automation.nodes.models import AutomationActionNode, AutomationNode
1414
from baserow.contrib.automation.nodes.node_types import AutomationNodeType
1515
from baserow.contrib.automation.nodes.registries import (
1616
ReplaceAutomationNodeTrashOperationType,
@@ -483,3 +483,122 @@ def redo(
483483
deleted_node=deleted_node,
484484
user=user,
485485
)
486+
487+
488+
class MoveAutomationNodeActionType(UndoableActionType):
489+
type = "move_automation_node"
490+
description = ActionTypeDescription(
491+
_("Moved automation node"),
492+
_("Node (%(node_id)s) moved"),
493+
NODE_ACTION_CONTEXT,
494+
)
495+
496+
@dataclass
497+
class Params:
498+
automation_id: int
499+
automation_name: str
500+
workflow_id: int
501+
node_id: int
502+
node_type: str
503+
origin_previous_node_id: int
504+
origin_previous_node_output: str
505+
origin_new_next_nodes_values: List[NextAutomationNodeValues]
506+
origin_old_next_nodes_values: List[NextAutomationNodeValues]
507+
destination_previous_node_id: int
508+
destination_previous_node_output: str
509+
destination_new_next_nodes_values: List[NextAutomationNodeValues]
510+
destination_old_next_nodes_values: List[NextAutomationNodeValues]
511+
512+
@classmethod
513+
def do(
514+
cls,
515+
user: AbstractUser,
516+
node_id: int,
517+
new_previous_node_id: int,
518+
new_previous_node_output: Optional[str] = None,
519+
) -> AutomationActionNode:
520+
move = AutomationNodeService().move_node(
521+
user, node_id, new_previous_node_id, new_previous_node_output
522+
)
523+
workflow = move.node.workflow
524+
cls.register_action(
525+
user=user,
526+
params=cls.Params(
527+
workflow.automation_id,
528+
workflow.automation.name,
529+
workflow.id,
530+
move.node.id,
531+
move.node.get_type().type,
532+
move.origin_previous_node_id,
533+
move.origin_previous_node_output,
534+
move.origin_new_next_nodes_values,
535+
move.origin_old_next_nodes_values,
536+
move.destination_previous_node_id,
537+
move.destination_previous_node_output,
538+
move.destination_new_next_nodes_values,
539+
move.destination_old_next_nodes_values,
540+
),
541+
scope=cls.scope(workflow.id),
542+
workspace=workflow.automation.workspace,
543+
)
544+
return move.node
545+
546+
@classmethod
547+
def scope(cls, workflow_id):
548+
return WorkflowActionScopeType.value(workflow_id)
549+
550+
@classmethod
551+
def undo(
552+
cls,
553+
user: AbstractUser,
554+
params: Params,
555+
action_to_undo: Action,
556+
):
557+
# Revert the node to its original position & output (if applicable).
558+
AutomationNodeService().update_node(
559+
user,
560+
params.node_id,
561+
previous_node_id=params.origin_previous_node_id,
562+
previous_node_output=params.origin_previous_node_output,
563+
)
564+
565+
# Pluck out the workflow, we need it to send our signals for next nodes.
566+
workflow = AutomationWorkflowService().get_workflow(user, params.workflow_id)
567+
568+
# Revert the origin's next nodes back to their original position.
569+
AutomationNodeService().update_next_nodes_values(
570+
user, params.origin_old_next_nodes_values, workflow
571+
)
572+
573+
# Revert the destination's next nodes back to their original position.
574+
AutomationNodeService().update_next_nodes_values(
575+
user, params.destination_old_next_nodes_values, workflow
576+
)
577+
578+
@classmethod
579+
def redo(
580+
cls,
581+
user: AbstractUser,
582+
params: Params,
583+
action_to_redo: Action,
584+
):
585+
# Set the node to its new position & output (if applicable).
586+
AutomationNodeService().update_node(
587+
user,
588+
params.node_id,
589+
previous_node_id=params.destination_previous_node_id,
590+
previous_node_output=params.destination_previous_node_output,
591+
)
592+
593+
# Pluck out the workflow, we need it to send our signals for next nodes.
594+
workflow = AutomationWorkflowService().get_workflow(user, params.workflow_id)
595+
596+
# Set the origin's next nodes to their new position.
597+
AutomationNodeService().update_next_nodes_values(
598+
user, params.origin_new_next_nodes_values, workflow
599+
)
600+
601+
# Set the destination's next nodes to their new position.
602+
AutomationNodeService().update_next_nodes_values(
603+
user, params.destination_new_next_nodes_values, workflow
604+
)

backend/src/baserow/contrib/automation/nodes/exceptions.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,10 @@ class AutomationNodeNotReplaceable(AutomationNodeError):
6666

6767
class AutomationNodeSimulateDispatchError(AutomationNodeError):
6868
"""Raised when there is an error while simulating a dispatch of a node."""
69+
70+
71+
class AutomationNodeNotMovable(AutomationNodeError):
72+
"""
73+
Raised when an automation node is not movable. This can happen if
74+
the node's type dictates that it cannot be moved due to its state.
75+
"""

0 commit comments

Comments
 (0)