1
0
Fork 0
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:
Alexander Haller 2023-12-06 09:44:50 +00:00
parent 947a1e342b
commit 242e8359cd
31 changed files with 593 additions and 53 deletions
backend
enterprise/backend/src/baserow_enterprise/role
web-frontend/modules
builder
components/event
services
store
core

View file

@ -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},

View file

@ -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",
)

View file

@ -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"
)

View file

@ -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",
),
]

View file

@ -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)

View file

@ -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

View file

@ -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,
),
]

View file

@ -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,
)

View file

@ -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

View file

@ -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="")

View file

@ -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"

View file

@ -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"

View file

@ -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

View file

@ -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()

View file

@ -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,
):

View file

@ -6,6 +6,7 @@ from baserow.core.workflow_actions.models import WorkflowAction
class WorkflowActionDict(TypedDict):
id: int
type: str
order: int
WorkflowActionDictSubClass = TypeVar(

View file

@ -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)

View file

@ -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

View file

@ -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",

View file

@ -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}}

View file

@ -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

View file

@ -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,
)

View file

@ -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(

View file

@ -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>

View file

@ -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
)
},
}
}

View file

@ -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)
},
}

View file

@ -144,3 +144,4 @@
@import 'anchor';
@import 'call_to_action';
@import 'toast_button';
@import 'workflow_action';

View file

@ -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;
}
}

View file

@ -1,6 +1,7 @@
.workflow-action-selector {
display: flex;
align-items: center;
flex-grow: 1;
}
.workflow-action-selector__options {

View file

@ -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%);
}
}

View file

@ -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"