mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-07 22:35:36 +00:00
Order workflow actions
This commit is contained in:
parent
947a1e342b
commit
242e8359cd
31 changed files with 593 additions and 53 deletions
backend
src/baserow
api/workflow_actions
contrib/builder
core/workflow_actions
test_utils/fixtures
tests/baserow/contrib/builder
enterprise/backend/src/baserow_enterprise/role
web-frontend/modules
builder
core
assets/scss/components
components/workflowActions
|
@ -12,7 +12,7 @@ class WorkflowActionSerializer(serializers.ModelSerializer):
|
|||
|
||||
class Meta:
|
||||
model = WorkflowAction
|
||||
fields = ("id", "type")
|
||||
fields = ("id", "order", "type")
|
||||
|
||||
extra_kwargs = {
|
||||
"id": {"read_only": True},
|
||||
|
|
|
@ -5,3 +5,9 @@ ERROR_WORKFLOW_ACTION_DOES_NOT_EXIST = (
|
|||
HTTP_404_NOT_FOUND,
|
||||
"The requested workflow action does not exist.",
|
||||
)
|
||||
|
||||
ERROR_WORKFLOW_ACTION_NOT_IN_ELEMENT = (
|
||||
"ERROR_WORKFLOW_ACTION_NOT_IN_ELEMENT",
|
||||
HTTP_404_NOT_FOUND,
|
||||
"The requested workflow action does not belong to the element",
|
||||
)
|
||||
|
|
|
@ -24,7 +24,7 @@ class BuilderWorkflowActionSerializer(WorkflowActionSerializer):
|
|||
|
||||
class Meta:
|
||||
model = BuilderWorkflowAction
|
||||
fields = ("id", "element_id", "type", "event")
|
||||
fields = ("id", "order", "element_id", "type", "event")
|
||||
|
||||
extra_kwargs = {
|
||||
"id": {"read_only": True},
|
||||
|
@ -59,3 +59,14 @@ class UpdateBuilderWorkflowActionsSerializer(serializers.ModelSerializer):
|
|||
class Meta:
|
||||
model = BuilderWorkflowAction
|
||||
fields = ("type",)
|
||||
|
||||
|
||||
class OrderWorkflowActionsSerializer(serializers.Serializer):
|
||||
workflow_action_ids = serializers.ListField(
|
||||
child=serializers.IntegerField(),
|
||||
help_text="The ids of the workflow actions in the order they are supposed to be "
|
||||
"set in",
|
||||
)
|
||||
element_id = serializers.IntegerField(
|
||||
required=False, help_text="The element the workflow actions belong to"
|
||||
)
|
||||
|
|
|
@ -3,6 +3,7 @@ from django.urls import re_path
|
|||
from baserow.contrib.builder.api.workflow_actions.views import (
|
||||
BuilderWorkflowActionsView,
|
||||
BuilderWorkflowActionView,
|
||||
OrderBuilderWorkflowActionsView,
|
||||
)
|
||||
|
||||
app_name = "baserow.contrib.builder.api.workflow_actions"
|
||||
|
@ -18,4 +19,9 @@ urls_without_builder_id = [
|
|||
BuilderWorkflowActionView.as_view(),
|
||||
name="item",
|
||||
),
|
||||
re_path(
|
||||
r"page/(?P<page_id>[0-9]+)/workflow_actions/order/$",
|
||||
OrderBuilderWorkflowActionsView.as_view(),
|
||||
name="order",
|
||||
),
|
||||
]
|
||||
|
|
|
@ -8,7 +8,7 @@ from rest_framework.permissions import IsAuthenticated
|
|||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from baserow.api.decorators import validate_body_custom_fields
|
||||
from baserow.api.decorators import validate_body, validate_body_custom_fields
|
||||
from baserow.api.schemas import CLIENT_SESSION_ID_SCHEMA_PARAMETER, get_error_schema
|
||||
from baserow.api.utils import (
|
||||
CustomFieldRegistryMappingSerializer,
|
||||
|
@ -21,15 +21,21 @@ from baserow.contrib.builder.api.elements.errors import ERROR_ELEMENT_DOES_NOT_E
|
|||
from baserow.contrib.builder.api.pages.errors import ERROR_PAGE_DOES_NOT_EXIST
|
||||
from baserow.contrib.builder.api.workflow_actions.errors import (
|
||||
ERROR_WORKFLOW_ACTION_DOES_NOT_EXIST,
|
||||
ERROR_WORKFLOW_ACTION_NOT_IN_ELEMENT,
|
||||
)
|
||||
from baserow.contrib.builder.api.workflow_actions.serializers import (
|
||||
BuilderWorkflowActionSerializer,
|
||||
CreateBuilderWorkflowActionSerializer,
|
||||
OrderWorkflowActionsSerializer,
|
||||
UpdateBuilderWorkflowActionsSerializer,
|
||||
)
|
||||
from baserow.contrib.builder.elements.exceptions import ElementDoesNotExist
|
||||
from baserow.contrib.builder.elements.handler import ElementHandler
|
||||
from baserow.contrib.builder.pages.exceptions import PageDoesNotExist
|
||||
from baserow.contrib.builder.pages.handler import PageHandler
|
||||
from baserow.contrib.builder.workflow_actions.exceptions import (
|
||||
WorkflowActionNotInElement,
|
||||
)
|
||||
from baserow.contrib.builder.workflow_actions.handler import (
|
||||
BuilderWorkflowActionHandler,
|
||||
)
|
||||
|
@ -257,3 +263,58 @@ class BuilderWorkflowActionView(APIView):
|
|||
workflow_action_updated, BuilderWorkflowActionSerializer
|
||||
)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class OrderBuilderWorkflowActionsView(APIView):
|
||||
@extend_schema(
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="page_id",
|
||||
location=OpenApiParameter.PATH,
|
||||
type=OpenApiTypes.INT,
|
||||
description="The page the workflow actions belong to",
|
||||
),
|
||||
CLIENT_SESSION_ID_SCHEMA_PARAMETER,
|
||||
],
|
||||
tags=["Builder workflow_actions"],
|
||||
operation_id="order_builder_workflow_actions",
|
||||
description="Apply a new order to the workflow actions of a page",
|
||||
request=OrderWorkflowActionsSerializer,
|
||||
responses={
|
||||
204: None,
|
||||
400: get_error_schema(
|
||||
[
|
||||
"ERROR_USER_NOT_IN_GROUP",
|
||||
"ERROR_REQUEST_BODY_VALIDATION",
|
||||
]
|
||||
),
|
||||
404: get_error_schema(
|
||||
[
|
||||
"ERROR_PAGE_DOES_NOT_EXIST",
|
||||
"ERROR_WORKFLOW_ACTION_DOES_NOT_EXIST",
|
||||
"ERROR_WORKFLOW_ACTION_NOT_IN_ELEMENT",
|
||||
]
|
||||
),
|
||||
},
|
||||
)
|
||||
@transaction.atomic
|
||||
@map_exceptions(
|
||||
{
|
||||
PageDoesNotExist: ERROR_PAGE_DOES_NOT_EXIST,
|
||||
WorkflowActionDoesNotExist: ERROR_WORKFLOW_ACTION_DOES_NOT_EXIST,
|
||||
WorkflowActionNotInElement: ERROR_WORKFLOW_ACTION_NOT_IN_ELEMENT,
|
||||
}
|
||||
)
|
||||
@validate_body(OrderWorkflowActionsSerializer)
|
||||
def post(self, request, data: Dict, page_id: int):
|
||||
page = PageHandler().get_page(page_id)
|
||||
element_id = data.get("element_id", None)
|
||||
element = (
|
||||
ElementHandler().get_element(element_id) if element_id is not None else None
|
||||
)
|
||||
|
||||
BuilderWorkflowActionService().order_workflow_actions(
|
||||
request.user, page, data["workflow_action_ids"], element=element
|
||||
)
|
||||
|
||||
return Response(status=204)
|
||||
|
|
|
@ -136,6 +136,7 @@ class BuilderConfig(AppConfig):
|
|||
CreateBuilderWorkflowActionOperationType,
|
||||
DeleteBuilderWorkflowActionOperationType,
|
||||
ListBuilderWorkflowActionsPageOperationType,
|
||||
OrderBuilderWorkflowActionOperationType,
|
||||
ReadBuilderWorkflowActionOperationType,
|
||||
UpdateBuilderWorkflowActionOperationType,
|
||||
)
|
||||
|
@ -145,6 +146,7 @@ class BuilderConfig(AppConfig):
|
|||
operation_type_registry.register(DeleteBuilderWorkflowActionOperationType())
|
||||
operation_type_registry.register(UpdateBuilderWorkflowActionOperationType())
|
||||
operation_type_registry.register(ReadBuilderWorkflowActionOperationType())
|
||||
operation_type_registry.register(OrderBuilderWorkflowActionOperationType())
|
||||
|
||||
from baserow.core.registries import permission_manager_type_registry
|
||||
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 3.2.21 on 2023-10-31 10:29
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("builder", "0029_inputtextelement_label"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="builderworkflowaction",
|
||||
name="order",
|
||||
field=models.PositiveIntegerField(default=0),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
|
@ -0,0 +1,10 @@
|
|||
class WorkflowActionNotInElement(Exception):
|
||||
"""Raised when trying to get a workflow action that does not belong to an element"""
|
||||
|
||||
def __init__(self, workflow_action_id=None, *args, **kwargs):
|
||||
self.workflow_action_id = workflow_action_id
|
||||
super().__init__(
|
||||
f"The workflow action {workflow_action_id} does not belong to the element.",
|
||||
*args,
|
||||
**kwargs,
|
||||
)
|
|
@ -1,16 +1,23 @@
|
|||
from typing import Dict, Iterable, Optional
|
||||
from typing import Dict, Iterable, List, Optional
|
||||
from zipfile import ZipFile
|
||||
|
||||
from django.core.files.storage import Storage
|
||||
from django.db.models import QuerySet
|
||||
|
||||
from baserow.contrib.builder.elements.handler import ElementHandler
|
||||
from baserow.contrib.builder.elements.models import Element
|
||||
from baserow.contrib.builder.pages.models import Page
|
||||
from baserow.contrib.builder.workflow_actions.exceptions import (
|
||||
WorkflowActionNotInElement,
|
||||
)
|
||||
from baserow.contrib.builder.workflow_actions.models import BuilderWorkflowAction
|
||||
from baserow.contrib.builder.workflow_actions.registries import (
|
||||
builder_workflow_action_type_registry,
|
||||
)
|
||||
from baserow.core.exceptions import IdDoesNotExist
|
||||
from baserow.core.workflow_actions.handler import WorkflowActionHandler
|
||||
from baserow.core.workflow_actions.models import WorkflowAction
|
||||
from baserow.core.workflow_actions.registries import WorkflowActionType
|
||||
|
||||
|
||||
class BuilderWorkflowActionHandler(WorkflowActionHandler):
|
||||
|
@ -44,6 +51,7 @@ class BuilderWorkflowActionHandler(WorkflowActionHandler):
|
|||
kwargs["page_id"] = workflow_action.page_id
|
||||
kwargs["element_id"] = workflow_action.element_id
|
||||
kwargs["event"] = workflow_action.event
|
||||
kwargs["order"] = workflow_action.order
|
||||
|
||||
return super().update_workflow_action(workflow_action, **kwargs)
|
||||
|
||||
|
@ -74,3 +82,45 @@ class BuilderWorkflowActionHandler(WorkflowActionHandler):
|
|||
return workflow_action_type.import_serialized(
|
||||
page, serialized_workflow_action, id_mapping
|
||||
)
|
||||
|
||||
def order_workflow_actions(
|
||||
self, page: Page, order: List[int], base_qs=None, element: Element = None
|
||||
):
|
||||
"""
|
||||
Assigns a new order to the domains in a builder application.
|
||||
You can provide a base_qs for pre-filter the domains affected by this change.
|
||||
|
||||
:param page: The page the workflow actions belong to
|
||||
:param order: The new order of the workflow actions
|
||||
:param base_qs: A QS that can have filters already applied
|
||||
:param element: The element the workflow action belongs to
|
||||
:raises WorkflowActionNotInElement: If the workflow action is not part of the
|
||||
provided element
|
||||
:return: The new order of the domains
|
||||
"""
|
||||
|
||||
if base_qs is None:
|
||||
base_qs = BuilderWorkflowAction.objects.filter(page=page, element=element)
|
||||
|
||||
try:
|
||||
full_order = BuilderWorkflowAction.order_objects(base_qs, order)
|
||||
except IdDoesNotExist as error:
|
||||
raise WorkflowActionNotInElement(error.not_existing_id)
|
||||
|
||||
return full_order
|
||||
|
||||
def create_workflow_action(
|
||||
self, workflow_action_type: WorkflowActionType, **kwargs
|
||||
) -> BuilderWorkflowAction:
|
||||
if "order" not in kwargs:
|
||||
if "element_id" in kwargs:
|
||||
element = ElementHandler().get_element(element_id=kwargs["element_id"])
|
||||
kwargs["order"] = BuilderWorkflowAction.get_last_order_element_scope(
|
||||
element
|
||||
)
|
||||
else:
|
||||
kwargs["order"] = BuilderWorkflowAction.get_last_order_page_scope(
|
||||
kwargs["page"]
|
||||
)
|
||||
|
||||
return super().create_workflow_action(workflow_action_type, **kwargs).specific
|
||||
|
|
|
@ -4,6 +4,7 @@ from django.db import models
|
|||
from baserow.contrib.builder.elements.models import Element
|
||||
from baserow.contrib.builder.pages.models import Page
|
||||
from baserow.core.formula.field import FormulaField
|
||||
from baserow.core.mixins import OrderableMixin
|
||||
from baserow.core.registry import ModelRegistryMixin
|
||||
from baserow.core.workflow_actions.models import WorkflowAction
|
||||
|
||||
|
@ -12,7 +13,11 @@ class EventTypes(models.TextChoices):
|
|||
CLICK = "click"
|
||||
|
||||
|
||||
class BuilderWorkflowAction(WorkflowAction):
|
||||
class BuilderWorkflowAction(
|
||||
WorkflowAction,
|
||||
OrderableMixin,
|
||||
):
|
||||
order = models.PositiveIntegerField()
|
||||
content_type = models.ForeignKey(
|
||||
ContentType,
|
||||
verbose_name="content type",
|
||||
|
@ -40,6 +45,16 @@ class BuilderWorkflowAction(WorkflowAction):
|
|||
def get_parent(self):
|
||||
return self.page
|
||||
|
||||
@classmethod
|
||||
def get_last_order_element_scope(cls, element: Element):
|
||||
queryset = BuilderWorkflowAction.objects.filter(element=element)
|
||||
return cls.get_highest_order_of_queryset(queryset) + 1
|
||||
|
||||
@classmethod
|
||||
def get_last_order_page_scope(cls, page: Page):
|
||||
queryset = BuilderWorkflowAction.objects.filter(page=page, element=None)
|
||||
return cls.get_highest_order_of_queryset(queryset) + 1
|
||||
|
||||
|
||||
class NotificationWorkflowAction(BuilderWorkflowAction):
|
||||
title = FormulaField(default="")
|
||||
|
|
|
@ -13,6 +13,10 @@ class CreateBuilderWorkflowActionOperationType(BuilderPageOperationType):
|
|||
type = "builder.page.create_workflow_action"
|
||||
|
||||
|
||||
class OrderBuilderWorkflowActionOperationType(BuilderPageOperationType):
|
||||
type = "builder.page.workflow_action.order"
|
||||
|
||||
|
||||
class BuilderWorkflowActionOperationType(OperationType, ABC):
|
||||
context_scope_name = "builder_workflow_action"
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ from baserow.core.workflow_actions.registries import WorkflowActionType
|
|||
|
||||
|
||||
class BuilderWorkflowActionType(WorkflowActionType, PublicCustomFieldsInstanceMixin):
|
||||
allowed_fields = ["page", "page_id", "element", "element_id", "event"]
|
||||
allowed_fields = ["order", "page", "page_id", "element", "element_id", "event"]
|
||||
|
||||
parent_property_name = "page"
|
||||
id_mapping_name = "builder_workflow_actions"
|
||||
|
|
|
@ -2,6 +2,7 @@ from typing import List
|
|||
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
|
||||
from baserow.contrib.builder.elements.models import Element
|
||||
from baserow.contrib.builder.pages.models import Page
|
||||
from baserow.contrib.builder.workflow_actions.handler import (
|
||||
BuilderWorkflowActionHandler,
|
||||
|
@ -14,6 +15,7 @@ from baserow.contrib.builder.workflow_actions.operations import (
|
|||
CreateBuilderWorkflowActionOperationType,
|
||||
DeleteBuilderWorkflowActionOperationType,
|
||||
ListBuilderWorkflowActionsPageOperationType,
|
||||
OrderBuilderWorkflowActionOperationType,
|
||||
ReadBuilderWorkflowActionOperationType,
|
||||
UpdateBuilderWorkflowActionOperationType,
|
||||
)
|
||||
|
@ -21,6 +23,7 @@ from baserow.contrib.builder.workflow_actions.signals import (
|
|||
workflow_action_created,
|
||||
workflow_action_deleted,
|
||||
workflow_action_updated,
|
||||
workflow_actions_reordered,
|
||||
)
|
||||
from baserow.contrib.builder.workflow_actions.workflow_action_types import (
|
||||
BuilderWorkflowActionType,
|
||||
|
@ -172,3 +175,46 @@ class BuilderWorkflowActionService:
|
|||
workflow_action_deleted.send(
|
||||
self, workflow_action_id=workflow_action.id, page=page, user=user
|
||||
)
|
||||
|
||||
def order_workflow_actions(
|
||||
self,
|
||||
user: AbstractUser,
|
||||
page: Page,
|
||||
order: List[int],
|
||||
element: Element = None,
|
||||
) -> List[int]:
|
||||
"""
|
||||
Assigns a new order to the workflow actions in a builder application.
|
||||
|
||||
:param user: The user trying to order the domains
|
||||
:param page: The page that the workflow actions belong to
|
||||
:param order: The new order of the workflow actions
|
||||
:param element: The element the page belongs to
|
||||
:return: The new order of the workflow actions
|
||||
"""
|
||||
|
||||
CoreHandler().check_permissions(
|
||||
user,
|
||||
OrderBuilderWorkflowActionOperationType.type,
|
||||
workspace=page.builder.workspace,
|
||||
context=page,
|
||||
)
|
||||
|
||||
all_workflow_actions = BuilderWorkflowAction.objects.filter(
|
||||
page=page, element=element
|
||||
)
|
||||
|
||||
user_workflow_actions = CoreHandler().filter_queryset(
|
||||
user,
|
||||
OrderBuilderWorkflowActionOperationType.type,
|
||||
all_workflow_actions,
|
||||
workspace=page.builder.workspace,
|
||||
)
|
||||
|
||||
full_order = self.handler.order_workflow_actions(
|
||||
page, order, base_qs=user_workflow_actions, element=element
|
||||
)
|
||||
|
||||
workflow_actions_reordered.send(self, order=full_order, user=user)
|
||||
|
||||
return full_order
|
||||
|
|
|
@ -3,3 +3,4 @@ from django.dispatch import Signal
|
|||
workflow_action_created = Signal()
|
||||
workflow_action_deleted = Signal()
|
||||
workflow_action_updated = Signal()
|
||||
workflow_actions_reordered = Signal()
|
||||
|
|
|
@ -3,7 +3,6 @@ from django.db import models
|
|||
from baserow.core.mixins import (
|
||||
CreatedAndUpdatedOnMixin,
|
||||
HierarchicalModelMixin,
|
||||
OrderableMixin,
|
||||
PolymorphicContentTypeMixin,
|
||||
WithRegistry,
|
||||
)
|
||||
|
@ -14,7 +13,6 @@ class WorkflowAction(
|
|||
PolymorphicContentTypeMixin,
|
||||
CreatedAndUpdatedOnMixin,
|
||||
HierarchicalModelMixin,
|
||||
OrderableMixin,
|
||||
models.Model,
|
||||
WithRegistry,
|
||||
):
|
||||
|
|
|
@ -6,6 +6,7 @@ from baserow.core.workflow_actions.models import WorkflowAction
|
|||
class WorkflowActionDict(TypedDict):
|
||||
id: int
|
||||
type: str
|
||||
order: int
|
||||
|
||||
|
||||
WorkflowActionDictSubClass = TypeVar(
|
||||
|
|
|
@ -6,4 +6,10 @@ class WorkflowActionFixture:
|
|||
return self.create_workflow_action(NotificationWorkflowAction, **kwargs)
|
||||
|
||||
def create_workflow_action(self, model_class, **kwargs):
|
||||
if "order" not in "kwargs":
|
||||
kwargs["order"] = 0
|
||||
|
||||
if "page" not in kwargs and "element" in kwargs:
|
||||
kwargs["page"] = kwargs["element"].page
|
||||
|
||||
return model_class.objects.create(**kwargs)
|
||||
|
|
|
@ -203,3 +203,105 @@ def test_public_workflow_actions_view(
|
|||
|
||||
[workflow_action_in_response] = response.json()
|
||||
assert "test" in workflow_action_in_response
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_workflow_actions(api_client, data_fixture):
|
||||
user, token = data_fixture.create_user_and_token()
|
||||
page = data_fixture.create_builder_page(user=user)
|
||||
element = data_fixture.create_builder_button_element(page=page)
|
||||
workflow_action_one = data_fixture.create_notification_workflow_action(
|
||||
page=page, element=element, order=1
|
||||
)
|
||||
workflow_action_two = data_fixture.create_notification_workflow_action(
|
||||
page=page, element=element, order=2
|
||||
)
|
||||
|
||||
order = [workflow_action_two.id, workflow_action_one.id]
|
||||
|
||||
url = reverse(
|
||||
"api:builder:workflow_action:order",
|
||||
kwargs={"page_id": page.id},
|
||||
)
|
||||
response = api_client.post(
|
||||
url,
|
||||
{"workflow_action_ids": order, "element_id": element.id},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_204_NO_CONTENT
|
||||
|
||||
workflow_action_one.refresh_from_db()
|
||||
workflow_action_two.refresh_from_db()
|
||||
|
||||
assert workflow_action_one.order > workflow_action_two.order
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_workflow_actions_page_does_not_exist(api_client, data_fixture):
|
||||
user, token = data_fixture.create_user_and_token()
|
||||
|
||||
url = reverse(
|
||||
"api:builder:workflow_action:order",
|
||||
kwargs={"page_id": 99999},
|
||||
)
|
||||
response = api_client.post(
|
||||
url,
|
||||
{"workflow_action_ids": []},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_404_NOT_FOUND
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_workflow_actions_workflow_action_does_not_exist(
|
||||
api_client, data_fixture
|
||||
):
|
||||
user, token = data_fixture.create_user_and_token()
|
||||
page = data_fixture.create_builder_page(user=user)
|
||||
|
||||
url = reverse(
|
||||
"api:builder:workflow_action:order",
|
||||
kwargs={"page_id": page.id},
|
||||
)
|
||||
response = api_client.post(
|
||||
url,
|
||||
{"workflow_action_ids": [9999]},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_404_NOT_FOUND
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_workflow_actions_workflow_action_not_in_element(
|
||||
api_client, data_fixture
|
||||
):
|
||||
user, token = data_fixture.create_user_and_token()
|
||||
page = data_fixture.create_builder_page(user=user)
|
||||
element = data_fixture.create_builder_button_element(page=page)
|
||||
workflow_action_one = data_fixture.create_notification_workflow_action(
|
||||
page=page, element=element, order=1
|
||||
)
|
||||
workflow_action_two = data_fixture.create_notification_workflow_action(
|
||||
page=page, order=2
|
||||
)
|
||||
|
||||
order = [workflow_action_two.id, workflow_action_one.id]
|
||||
|
||||
url = reverse(
|
||||
"api:builder:workflow_action:order",
|
||||
kwargs={"page_id": page.id},
|
||||
)
|
||||
response = api_client.post(
|
||||
url,
|
||||
{"workflow_action_ids": order, "element_id": element.id},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTP_404_NOT_FOUND
|
||||
|
|
|
@ -95,6 +95,7 @@ def test_builder_application_export(data_fixture):
|
|||
"workflow_actions": [
|
||||
{
|
||||
"id": workflow_action_1.id,
|
||||
"order": 0,
|
||||
"type": "notification",
|
||||
"element_id": element1.id,
|
||||
"event": EventTypes.CLICK.value,
|
||||
|
@ -329,6 +330,7 @@ IMPORT_REFERENCE = {
|
|||
"workflow_actions": [
|
||||
{
|
||||
"id": 123,
|
||||
"order": 1,
|
||||
"page_id": 999,
|
||||
"element_id": 998,
|
||||
"type": "notification",
|
||||
|
|
|
@ -41,7 +41,12 @@ def test_import_workflow_action(data_fixture, workflow_action_type: WorkflowActi
|
|||
|
||||
page_after_import = data_fixture.create_builder_page()
|
||||
|
||||
serialized = {"id": 9999, "type": workflow_action_type.type, "page_id": page.id}
|
||||
serialized = {
|
||||
"id": 9999,
|
||||
"type": workflow_action_type.type,
|
||||
"page_id": page.id,
|
||||
"order": 0,
|
||||
}
|
||||
serialized.update(workflow_action_type.get_sample_params())
|
||||
|
||||
id_mapping = {"builder_pages": {page.id: page_after_import.id}}
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
import pytest
|
||||
|
||||
from baserow.contrib.builder.workflow_actions.exceptions import (
|
||||
WorkflowActionNotInElement,
|
||||
)
|
||||
from baserow.contrib.builder.workflow_actions.handler import (
|
||||
BuilderWorkflowActionHandler,
|
||||
)
|
||||
|
@ -122,3 +125,65 @@ def test_get_workflow_actions(data_fixture):
|
|||
|
||||
assert workflow_action_one_fetched.id == workflow_action_one.id
|
||||
assert workflow_action_two_fetched.id == workflow_action_two.id
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_workflow_actions(data_fixture):
|
||||
element = data_fixture.create_builder_button_element()
|
||||
workflow_action_one = data_fixture.create_notification_workflow_action(
|
||||
page=element.page, element=element, order=1
|
||||
)
|
||||
workflow_action_two = data_fixture.create_notification_workflow_action(
|
||||
page=element.page, element=element, order=2
|
||||
)
|
||||
|
||||
assert BuilderWorkflowActionHandler().order_workflow_actions(
|
||||
element.page,
|
||||
[workflow_action_two.id, workflow_action_one.id],
|
||||
element=element,
|
||||
) == [
|
||||
workflow_action_two.id,
|
||||
workflow_action_one.id,
|
||||
]
|
||||
|
||||
workflow_action_one.refresh_from_db()
|
||||
workflow_action_two.refresh_from_db()
|
||||
|
||||
assert workflow_action_one.order == 2
|
||||
assert workflow_action_two.order == 1
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_workflow_action_not_in_element(data_fixture):
|
||||
element = data_fixture.create_builder_button_element()
|
||||
element_unrelated = data_fixture.create_builder_button_element()
|
||||
workflow_action_one = data_fixture.create_notification_workflow_action(
|
||||
page=element.page, element=element, order=1
|
||||
)
|
||||
workflow_action_two = data_fixture.create_notification_workflow_action(
|
||||
page=element_unrelated.page, order=2
|
||||
)
|
||||
|
||||
base_qs = BuilderWorkflowAction.objects.filter(id=workflow_action_two.id)
|
||||
|
||||
with pytest.raises(WorkflowActionNotInElement):
|
||||
BuilderWorkflowActionHandler().order_workflow_actions(
|
||||
element.page,
|
||||
[workflow_action_two.id, workflow_action_one.id],
|
||||
element=element,
|
||||
base_qs=base_qs,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_workflow_actions_different_scopes(data_fixture):
|
||||
page = data_fixture.create_builder_page()
|
||||
element = data_fixture.create_builder_button_element(page=page)
|
||||
page_workflow_action = BuilderWorkflowActionHandler().create_workflow_action(
|
||||
NotificationWorkflowActionType(), page=page
|
||||
)
|
||||
element_workflow_action = BuilderWorkflowActionHandler().create_workflow_action(
|
||||
NotificationWorkflowActionType(), page=page, element_id=element.id
|
||||
)
|
||||
|
||||
assert page_workflow_action.order == element_workflow_action.order
|
||||
|
|
|
@ -184,3 +184,47 @@ def test_get_workflow_actions_no_permissions(data_fixture):
|
|||
|
||||
with pytest.raises(UserNotInWorkspace):
|
||||
BuilderWorkflowActionService().get_workflow_actions(user, page)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_workflow_actions(data_fixture):
|
||||
user = data_fixture.create_user()
|
||||
element = data_fixture.create_builder_button_element(user=user)
|
||||
workflow_action_one = data_fixture.create_notification_workflow_action(
|
||||
element=element, order=0
|
||||
)
|
||||
workflow_action_two = data_fixture.create_notification_workflow_action(
|
||||
element=element, order=1
|
||||
)
|
||||
|
||||
BuilderWorkflowActionService().order_workflow_actions(
|
||||
user,
|
||||
element.page,
|
||||
[workflow_action_two.id, workflow_action_one.id],
|
||||
element=element,
|
||||
)
|
||||
|
||||
workflow_action_one.refresh_from_db()
|
||||
workflow_action_two.refresh_from_db()
|
||||
|
||||
assert workflow_action_one.order > workflow_action_two.order
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_order_workflow_actions_user_not_in_workspace(data_fixture):
|
||||
user = data_fixture.create_user()
|
||||
element = data_fixture.create_builder_button_element()
|
||||
workflow_action_one = data_fixture.create_notification_workflow_action(
|
||||
element=element, order=0
|
||||
)
|
||||
workflow_action_two = data_fixture.create_notification_workflow_action(
|
||||
element=element, order=1
|
||||
)
|
||||
|
||||
with pytest.raises(UserNotInWorkspace):
|
||||
BuilderWorkflowActionService().order_workflow_actions(
|
||||
user,
|
||||
element.page,
|
||||
[workflow_action_two.id, workflow_action_one.id],
|
||||
element=element,
|
||||
)
|
||||
|
|
|
@ -52,6 +52,7 @@ from baserow.contrib.builder.workflow_actions.operations import (
|
|||
CreateBuilderWorkflowActionOperationType,
|
||||
DeleteBuilderWorkflowActionOperationType,
|
||||
ListBuilderWorkflowActionsPageOperationType,
|
||||
OrderBuilderWorkflowActionOperationType,
|
||||
ReadBuilderWorkflowActionOperationType,
|
||||
UpdateBuilderWorkflowActionOperationType,
|
||||
)
|
||||
|
@ -323,6 +324,7 @@ default_roles[EDITOR_ROLE_UID].extend(
|
|||
ListTeamSubjectsOperationType,
|
||||
ReadTeamSubjectOperationType,
|
||||
UpdateBuilderWorkflowActionOperationType,
|
||||
OrderBuilderWorkflowActionOperationType,
|
||||
]
|
||||
)
|
||||
default_roles[BUILDER_ROLE_UID].extend(
|
||||
|
|
|
@ -19,15 +19,22 @@
|
|||
</div>
|
||||
</template>
|
||||
<template #default>
|
||||
<WorkflowAction
|
||||
v-for="workflowAction in workflowActions"
|
||||
:key="workflowAction.id"
|
||||
class="margin-top-2 event__workflow-action"
|
||||
:available-workflow-action-types="availableWorkflowActionTypes"
|
||||
:workflow-action="workflowAction"
|
||||
@delete="deleteWorkflowAction(workflowAction)"
|
||||
@update="updateWorkflowAction(workflowAction, $event)"
|
||||
/>
|
||||
<div>
|
||||
<WorkflowAction
|
||||
v-for="(workflowAction, index) in workflowActions"
|
||||
:key="workflowAction.id"
|
||||
v-sortable="{
|
||||
id: workflowAction.id,
|
||||
handle: '[data-sortable-handle]',
|
||||
update: orderWorkflowActions,
|
||||
}"
|
||||
class="event__workflow-action"
|
||||
:class="{ 'event__workflow-action--first': index === 0 }"
|
||||
:available-workflow-action-types="availableWorkflowActionTypes"
|
||||
:workflow-action="workflowAction"
|
||||
@delete="deleteWorkflowAction(workflowAction)"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
size="tiny"
|
||||
type="link"
|
||||
|
@ -82,6 +89,7 @@ export default {
|
|||
...mapActions({
|
||||
actionCreateWorkflowAction: 'workflowAction/create',
|
||||
actionDeleteWorkflowAction: 'workflowAction/delete',
|
||||
actionOrderWorkflowActions: 'workflowAction/order',
|
||||
}),
|
||||
getIcon(expanded) {
|
||||
return expanded ? 'iconoir-nav-arrow-down' : 'iconoir-nav-arrow-right'
|
||||
|
@ -112,6 +120,17 @@ export default {
|
|||
notifyIf(error)
|
||||
}
|
||||
},
|
||||
async orderWorkflowActions(order) {
|
||||
try {
|
||||
await this.actionOrderWorkflowActions({
|
||||
page: this.page,
|
||||
element: this.element,
|
||||
order,
|
||||
})
|
||||
} catch (error) {
|
||||
notifyIf(error)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -21,5 +21,17 @@ export default (client) => {
|
|||
values
|
||||
)
|
||||
},
|
||||
order(pageId, order, elementId = null) {
|
||||
const payload = { workflow_action_ids: order }
|
||||
|
||||
if (elementId) {
|
||||
payload.element_id = elementId
|
||||
}
|
||||
|
||||
return client.post(
|
||||
`builder/page/${pageId}/workflow_actions/order/`,
|
||||
payload
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,6 +36,12 @@ const mutations = {
|
|||
workflowAction.id === workflowActionToSet.id ? values : workflowAction
|
||||
)
|
||||
},
|
||||
ORDER_ITEMS(state, { page, order }) {
|
||||
page.workflowActions.forEach((workflowAction) => {
|
||||
const index = order.findIndex((value) => value === workflowAction.id)
|
||||
workflowAction.order = index === -1 ? 0 : index + 1
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
const actions = {
|
||||
|
@ -51,6 +57,9 @@ const actions = {
|
|||
forceSet({ commit }, { page, workflowAction, values }) {
|
||||
commit('SET_ITEM', { page, workflowAction, values })
|
||||
},
|
||||
forceOrder({ commit }, { page, order }) {
|
||||
commit('ORDER_ITEMS', { page, order })
|
||||
},
|
||||
async create(
|
||||
{ dispatch },
|
||||
{ page, workflowActionType, eventType, configuration = null }
|
||||
|
@ -87,34 +96,17 @@ const actions = {
|
|||
throw error
|
||||
}
|
||||
},
|
||||
async update({ dispatch }, { page, workflowAction, values }) {
|
||||
const oldValues = {}
|
||||
const newValues = {}
|
||||
Object.keys(values).forEach((name) => {
|
||||
if (Object.prototype.hasOwnProperty.call(workflowAction, name)) {
|
||||
oldValues[name] = workflowAction[name]
|
||||
newValues[name] = values[name]
|
||||
}
|
||||
})
|
||||
|
||||
await dispatch('forceUpdate', { page, workflowAction, values: newValues })
|
||||
|
||||
try {
|
||||
const { data } = await WorkflowActionService(this.$client).update(
|
||||
workflowAction.id,
|
||||
values
|
||||
)
|
||||
await dispatch('forceSet', { page, workflowAction, values: data })
|
||||
} catch (error) {
|
||||
await dispatch('forceUpdate', { page, workflowAction, values: oldValues })
|
||||
throw error
|
||||
}
|
||||
},
|
||||
async updateDebounced({ dispatch }, { page, workflowAction, values }) {
|
||||
// These values should not be updated via a regular update request
|
||||
const excludeValues = ['order']
|
||||
|
||||
const oldValues = {}
|
||||
const newValues = {}
|
||||
Object.keys(values).forEach((name) => {
|
||||
if (Object.prototype.hasOwnProperty.call(workflowAction, name)) {
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(workflowAction, name) &&
|
||||
!excludeValues.includes(name)
|
||||
) {
|
||||
oldValues[name] = workflowAction[name]
|
||||
newValues[name] = values[name]
|
||||
}
|
||||
|
@ -129,7 +121,12 @@ const actions = {
|
|||
workflowAction.id,
|
||||
values
|
||||
)
|
||||
await dispatch('forceSet', {
|
||||
|
||||
excludeValues.forEach((name) => {
|
||||
delete data[name]
|
||||
})
|
||||
|
||||
await dispatch('forceUpdate', {
|
||||
page,
|
||||
workflowAction,
|
||||
values: data,
|
||||
|
@ -161,13 +158,37 @@ const actions = {
|
|||
updateContext.promiseResolve = resolve
|
||||
})
|
||||
},
|
||||
async order({ commit, getters }, { page, order, element = null }) {
|
||||
const workflowActions =
|
||||
element !== null
|
||||
? getters.getElementWorkflowActions(page, element.id)
|
||||
: getters.getWorkflowActions(page)
|
||||
|
||||
const oldOrder = workflowActions.map(({ id }) => id)
|
||||
|
||||
commit('ORDER_ITEMS', { page, order })
|
||||
|
||||
try {
|
||||
await WorkflowActionService(this.$client).order(
|
||||
page.id,
|
||||
order,
|
||||
element.id
|
||||
)
|
||||
} catch (error) {
|
||||
commit('ORDER_ITEMS', { page, order: oldOrder })
|
||||
throw error
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const getters = {
|
||||
getWorkflowActions: (state) => (page) => {
|
||||
return page.workflowActions.map((w) => w).sort((a, b) => a.order - b.order)
|
||||
},
|
||||
getElementWorkflowActions: (state) => (page, elementId) => {
|
||||
return page.workflowActions.filter(
|
||||
(workflowAction) => workflowAction.element_id === elementId
|
||||
)
|
||||
return page.workflowActions
|
||||
.filter((workflowAction) => workflowAction.element_id === elementId)
|
||||
.sort((a, b) => a.order - b.order)
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -144,3 +144,4 @@
|
|||
@import 'anchor';
|
||||
@import 'call_to_action';
|
||||
@import 'toast_button';
|
||||
@import 'workflow_action';
|
||||
|
|
|
@ -22,7 +22,14 @@
|
|||
color: $color-neutral-900;
|
||||
}
|
||||
|
||||
.event__workflow-action:not(:first-child) {
|
||||
.event__workflow-action {
|
||||
border-top: 1px solid $color-neutral-200;
|
||||
padding-top: 20px;
|
||||
|
||||
// :first-child doesn't work here as the draggable directives adds an element
|
||||
&.event__workflow-action--first {
|
||||
border-top: none;
|
||||
padding-top: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
.workflow-action-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.workflow-action-selector__options {
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
.workflow-action__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.workflow-action__header-handle {
|
||||
width: 15px;
|
||||
height: 25px;
|
||||
background-image: radial-gradient($color-neutral-200 40%, transparent 40%);
|
||||
background-size: 5px 5px;
|
||||
background-repeat: repeat;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-image: radial-gradient($color-neutral-500 40%, transparent 40%);
|
||||
}
|
||||
}
|
|
@ -1,11 +1,18 @@
|
|||
<template>
|
||||
<div>
|
||||
<WorkflowActionSelector
|
||||
:available-workflow-action-types="availableWorkflowActionTypes"
|
||||
:workflow-action="workflowAction"
|
||||
@change="updateWorkflowAction({ type: $event })"
|
||||
@delete="$emit('delete')"
|
||||
/>
|
||||
<div class="workflow-action__header">
|
||||
<div
|
||||
class="workflow-action__header-handle margin-right-1"
|
||||
data-sortable-handle
|
||||
@mousedown.prevent
|
||||
></div>
|
||||
<WorkflowActionSelector
|
||||
:available-workflow-action-types="availableWorkflowActionTypes"
|
||||
:workflow-action="workflowAction"
|
||||
@change="updateWorkflowAction({ type: $event })"
|
||||
@delete="$emit('delete')"
|
||||
/>
|
||||
</div>
|
||||
<component
|
||||
:is="workflowActionType.form"
|
||||
ref="actionForm"
|
||||
|
|
Loading…
Add table
Reference in a new issue