1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-04-17 10:22:36 +00:00

Resolve "WorkflowActions for the AB"

This commit is contained in:
Alexander Haller 2023-10-18 15:36:12 +00:00 committed by Jérémie Pardou
parent cadd8f7072
commit d990d2852c
64 changed files with 2711 additions and 48 deletions

View file

@ -0,0 +1,23 @@
from abc import abstractmethod
from rest_framework import serializers
from baserow.core.workflow_actions.models import WorkflowAction
class WorkflowActionSerializer(serializers.ModelSerializer):
type = serializers.SerializerMethodField(
help_text="The type of the workflow action"
)
class Meta:
model = WorkflowAction
fields = ("id", "type")
extra_kwargs = {
"id": {"read_only": True},
}
@abstractmethod
def get_type(self, instance: WorkflowAction):
pass

View file

@ -4,8 +4,15 @@ from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
from baserow.contrib.builder.api.workflow_actions.serializers import (
BuilderWorkflowActionSerializer,
)
from baserow.contrib.builder.elements.models import CollectionElementField, Element
from baserow.contrib.builder.elements.registries import element_type_registry
from baserow.contrib.builder.elements.types import ElementsAndWorkflowActions
from baserow.contrib.builder.workflow_actions.registries import (
builder_workflow_action_type_registry,
)
from baserow.core.formula.serializers import FormulaSerializerField
@ -109,6 +116,29 @@ class MoveElementSerializer(serializers.Serializer):
)
class DuplicateElementSerializer(serializers.Serializer):
elements = serializers.SerializerMethodField(help_text="The duplicated elements.")
workflow_actions = serializers.SerializerMethodField(
help_text="The duplicated workflow actions"
)
@extend_schema_field(ElementSerializer(many=True))
def get_elements(self, obj: ElementsAndWorkflowActions):
return [
element_type_registry.get_serializer(element, ElementSerializer).data
for element in obj["elements"]
]
@extend_schema_field(BuilderWorkflowActionSerializer(many=True))
def get_workflow_actions(self, obj: ElementsAndWorkflowActions):
return [
builder_workflow_action_type_registry.get_serializer(
workflow_action, BuilderWorkflowActionSerializer
).data
for workflow_action in obj["workflow_actions"]
]
class PageParameterValueSerializer(serializers.Serializer):
name = serializers.CharField()
value = FormulaSerializerField(allow_blank=True)

View file

@ -26,6 +26,7 @@ from baserow.contrib.builder.api.elements.errors import (
)
from baserow.contrib.builder.api.elements.serializers import (
CreateElementSerializer,
DuplicateElementSerializer,
ElementSerializer,
MoveElementSerializer,
UpdateElementSerializer,
@ -340,11 +341,10 @@ class DuplicateElementView(APIView):
],
tags=["Builder elements"],
operation_id="duplicate_builder_page_element",
description="Duplicates an element and all of the elements children",
description="Duplicates an element and all of the elements children and the "
"associated workflow actions as well.",
responses={
200: DiscriminatorCustomFieldsMappingSerializer(
element_type_registry, ElementSerializer, many=True
),
200: DuplicateElementSerializer,
400: get_error_schema(["ERROR_REQUEST_BODY_VALIDATION"]),
404: get_error_schema(
[
@ -366,13 +366,12 @@ class DuplicateElementView(APIView):
element = ElementHandler().get_element_for_update(element_id)
elements_duplicated = ElementService().duplicate_element(request.user, element)
elements_and_workflow_actions_duplicated = ElementService().duplicate_element(
request.user, element
)
elements_serialized = [
element_type_registry.get_serializer(
element_current, ElementSerializer
).data
for element_current in elements_duplicated
]
serializer = DuplicateElementSerializer(
elements_and_workflow_actions_duplicated
)
return Response(elements_serialized)
return Response(serializer.data)

View file

@ -5,6 +5,7 @@ from .domains import urls as domain_urls
from .elements import urls as element_urls
from .pages import urls as page_urls
from .theme import urls as theme_urls
from .workflow_actions import urls as workflow_action_urls
app_name = "baserow.contrib.builder.api"
@ -61,6 +62,16 @@ paths_without_builder_id = [
namespace="domains",
),
),
path(
"",
include(
(
workflow_action_urls.urls_without_builder_id,
workflow_action_urls.app_name,
),
namespace="workflow_action",
),
),
]
urlpatterns = [

View file

@ -0,0 +1,7 @@
from rest_framework.status import HTTP_404_NOT_FOUND
ERROR_WORKFLOW_ACTION_DOES_NOT_EXIST = (
"ERROR_WORKFLOW_ACTION_DOES_NOT_EXIST",
HTTP_404_NOT_FOUND,
"The requested workflow action does not exist.",
)

View file

@ -0,0 +1,61 @@
from django.utils.functional import lazy
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
from baserow.api.workflow_actions.serializers import WorkflowActionSerializer
from baserow.contrib.builder.workflow_actions.models import BuilderWorkflowAction
from baserow.contrib.builder.workflow_actions.registries import (
builder_workflow_action_type_registry,
)
class BuilderWorkflowActionSerializer(WorkflowActionSerializer):
"""
Basic builder workflow action serializer
"""
@extend_schema_field(OpenApiTypes.STR)
def get_type(self, instance):
return builder_workflow_action_type_registry.get_by_model(
instance.specific_class
).type
class Meta:
model = BuilderWorkflowAction
fields = ("id", "element_id", "type", "event")
extra_kwargs = {
"id": {"read_only": True},
"element_id": {"read_only": True},
}
class CreateBuilderWorkflowActionSerializer(serializers.ModelSerializer):
type = serializers.ChoiceField(
choices=lazy(builder_workflow_action_type_registry.get_types, list)(),
required=True,
help_text="The type of the workflow action",
)
element_id = serializers.IntegerField(
allow_null=True,
required=False,
help_text="The id of the element the workflow action is associated with",
)
class Meta:
model = BuilderWorkflowAction
fields = ("id", "element_id", "type", "event")
class UpdateBuilderWorkflowActionsSerializer(serializers.ModelSerializer):
type = serializers.ChoiceField(
choices=lazy(builder_workflow_action_type_registry.get_types, list)(),
required=False,
help_text="The type of the workflow action",
)
class Meta:
model = BuilderWorkflowAction
fields = ("type",)

View file

@ -0,0 +1,21 @@
from django.urls import re_path
from baserow.contrib.builder.api.workflow_actions.views import (
BuilderWorkflowActionsView,
BuilderWorkflowActionView,
)
app_name = "baserow.contrib.builder.api.workflow_actions"
urls_without_builder_id = [
re_path(
r"page/(?P<page_id>[0-9]+)/workflow_actions/$",
BuilderWorkflowActionsView.as_view(),
name="list",
),
re_path(
r"workflow_action/(?P<workflow_action_id>[0-9]+)/$",
BuilderWorkflowActionView.as_view(),
name="item",
),
]

View file

@ -0,0 +1,259 @@
from typing import Dict
from django.db import transaction
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter, extend_schema
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.schemas import CLIENT_SESSION_ID_SCHEMA_PARAMETER, get_error_schema
from baserow.api.utils import (
CustomFieldRegistryMappingSerializer,
DiscriminatorCustomFieldsMappingSerializer,
map_exceptions,
type_from_data_or_registry,
validate_data_custom_fields,
)
from baserow.contrib.builder.api.elements.errors import ERROR_ELEMENT_DOES_NOT_EXIST
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,
)
from baserow.contrib.builder.api.workflow_actions.serializers import (
BuilderWorkflowActionSerializer,
CreateBuilderWorkflowActionSerializer,
UpdateBuilderWorkflowActionsSerializer,
)
from baserow.contrib.builder.elements.exceptions import ElementDoesNotExist
from baserow.contrib.builder.pages.exceptions import PageDoesNotExist
from baserow.contrib.builder.pages.handler import PageHandler
from baserow.contrib.builder.workflow_actions.handler import (
BuilderWorkflowActionHandler,
)
from baserow.contrib.builder.workflow_actions.registries import (
builder_workflow_action_type_registry,
)
from baserow.contrib.builder.workflow_actions.service import (
BuilderWorkflowActionService,
)
from baserow.core.workflow_actions.exceptions import WorkflowActionDoesNotExist
class BuilderWorkflowActionsView(APIView):
permission_classes = (IsAuthenticated,)
@extend_schema(
parameters=[
OpenApiParameter(
name="page_id",
location=OpenApiParameter.PATH,
type=OpenApiTypes.INT,
description="Creates a workflow action for the builder page related to "
"the provided value.",
),
CLIENT_SESSION_ID_SCHEMA_PARAMETER,
],
tags=["Builder workflow_actions"],
operation_id="create_builder_page_workflow_action",
description="Creates a new builder workflow action",
request=DiscriminatorCustomFieldsMappingSerializer(
builder_workflow_action_type_registry,
CreateBuilderWorkflowActionSerializer,
request=True,
),
responses={
200: DiscriminatorCustomFieldsMappingSerializer(
builder_workflow_action_type_registry, BuilderWorkflowActionSerializer
),
400: get_error_schema(
[
"ERROR_REQUEST_BODY_VALIDATION",
]
),
404: get_error_schema(["ERROR_PAGE_DOES_NOT_EXIST"]),
},
)
@transaction.atomic
@map_exceptions(
{
PageDoesNotExist: ERROR_PAGE_DOES_NOT_EXIST,
ElementDoesNotExist: ERROR_ELEMENT_DOES_NOT_EXIST,
}
)
@validate_body_custom_fields(
builder_workflow_action_type_registry,
base_serializer_class=CreateBuilderWorkflowActionSerializer,
)
def post(self, request, data: Dict, page_id: int):
type_name = data.pop("type")
workflow_action_type = builder_workflow_action_type_registry.get(type_name)
page = PageHandler().get_page(page_id)
workflow_action = BuilderWorkflowActionService().create_workflow_action(
request.user, workflow_action_type, page, **data
)
serializer = builder_workflow_action_type_registry.get_serializer(
workflow_action, BuilderWorkflowActionSerializer
)
return Response(serializer.data)
@extend_schema(
parameters=[
OpenApiParameter(
name="page_id",
location=OpenApiParameter.PATH,
type=OpenApiTypes.INT,
description="Returns only the workflow actions of the page related to "
"the provided Id.",
)
],
tags=["Builder workflow_actions"],
operation_id="list_builder_page_workflow_actions",
description=(
"Lists all the workflow actions of the page related to the provided parameter "
"if the user has access to the related builder's workspace. "
"If the workspace is related to a template, then this endpoint will be "
"publicly accessible."
),
responses={
200: DiscriminatorCustomFieldsMappingSerializer(
builder_workflow_action_type_registry,
BuilderWorkflowActionSerializer,
many=True,
),
404: get_error_schema(["ERROR_PAGE_DOES_NOT_EXIST"]),
},
)
@map_exceptions(
{
PageDoesNotExist: ERROR_PAGE_DOES_NOT_EXIST,
}
)
def get(self, request, page_id: int):
page = PageHandler().get_page(page_id)
workflow_actions = BuilderWorkflowActionService().get_workflow_actions(
request.user, page
)
data = [
builder_workflow_action_type_registry.get_serializer(
workflow_action, BuilderWorkflowActionSerializer
).data
for workflow_action in workflow_actions
]
return Response(data)
class BuilderWorkflowActionView(APIView):
permission_classes = (IsAuthenticated,)
@extend_schema(
parameters=[
OpenApiParameter(
name="workflow_action_id",
location=OpenApiParameter.PATH,
type=OpenApiTypes.INT,
description="The id of the workflow action",
),
CLIENT_SESSION_ID_SCHEMA_PARAMETER,
],
tags=["Builder workflow_actions"],
operation_id="delete_builder_page_workflow_action",
description="Deletes the workflow action related by the given id.",
responses={
204: None,
400: get_error_schema(
[
"ERROR_REQUEST_BODY_VALIDATION",
]
),
404: get_error_schema(["ERROR_WORKFLOW_ACTION_DOES_NOT_EXIST"]),
},
)
@transaction.atomic
@map_exceptions(
{
WorkflowActionDoesNotExist: ERROR_WORKFLOW_ACTION_DOES_NOT_EXIST,
}
)
def delete(self, request, workflow_action_id: int):
workflow_action = BuilderWorkflowActionHandler().get_workflow_action(
workflow_action_id
)
BuilderWorkflowActionService().delete_workflow_action(
request.user, workflow_action
)
return Response(status=204)
@extend_schema(
parameters=[
OpenApiParameter(
name="workflow_action_id",
location=OpenApiParameter.PATH,
type=OpenApiTypes.INT,
description="The id of the workflow action",
),
CLIENT_SESSION_ID_SCHEMA_PARAMETER,
],
tags=["Builder workflow_actions"],
operation_id="update_builder_page_workflow_action",
description="Updates an existing builder workflow action.",
request=CustomFieldRegistryMappingSerializer(
builder_workflow_action_type_registry,
UpdateBuilderWorkflowActionsSerializer,
request=True,
),
responses={
200: DiscriminatorCustomFieldsMappingSerializer(
builder_workflow_action_type_registry, BuilderWorkflowActionSerializer
),
400: get_error_schema(
[
"ERROR_REQUEST_BODY_VALIDATION",
]
),
404: get_error_schema(
[
"ERROR_WORKFLOW_ACTION_DOES_NOT_EXIST",
]
),
},
)
@transaction.atomic
@map_exceptions(
{
WorkflowActionDoesNotExist: ERROR_WORKFLOW_ACTION_DOES_NOT_EXIST,
}
)
def patch(self, request, workflow_action_id: int):
workflow_action = BuilderWorkflowActionHandler().get_workflow_action(
workflow_action_id
)
workflow_action_type = type_from_data_or_registry(
request.data, builder_workflow_action_type_registry, workflow_action
)
data = validate_data_custom_fields(
workflow_action_type.type,
builder_workflow_action_type_registry,
request.data,
base_serializer_class=UpdateBuilderWorkflowActionsSerializer,
partial=True,
)
workflow_action_updated = BuilderWorkflowActionService().update_workflow_action(
request.user, workflow_action, **data
)
serializer = builder_workflow_action_type_registry.get_serializer(
workflow_action_updated, BuilderWorkflowActionSerializer
)
return Response(serializer.data)

View file

@ -20,6 +20,12 @@ from baserow.contrib.builder.pages.models import Page
from baserow.contrib.builder.pages.service import PageService
from baserow.contrib.builder.theme.registries import theme_config_block_registry
from baserow.contrib.builder.types import BuilderDict, DataSourceDict, PageDict
from baserow.contrib.builder.workflow_actions.handler import (
BuilderWorkflowActionHandler,
)
from baserow.contrib.builder.workflow_actions.registries import (
builder_workflow_action_type_registry,
)
from baserow.contrib.database.constants import IMPORT_SERIALIZED_IMPORTING
from baserow.core.db import specific_iterator
from baserow.core.integrations.models import Integration
@ -88,6 +94,15 @@ class BuilderApplicationType(ApplicationType):
element.get_type().export_serialized(element)
)
# Get serialized versions of all workflow actions of the current page
serialized_workflow_actions = []
for workflow_action in BuilderWorkflowActionHandler().get_workflow_actions(
page=page
):
serialized_workflow_actions.append(
workflow_action.get_type().export_serialized(workflow_action)
)
# Get serialized version of all data_sources for the current page
serialized_data_sources = []
for data_source in DataSourceHandler().get_data_sources(page=page):
@ -119,6 +134,7 @@ class BuilderApplicationType(ApplicationType):
path_params=page.path_params,
elements=serialized_elements,
data_sources=serialized_data_sources,
workflow_actions=serialized_workflow_actions,
)
)
@ -255,6 +271,13 @@ class BuilderApplicationType(ApplicationType):
for page in serialized_pages
]
)
+ sum(
[
# Inserting every workflow action
len(page["workflow_actions"])
for page in serialized_pages
]
)
)
def import_pages_serialized(
@ -296,6 +319,9 @@ class BuilderApplicationType(ApplicationType):
if "integrations" not in id_mapping:
id_mapping["integrations"] = {}
if "builder_workflow_actions" not in id_mapping:
id_mapping["builder_workflow_actions"] = {}
if "workspace_id" not in id_mapping and builder.workspace is not None:
id_mapping["workspace_id"] = builder.workspace.id
@ -315,6 +341,7 @@ class BuilderApplicationType(ApplicationType):
serialized_page["_object"] = page_instance
serialized_page["_element_objects"] = []
serialized_page["_data_source_objects"] = []
serialized_page["_workflow_action_objects"] = []
imported_pages.append(page_instance)
progress.increment(state=IMPORT_SERIALIZED_IMPORTING)
@ -370,7 +397,20 @@ class BuilderApplicationType(ApplicationType):
id_mapping,
)
progress.increment(state=IMPORT_SERIALIZED_IMPORTING)
progress.increment(state=IMPORT_SERIALIZED_IMPORTING)
# Then we create all the workflow actions
for serialized_page in serialized_pages:
for serialized_workflow_action in serialized_page["workflow_actions"]:
page = serialized_page["_object"]
workflow_action_type = builder_workflow_action_type_registry.get(
serialized_workflow_action["type"]
)
workflow_action_type.import_serialized(
page, serialized_workflow_action, id_mapping
)
progress.increment(state=IMPORT_SERIALIZED_IMPORTING)
return imported_pages

View file

@ -30,12 +30,16 @@ class BuilderConfig(AppConfig):
from baserow.contrib.builder.pages.object_scopes import (
BuilderPageObjectScopeType,
)
from baserow.contrib.builder.workflow_actions.object_scopes import (
BuilderWorkflowActionScopeType,
)
object_scope_type_registry.register(BuilderObjectScopeType())
object_scope_type_registry.register(BuilderPageObjectScopeType())
object_scope_type_registry.register(BuilderElementObjectScopeType())
object_scope_type_registry.register(BuilderDomainObjectScopeType())
object_scope_type_registry.register(BuilderDataSourceObjectScopeType())
object_scope_type_registry.register(BuilderWorkflowActionScopeType())
from baserow.contrib.builder.operations import (
ListDomainsBuilderOperationType,
@ -128,6 +132,20 @@ class BuilderConfig(AppConfig):
operation_type_registry.register(UpdateElementOperationType())
operation_type_registry.register(DeleteElementOperationType())
from baserow.contrib.builder.workflow_actions.operations import (
CreateBuilderWorkflowActionOperationType,
DeleteBuilderWorkflowActionOperationType,
ListBuilderWorkflowActionsPageOperationType,
ReadBuilderWorkflowActionOperationType,
UpdateBuilderWorkflowActionOperationType,
)
operation_type_registry.register(ListBuilderWorkflowActionsPageOperationType())
operation_type_registry.register(CreateBuilderWorkflowActionOperationType())
operation_type_registry.register(DeleteBuilderWorkflowActionOperationType())
operation_type_registry.register(UpdateBuilderWorkflowActionOperationType())
operation_type_registry.register(ReadBuilderWorkflowActionOperationType())
from baserow.core.registries import permission_manager_type_registry
from .domains.permission_manager import AllowPublicBuilderManagerType
@ -186,6 +204,15 @@ class BuilderConfig(AppConfig):
theme_config_block_registry.register(MainThemeConfigBlockType())
from .workflow_actions.registries import builder_workflow_action_type_registry
from .workflow_actions.workflow_action_types import (
NotificationWorkflowActionType,
OpenPageWorkflowActionType,
)
builder_workflow_action_type_registry.register(NotificationWorkflowActionType())
builder_workflow_action_type_registry.register(OpenPageWorkflowActionType())
from .domains.receivers import connect_to_domain_pre_delete_signal
connect_to_domain_pre_delete_signal()

View file

@ -17,7 +17,9 @@ from baserow.core.db import specific_iterator
from baserow.core.exceptions import IdDoesNotExist
from baserow.core.utils import MirrorDict, extract_allowed
from .types import ElementForUpdate
from ..workflow_actions.models import BuilderWorkflowAction
from ..workflow_actions.registries import builder_workflow_action_type_registry
from .types import ElementForUpdate, ElementsAndWorkflowActions
class ElementHandler:
@ -323,7 +325,18 @@ class ElementHandler:
Element.recalculate_full_orders(queryset=Element.objects.filter(page=page))
def duplicate_element(self, element: Element) -> List[Element]:
def get_element_workflow_actions(
self, element: Element
) -> Iterable[BuilderWorkflowAction]:
"""
Get all the workflow actions that belong to an element
:param element: The element associated with the workflow actions
:return: All the workflow actions associated
"""
return specific_iterator(element.builderworkflowaction_set.all())
def duplicate_element(self, element: Element) -> ElementsAndWorkflowActions:
"""
Duplicate an element in a recursive fashion. If the element has any children
they will also be imported using the same method and so will their children
@ -335,13 +348,13 @@ class ElementHandler:
# We are just creating new elements here so other data id should remain
id_mapping = defaultdict(lambda: MirrorDict())
id_mapping["builder_elements"] = {}
id_mapping["builder_page_elements"] = {}
return self._duplicate_element_recursive(element, id_mapping)
def _duplicate_element_recursive(
self, element: Element, id_mapping
) -> List[Element]:
) -> ElementsAndWorkflowActions:
"""
Duplicates an element and all of its children.
@ -360,12 +373,54 @@ class ElementHandler:
element.page, serialized, id_mapping
)
elements_duplicated = [element_duplicated]
workflow_actions_duplicated = self._duplicate_workflow_actions_of_element(
element, id_mapping
)
elements_and_workflow_actions_duplicated = {
"elements": [element_duplicated],
"workflow_actions": workflow_actions_duplicated,
}
for child in element.children.all():
children_duplicated = self._duplicate_element_recursive(
child.specific, id_mapping
)
elements_duplicated += children_duplicated
elements_and_workflow_actions_duplicated["elements"] += children_duplicated[
"elements"
]
elements_and_workflow_actions_duplicated[
"workflow_actions"
] += children_duplicated["workflow_actions"]
return elements_duplicated
return elements_and_workflow_actions_duplicated
def _duplicate_workflow_actions_of_element(
self,
element: Element,
id_mapping: MirrorDict,
) -> List[BuilderWorkflowAction]:
"""
This helper function duplicates all the workflow actions associated with the
element.
:param element: The original element
:param element_duplicated: The duplicated reference of the original element
"""
workflow_actions_duplicated = []
for workflow_action in self.get_element_workflow_actions(element):
workflow_action_type = builder_workflow_action_type_registry.get_by_model(
workflow_action
)
workflow_action_serialized = workflow_action_type.export_serialized(
workflow_action
)
workflow_action_duplicated = workflow_action_type.import_serialized(
element.page, workflow_action_serialized, id_mapping
)
workflow_actions_duplicated.append(workflow_action_duplicated)
return workflow_actions_duplicated

View file

@ -145,9 +145,12 @@ class ElementType(
serialized_copy.pop("type")
if serialized_copy.get("parent_element_id", None):
serialized_copy["parent_element_id"] = id_mapping["builder_page_elements"][
serialized_copy["parent_element_id"]
]
serialized_copy["parent_element_id"] = id_mapping[
"builder_page_elements"
].get(
serialized_copy["parent_element_id"],
serialized_copy["parent_element_id"],
)
element = self.model_class(page=page, **serialized_copy)
element.save()

View file

@ -21,7 +21,10 @@ from baserow.contrib.builder.elements.signals import (
element_updated,
elements_created,
)
from baserow.contrib.builder.elements.types import ElementForUpdate
from baserow.contrib.builder.elements.types import (
ElementForUpdate,
ElementsAndWorkflowActions,
)
from baserow.contrib.builder.pages.models import Page
from baserow.core.exceptions import CannotCalculateIntermediateOrder
from baserow.core.handler import CoreHandler
@ -245,7 +248,9 @@ class ElementService:
element_orders_recalculated.send(self, page=page)
def duplicate_element(self, user: AbstractUser, element: Element) -> List[Element]:
def duplicate_element(
self, user: AbstractUser, element: Element
) -> ElementsAndWorkflowActions:
"""
Duplicate an element in a recursive fashion. If the element has any children
they will also be imported using the same method and so will their children
@ -265,8 +270,15 @@ class ElementService:
context=page,
)
elements_duplicated = self.handler.duplicate_element(element)
elements_and_workflow_actions_duplicated = self.handler.duplicate_element(
element
)
elements_created.send(self, elements=elements_duplicated, user=user, page=page)
elements_created.send(
self,
elements=elements_and_workflow_actions_duplicated["elements"],
user=user,
page=page,
)
return elements_duplicated
return elements_and_workflow_actions_duplicated

View file

@ -1,10 +1,16 @@
from typing import NewType, TypeVar
from typing import List, NewType, TypedDict, TypeVar
from baserow.contrib.builder.types import ElementDict
from ..workflow_actions.models import BuilderWorkflowAction
from .models import Element
ElementDictSubClass = TypeVar("ElementDictSubClass", bound=ElementDict)
ElementSubClass = TypeVar("ElementSubClass", bound=Element)
ElementForUpdate = NewType("ElementForUpdate", Element)
class ElementsAndWorkflowActions(TypedDict):
elements: List[Element]
workflow_actions: List[BuilderWorkflowAction]

View file

@ -0,0 +1,118 @@
# Generated by Django 3.2.21 on 2023-10-05 11:14
import django.db.models.deletion
from django.db import migrations, models
import baserow.core.fields
import baserow.core.formula.field
import baserow.core.mixins
class Migration(migrations.Migration):
dependencies = [
("contenttypes", "0002_remove_content_type_name"),
("builder", "0023_headingelement_font_color"),
]
operations = [
migrations.CreateModel(
name="BuilderWorkflowAction",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created_on", models.DateTimeField(auto_now_add=True)),
("updated_on", baserow.core.fields.SyncedDateTimeField(auto_now=True)),
(
"event",
models.CharField(
choices=[("click", "Click")],
help_text="The event that triggers the execution",
max_length=30,
),
),
(
"content_type",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="builder_workflow_actions",
to="contenttypes.contenttype",
verbose_name="content type",
),
),
(
"element",
models.ForeignKey(
default=None,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="builder.element",
),
),
(
"page",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="builder.page"
),
),
],
options={
"abstract": False,
},
bases=(
baserow.core.mixins.PolymorphicContentTypeMixin,
baserow.core.mixins.OrderableMixin,
models.Model,
baserow.core.mixins.WithRegistry,
),
),
migrations.CreateModel(
name="NotificationWorkflowAction",
fields=[
(
"builderworkflowaction_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="builder.builderworkflowaction",
),
),
("title", baserow.core.formula.field.FormulaField(default="")),
("description", baserow.core.formula.field.FormulaField(default="")),
],
options={
"abstract": False,
},
bases=("builder.builderworkflowaction",),
),
migrations.CreateModel(
name="OpenPageWorkflowAction",
fields=[
(
"builderworkflowaction_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="builder.builderworkflowaction",
),
),
("url", baserow.core.formula.field.FormulaField(default="")),
],
options={
"abstract": False,
},
bases=("builder.builderworkflowaction",),
),
]

View file

@ -3,6 +3,7 @@ from typing import List, Optional, TypedDict
from baserow.contrib.builder.pages.types import PagePathParams
from baserow.core.integrations.types import IntegrationDictSubClass
from baserow.core.services.types import ServiceDictSubClass
from baserow.core.workflow_actions.models import WorkflowAction
class ElementDict(TypedDict):
@ -30,6 +31,7 @@ class PageDict(TypedDict):
path_params: PagePathParams
elements: List[ElementDict]
data_sources: List[DataSourceDict]
workflow_actions: List[WorkflowAction]
class BuilderDict(TypedDict):

View file

@ -0,0 +1,46 @@
from typing import Iterable, Optional
from django.db.models import QuerySet
from baserow.contrib.builder.pages.models import Page
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.workflow_actions.handler import WorkflowActionHandler
from baserow.core.workflow_actions.models import WorkflowAction
class BuilderWorkflowActionHandler(WorkflowActionHandler):
model = BuilderWorkflowAction
registry = builder_workflow_action_type_registry
def get_workflow_actions(
self, page: Page, base_queryset: Optional[QuerySet] = None
) -> Iterable[WorkflowAction]:
"""
Get all the workflow actions of an page
:param page: The page associated with the workflow actions
:param base_queryset: Optional base queryset to filter the results
:return: A list of workflow actions
"""
if base_queryset is None:
base_queryset = self.model.objects
base_queryset = base_queryset.filter(page=page)
return super().get_all_workflow_actions(base_queryset)
def update_workflow_action(
self, workflow_action: BuilderWorkflowAction, **kwargs
) -> WorkflowAction:
# When we are switching types we want to preserve the event and element and
# page ids
if "type" in kwargs and kwargs["type"] != workflow_action.get_type().type:
kwargs["page_id"] = workflow_action.page_id
kwargs["element_id"] = workflow_action.element_id
kwargs["event"] = workflow_action.event
return super().update_workflow_action(workflow_action, **kwargs)

View file

@ -0,0 +1,49 @@
from django.contrib.contenttypes.models import ContentType
from django.db import models
from baserow.contrib.builder.elements.models import Element
from baserow.contrib.builder.pages.models import Page
from baserow.contrib.builder.workflow_actions.registries import (
builder_workflow_action_type_registry,
)
from baserow.core.formula.field import FormulaField
from baserow.core.registry import ModelRegistryMixin
from baserow.core.workflow_actions.models import WorkflowAction
class EventTypes(models.TextChoices):
CLICK = "click"
class BuilderWorkflowAction(WorkflowAction):
content_type = models.ForeignKey(
ContentType,
verbose_name="content type",
related_name="builder_workflow_actions",
on_delete=models.CASCADE,
)
event = models.CharField(
max_length=30,
choices=EventTypes.choices,
help_text="The event that triggers the execution",
)
page = models.ForeignKey(Page, on_delete=models.CASCADE)
element = models.ForeignKey(
Element, on_delete=models.CASCADE, null=True, default=None
)
@staticmethod
def get_type_registry() -> ModelRegistryMixin:
return builder_workflow_action_type_registry
def get_parent(self):
return self.page
class NotificationWorkflowAction(BuilderWorkflowAction):
title = FormulaField(default="")
description = FormulaField(default="")
class OpenPageWorkflowAction(BuilderWorkflowAction):
url = FormulaField(default="")

View file

@ -0,0 +1,42 @@
from typing import Optional
from django.db.models import Q
from baserow.contrib.builder.elements.object_scopes import BuilderElementObjectScopeType
from baserow.contrib.builder.object_scopes import BuilderObjectScopeType
from baserow.contrib.builder.pages.object_scopes import BuilderPageObjectScopeType
from baserow.contrib.builder.workflow_actions.models import BuilderWorkflowAction
from baserow.core.object_scopes import (
ApplicationObjectScopeType,
WorkspaceObjectScopeType,
)
from baserow.core.registries import ObjectScopeType, object_scope_type_registry
class BuilderWorkflowActionScopeType(ObjectScopeType):
type = "builder_workflow_action"
model_class = BuilderWorkflowAction
def get_parent_scope(self) -> Optional["ObjectScopeType"]:
return object_scope_type_registry.get("builder_element")
def get_filter_for_scope_type(self, scope_type, scopes):
if scope_type.type == WorkspaceObjectScopeType.type:
return Q(element__page__builder__workspace__in=[s.id for s in scopes])
if (
scope_type.type == BuilderObjectScopeType.type
or scope_type.type == ApplicationObjectScopeType.type
):
return Q(element__page__builder__in=[s.id for s in scopes])
if scope_type.type == BuilderPageObjectScopeType.type:
return Q(element__page__in=[s.id for s in scopes])
if scope_type.type == BuilderElementObjectScopeType.type:
return Q(element__in=[s.id for s in scopes])
if scope_type.type == self.type:
return Q(id__in=[s.id for s in scopes])
raise TypeError("The given type is not handled.")

View file

@ -0,0 +1,29 @@
from abc import ABC
from baserow.contrib.builder.pages.operations import BuilderPageOperationType
from baserow.core.registries import OperationType
class ListBuilderWorkflowActionsPageOperationType(BuilderPageOperationType):
type = "builder.page.list_workflow_actions"
object_scope_name = "builder_workflow_action"
class CreateBuilderWorkflowActionOperationType(BuilderPageOperationType):
type = "builder.page.create_workflow_action"
class BuilderWorkflowActionOperationType(OperationType, ABC):
context_scope_name = "builder_workflow_action"
class DeleteBuilderWorkflowActionOperationType(BuilderWorkflowActionOperationType):
type = "builder.page.workflow_action.delete"
class UpdateBuilderWorkflowActionOperationType(BuilderWorkflowActionOperationType):
type = "builder.page.workflow_action.update"
class ReadBuilderWorkflowActionOperationType(BuilderWorkflowActionOperationType):
type = "builder.page.workflow_action.read"

View file

@ -0,0 +1,18 @@
from baserow.core.registry import (
CustomFieldsRegistryMixin,
ModelRegistryMixin,
Registry,
)
class BuilderWorkflowActionTypeRegistry(
Registry, ModelRegistryMixin, CustomFieldsRegistryMixin
):
"""
Contains all the registered workflow action types for the builder module.
"""
name = "builder_workflow_action_type"
builder_workflow_action_type_registry = BuilderWorkflowActionTypeRegistry()

View file

@ -0,0 +1,175 @@
from typing import List
from django.contrib.auth.models import AbstractUser
from baserow.contrib.builder.pages.models import Page
from baserow.contrib.builder.workflow_actions.handler import (
BuilderWorkflowActionHandler,
)
from baserow.contrib.builder.workflow_actions.models import (
BuilderWorkflowAction,
WorkflowAction,
)
from baserow.contrib.builder.workflow_actions.operations import (
CreateBuilderWorkflowActionOperationType,
DeleteBuilderWorkflowActionOperationType,
ListBuilderWorkflowActionsPageOperationType,
ReadBuilderWorkflowActionOperationType,
UpdateBuilderWorkflowActionOperationType,
)
from baserow.contrib.builder.workflow_actions.signals import (
workflow_action_created,
workflow_action_deleted,
workflow_action_updated,
)
from baserow.contrib.builder.workflow_actions.workflow_action_types import (
BuilderWorkflowActionType,
)
from baserow.core.handler import CoreHandler
class BuilderWorkflowActionService:
def __init__(self):
self.handler = BuilderWorkflowActionHandler()
def get_workflow_action(
self, user: AbstractUser, workflow_action_id: int
) -> WorkflowAction:
"""
Returns an workflow_action instance from the database. Also checks the user
permissions.
:param user: The user trying to get the workflow_action
:param workflow_action_id: The ID of the workflow_action
:return: The workflow_action instance
"""
workflow_action = self.handler.get_workflow_action(workflow_action_id)
CoreHandler().check_permissions(
user,
ReadBuilderWorkflowActionOperationType.type,
workspace=workflow_action.page.builder.workspace,
context=workflow_action,
)
return workflow_action
def get_workflow_actions(
self,
user: AbstractUser,
page: Page,
) -> List[WorkflowAction]:
"""
Gets all the workflow_actions of a given page visible to the given user.
:param user: The user trying to get the workflow_actions.
:param page: The page that holds the workflow_actions.
:return: The workflow_actions of that page.
"""
CoreHandler().check_permissions(
user,
ListBuilderWorkflowActionsPageOperationType.type,
workspace=page.builder.workspace,
context=page,
)
user_workflow_actions = CoreHandler().filter_queryset(
user,
ListBuilderWorkflowActionsPageOperationType.type,
BuilderWorkflowAction.objects.all(),
workspace=page.builder.workspace,
context=page,
)
return self.handler.get_workflow_actions(
page, base_queryset=user_workflow_actions
)
def create_workflow_action(
self,
user: AbstractUser,
workflow_action_type: BuilderWorkflowActionType,
page: Page,
**kwargs,
) -> WorkflowAction:
"""
Creates a new workflow_action for a page given the user permissions.
:param user: The user trying to create the workflow_action.
:param workflow_action_type: The type of the workflow_action.
:param page: The page the workflow_action is associated with.
:param kwargs: Additional attributes of the workflow_action.
:return: The created workflow_action.
"""
CoreHandler().check_permissions(
user,
CreateBuilderWorkflowActionOperationType.type,
workspace=page.builder.workspace,
context=page,
)
new_workflow_action = self.handler.create_workflow_action(
workflow_action_type, page=page, **kwargs
)
workflow_action_created.send(
self,
workflow_action=new_workflow_action,
user=user,
)
return new_workflow_action
def update_workflow_action(
self, user: AbstractUser, workflow_action: WorkflowAction, **kwargs
) -> WorkflowAction:
"""
Updates and workflow_action with values. Will also check if the values are
allowed to be set on the workflow_action first.
:param user: The user trying to update the workflow_action.
:param workflow_action: The workflow_action that should be updated.
:param kwargs: Additional attributes of the workflow_action.
:return: The updated workflow_action.
"""
CoreHandler().check_permissions(
user,
UpdateBuilderWorkflowActionOperationType.type,
workspace=workflow_action.page.builder.workspace,
context=workflow_action,
)
workflow_action = self.handler.update_workflow_action(workflow_action, **kwargs)
workflow_action_updated.send(self, workflow_action=workflow_action, user=user)
return workflow_action
def delete_workflow_action(
self, user: AbstractUser, workflow_action: WorkflowAction
):
"""
Deletes a workflow_action.
:param user: The user trying to delete the workflow_action.
:param workflow_action: The to-be-deleted workflow_action.
"""
page = workflow_action.page
CoreHandler().check_permissions(
user,
DeleteBuilderWorkflowActionOperationType.type,
workspace=page.builder.workspace,
context=workflow_action,
)
self.handler.delete_workflow_action(workflow_action)
workflow_action_deleted.send(
self, workflow_action_id=workflow_action.id, page=page, user=user
)

View file

@ -0,0 +1,5 @@
from django.dispatch import Signal
workflow_action_created = Signal()
workflow_action_deleted = Signal()
workflow_action_updated = Signal()

View file

@ -0,0 +1,7 @@
from baserow.core.workflow_actions.types import WorkflowActionDict
class BuilderWorkflowActionDict(WorkflowActionDict):
page_id: int
element_id: int
event: str

View file

@ -0,0 +1,117 @@
from abc import abstractmethod
from typing import TYPE_CHECKING, Any, Dict
from baserow.contrib.builder.elements.handler import ElementHandler
from baserow.contrib.builder.workflow_actions.models import (
BuilderWorkflowAction,
NotificationWorkflowAction,
OpenPageWorkflowAction,
)
from baserow.contrib.builder.workflow_actions.types import BuilderWorkflowActionDict
from baserow.core.formula.serializers import FormulaSerializerField
from baserow.core.formula.types import BaserowFormula
from baserow.core.workflow_actions.registries import WorkflowActionType
if TYPE_CHECKING:
from baserow.contrib.builder.pages.models import Page
class BuilderWorkflowActionType(WorkflowActionType):
allowed_fields = ["page", "page_id", "element", "element_id", "event"]
def prepare_value_for_db(
self, values: Dict, instance: BuilderWorkflowAction = None
):
if "element_id" in values:
values["element"] = ElementHandler().get_element(values["element_id"])
return super().prepare_value_for_db(values, instance=instance)
@abstractmethod
def get_sample_params(self) -> Dict[str, Any]:
pass
def import_serialized(
self, page: "Page", serialized_values: Dict[str, Any], id_mapping: Dict
) -> BuilderWorkflowAction:
if "builder_workflow_actions" not in id_mapping:
id_mapping["builder_workflow_actions"] = {}
serialized_copy = serialized_values.copy()
# Remove extra keys
workflow_action_id = serialized_copy.pop("id")
serialized_copy.pop("type")
# Convert table id
serialized_copy["page_id"] = id_mapping["builder_pages"][
serialized_copy["page_id"]
]
# Convert element id
if "element_id" in serialized_copy:
serialized_copy["element_id"] = id_mapping["builder_page_elements"][
serialized_copy["element_id"]
]
workflow_action = self.model_class(page=page, **serialized_copy)
workflow_action.save()
id_mapping["builder_workflow_actions"][workflow_action_id] = workflow_action.id
return workflow_action
class NotificationWorkflowActionType(BuilderWorkflowActionType):
type = "notification"
model_class = NotificationWorkflowAction
serializer_field_names = ["title", "description"]
serializer_field_overrides = {
"title": FormulaSerializerField(
help_text="The title of the notification. Must be an formula.",
required=False,
allow_blank=True,
default="",
),
"description": FormulaSerializerField(
help_text="The description of the notification. Must be an formula.",
required=False,
allow_blank=True,
default="",
),
}
class SerializedDict(BuilderWorkflowActionDict):
title: BaserowFormula
description: BaserowFormula
@property
def allowed_fields(self):
return super().allowed_fields + ["title", "description"]
def get_sample_params(self) -> Dict[str, Any]:
return {"title": "'hello'", "description": "'there'"}
class OpenPageWorkflowActionType(BuilderWorkflowActionType):
type = "open_page"
model_class = OpenPageWorkflowAction
serializer_field_names = ["url"]
serializer_field_overrides = {
"url": FormulaSerializerField(
help_text="The url to open. Must be an formula.",
required=False,
allow_blank=True,
default="",
),
}
class SerializedDict(BuilderWorkflowActionDict):
url: BaserowFormula
@property
def allowed_fields(self):
return super().allowed_fields + ["url"]
def get_sample_params(self) -> Dict[str, Any]:
return {"url": "'hello'"}

View file

@ -0,0 +1,2 @@
class WorkflowActionDoesNotExist(Exception):
"""Raised when trying to get a workflow action that doesn't exist"""

View file

@ -0,0 +1,129 @@
from abc import ABC, abstractmethod
from typing import Iterable, Optional, Type, cast
from django.db.models import QuerySet
from baserow.core.db import specific_iterator
from baserow.core.registry import Registry
from baserow.core.utils import extract_allowed
from baserow.core.workflow_actions.exceptions import WorkflowActionDoesNotExist
from baserow.core.workflow_actions.models import WorkflowAction
from baserow.core.workflow_actions.registries import WorkflowActionType
class WorkflowActionHandler(ABC):
"""
This is an abstract handler, each module that wants to use workflow actions will
need to implement their own handler.
"""
@property
@abstractmethod
def model(self) -> Type[WorkflowAction]:
pass
@property
@abstractmethod
def registry(self) -> Registry:
pass
def get_workflow_action(self, workflow_action_id: int) -> WorkflowAction:
"""
Returns a workflow action from the database.
The queryset here is not optional since every module needs to provide their
own model at least.
:param workflow_action_id: The ID of the workflow action.
:return: The workflow action instance.
"""
try:
return self.model.objects.get(id=workflow_action_id).specific
except self.model.DoesNotExist:
raise WorkflowActionDoesNotExist()
def get_all_workflow_actions(
self, base_queryset: Optional[QuerySet] = None
) -> Iterable[WorkflowAction]:
"""
Gets all the workflow actions of the defined model.
:param base_queryset: A query set that lets you prefilter the results
:return: A list of workflow actions
"""
if base_queryset is None:
base_queryset = self.model.objects
return specific_iterator(base_queryset)
def create_workflow_action(
self, workflow_action_type: WorkflowActionType, **kwargs
) -> WorkflowAction:
"""
Creates a new workflow action of the given type.
:param workflow_action_type: The type of the new workflow action
:param kwargs: Any fields that need to be set for that specific type
:return: The created workflow action
"""
allowed_values = extract_allowed(kwargs, workflow_action_type.allowed_fields)
allowed_values = workflow_action_type.prepare_value_for_db(allowed_values)
model_class = cast(WorkflowAction, workflow_action_type.model_class)
workflow_action = model_class(**allowed_values)
workflow_action.save()
return workflow_action
def delete_workflow_action(self, workflow_action: WorkflowAction):
"""
Deletes a given workflow action.
:param workflow_action: The workflow action to be deleted
"""
workflow_action.delete()
def update_workflow_action(
self, workflow_action: WorkflowAction, **kwargs
) -> WorkflowAction:
"""
Update an existing workflow action.
:param workflow_action: The workflow action you want to update.
:param kwargs: The updates you wish to perform on the workflow action.
:return: The updated workflow action.
"""
has_type_changed = (
"type" in kwargs and kwargs["type"] != workflow_action.get_type().type
)
if has_type_changed:
workflow_action_type = self.registry.get(kwargs["type"])
else:
workflow_action_type = workflow_action.get_type()
allowed_updates = extract_allowed(kwargs, workflow_action_type.allowed_fields)
allowed_updates = workflow_action_type.prepare_value_for_db(
allowed_updates, instance=workflow_action
)
if has_type_changed:
self.delete_workflow_action(workflow_action)
workflow_action = self.create_workflow_action(
workflow_action_type, **allowed_updates
)
else:
for key, value in allowed_updates.items():
setattr(workflow_action, key, value)
workflow_action.save()
return workflow_action.specific

View file

@ -0,0 +1,28 @@
from django.db import models
from baserow.core.mixins import (
CreatedAndUpdatedOnMixin,
HierarchicalModelMixin,
OrderableMixin,
PolymorphicContentTypeMixin,
WithRegistry,
)
from baserow.core.registry import ModelRegistryMixin
class WorkflowAction(
PolymorphicContentTypeMixin,
CreatedAndUpdatedOnMixin,
HierarchicalModelMixin,
OrderableMixin,
models.Model,
WithRegistry,
):
@staticmethod
def get_type_registry() -> ModelRegistryMixin:
raise Exception(
"Needs to be implement by module specific workflow actions parent"
)
class Meta:
abstract = True

View file

@ -0,0 +1,66 @@
from abc import ABC, abstractmethod
from typing import Any, Dict, Type
from baserow.core.registry import (
CustomFieldsInstanceMixin,
ImportExportMixin,
Instance,
ModelInstanceMixin,
)
from baserow.core.workflow_actions.models import WorkflowAction
from baserow.core.workflow_actions.types import WorkflowActionDictSubClass
class WorkflowActionType(
Instance, ModelInstanceMixin, ImportExportMixin, CustomFieldsInstanceMixin, ABC
):
SerializedDict: Type[WorkflowActionDictSubClass]
def export_serialized(self, instance: WorkflowAction) -> Dict[str, Any]:
property_names = self.SerializedDict.__annotations__.keys()
serialized = self.SerializedDict(
**{
key: self.get_property_for_serialization(instance, key)
for key in property_names
}
)
return serialized
def get_property_for_serialization(
self, workflow_action: WorkflowAction, prop_name: str
):
"""
You can customize the behavior of the serialization of a property with this
hook.
"""
if prop_name == "type":
return self.type
return getattr(workflow_action, prop_name)
@abstractmethod
def import_serialized(
self, parent: Any, serialized_values: Dict[str, Any], id_mapping: Dict
) -> WorkflowAction:
pass
def prepare_value_for_db(self, values: Dict, instance: WorkflowAction = None):
"""
A hook which can be called when before a workflow action is created or updated
:param values: The values that are about to be set on the workflow action
:param instance: The current instance (only when an update happens)
:return: The prepared values
"""
return values
@abstractmethod
def get_sample_params(self) -> Dict[str, Any]:
"""
Returns a sample of params for this type. This can be used to tests the element
for instance.
"""

View file

@ -0,0 +1,14 @@
from typing import TypedDict, TypeVar
from baserow.core.workflow_actions.models import WorkflowAction
class WorkflowActionDict(TypedDict):
id: int
type: str
WorkflowActionDictSubClass = TypeVar(
"WorkflowActionDictSubClass", bound=WorkflowActionDict
)
WorkflowActionSubClass = TypeVar("WorkflowActionSubClass", bound=WorkflowAction)

View file

@ -23,6 +23,7 @@ from .user import UserFixtures
from .user_file import UserFileFixtures
from .view import ViewFixtures
from .webhook import TableWebhookFixture
from .workflow_action import WorkflowActionFixture
from .workspace import WorkspaceFixtures
@ -51,6 +52,7 @@ class Fixtures(
ServiceFixtures,
DataSourceFixtures,
NotificationsFixture,
WorkflowActionFixture,
):
def __init__(self, fake=None):
self.fake = fake

View file

@ -1,6 +1,7 @@
from copy import deepcopy
from baserow.contrib.builder.elements.models import (
ButtonElement,
CollectionElementField,
ColumnElement,
HeadingElement,
@ -62,6 +63,10 @@ class ElementFixtures:
return element
def create_builder_button_element(self, user=None, page=None, **kwargs):
element = self.create_builder_element(ButtonElement, user, page, **kwargs)
return element
def create_builder_element(self, model_class, user=None, page=None, **kwargs):
if user is None:
user = self.create_user()
@ -74,6 +79,6 @@ class ElementFixtures:
if "order" not in kwargs:
kwargs["order"] = model_class.get_last_order(page)
page = model_class.objects.create(page=page, **kwargs)
element = model_class.objects.create(page=page, **kwargs)
return page
return element

View file

@ -0,0 +1,9 @@
from baserow.contrib.builder.workflow_actions.models import NotificationWorkflowAction
class WorkflowActionFixture:
def create_notification_workflow_action(self, **kwargs):
return self.create_workflow_action(NotificationWorkflowAction, **kwargs)
def create_workflow_action(self, model_class, **kwargs):
return model_class.objects.create(**kwargs)

View file

@ -426,8 +426,8 @@ def test_duplicate_element(api_client, data_fixture):
response_json = response.json()
assert response.status_code == HTTP_200_OK
assert response_json[0]["id"] != element.id
assert response_json[0]["value"] == element.value
assert response_json["elements"][0]["id"] != element.id
assert response_json["elements"][0]["value"] == element.value
@pytest.mark.django_db

View file

@ -0,0 +1,146 @@
from django.urls import reverse
import pytest
from rest_framework.status import (
HTTP_200_OK,
HTTP_204_NO_CONTENT,
HTTP_400_BAD_REQUEST,
HTTP_404_NOT_FOUND,
)
from baserow.contrib.builder.workflow_actions.workflow_action_types import (
NotificationWorkflowActionType,
)
@pytest.mark.django_db
def test_create_workflow_action(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_type = NotificationWorkflowActionType.type
url = reverse("api:builder:workflow_action:list", kwargs={"page_id": page.id})
response = api_client.post(
url,
{"type": workflow_action_type, "event": "click", "element_id": element.id},
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
response_json = response.json()
assert response.status_code == HTTP_200_OK
assert response_json["type"] == workflow_action_type
assert response_json["element_id"] == element.id
@pytest.mark.django_db
def test_create_workflow_action_page_does_not_exist(api_client, data_fixture):
user, token = data_fixture.create_user_and_token()
workflow_action_type = NotificationWorkflowActionType.type
url = reverse("api:builder:workflow_action:list", kwargs={"page_id": 99999})
response = api_client.post(
url,
{"type": workflow_action_type, "event": "click"},
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
assert response.status_code == HTTP_404_NOT_FOUND
@pytest.mark.django_db(transaction=True)
def test_create_workflow_action_element_does_not_exist(api_client, data_fixture):
user, token = data_fixture.create_user_and_token()
page = data_fixture.create_builder_page(user=user)
workflow_action_type = NotificationWorkflowActionType.type
url = reverse("api:builder:workflow_action:list", kwargs={"page_id": page.id})
response = api_client.post(
url,
{"type": workflow_action_type, "event": "click", "element_id": 9999},
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
assert response.status_code == HTTP_404_NOT_FOUND
@pytest.mark.django_db(transaction=True)
def test_create_workflow_action_event_does_not_exist(api_client, data_fixture):
user, token = data_fixture.create_user_and_token()
page = data_fixture.create_builder_page(user=user)
workflow_action_type = NotificationWorkflowActionType.type
url = reverse("api:builder:workflow_action:list", kwargs={"page_id": page.id})
response = api_client.post(
url,
{"type": workflow_action_type, "event": "invalid"},
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
assert response.status_code == HTTP_400_BAD_REQUEST
@pytest.mark.django_db
def test_get_workflow_actions(api_client, data_fixture):
user, token = data_fixture.create_user_and_token()
page = data_fixture.create_builder_page(user=user)
workflow_action_one = data_fixture.create_notification_workflow_action(page=page)
workflow_action_two = data_fixture.create_notification_workflow_action(page=page)
url = reverse("api:builder:workflow_action:list", kwargs={"page_id": page.id})
response = api_client.get(
url,
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
response_json = response.json()
assert response.status_code == HTTP_200_OK
assert len(response_json) == 2
assert response_json[0]["id"] == workflow_action_one.id
assert response_json[1]["id"] == workflow_action_two.id
@pytest.mark.django_db
def test_delete_workflow_actions(api_client, data_fixture):
user, token = data_fixture.create_user_and_token()
page = data_fixture.create_builder_page(user=user)
workflow_action = data_fixture.create_notification_workflow_action(page=page)
url = reverse(
"api:builder:workflow_action:item",
kwargs={"workflow_action_id": workflow_action.id},
)
response = api_client.delete(
url,
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
assert response.status_code == HTTP_204_NO_CONTENT
@pytest.mark.django_db
def test_patch_workflow_actions(api_client, data_fixture):
user, token = data_fixture.create_user_and_token()
page = data_fixture.create_builder_page(user=user)
workflow_action = data_fixture.create_notification_workflow_action(page=page)
url = reverse(
"api:builder:workflow_action:item",
kwargs={"workflow_action_id": workflow_action.id},
)
response = api_client.patch(
url,
{"description": "'hello'"},
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
response_json = response.json()
assert response.status_code == HTTP_200_OK
assert response_json["description"] == "'hello'"

View file

@ -368,7 +368,7 @@ def test_before_places_in_container_removed_no_change(data_fixture):
def test_duplicate_element_single_element(data_fixture):
element = data_fixture.create_builder_paragraph_element(value="test")
[element_duplicated] = ElementHandler().duplicate_element(element)
[element_duplicated] = ElementHandler().duplicate_element(element)["elements"]
assert element.id != element_duplicated.id
assert element.value == element_duplicated.value
@ -389,7 +389,7 @@ def test_duplicate_element_multiple_elements(data_fixture):
container_element_duplicated,
child_duplicated,
child_two_duplicated,
] = ElementHandler().duplicate_element(container_element)
] = ElementHandler().duplicate_element(container_element)["elements"]
assert container_element.id != container_element_duplicated.id
assert container_element.column_amount == container_element_duplicated.column_amount
@ -421,7 +421,7 @@ def test_duplicate_element_deeply_nested(data_fixture):
container_element_duplicated,
child_first_level_duplicated,
child_second_level_duplicated,
] = ElementHandler().duplicate_element(container_element)
] = ElementHandler().duplicate_element(container_element)["elements"]
assert container_element.id != container_element_duplicated.id
assert container_element.column_amount == container_element_duplicated.column_amount
@ -441,3 +441,70 @@ def test_duplicate_element_deeply_nested(data_fixture):
child_second_level_duplicated.parent_element_id
== child_first_level_duplicated.id
)
@pytest.mark.django_db
def test_duplicate_element_with_workflow_action(data_fixture):
page = data_fixture.create_builder_page()
element = data_fixture.create_builder_button_element(page=page)
workflow_action = data_fixture.create_notification_workflow_action(
page=page, element=element
)
result = ElementHandler().duplicate_element(element)
[element_duplicated] = result["elements"]
[duplicated_workflow_action] = result["workflow_actions"]
assert duplicated_workflow_action.id != workflow_action.id
assert duplicated_workflow_action.page_id == workflow_action.page_id
assert duplicated_workflow_action.element_id == element_duplicated.id
@pytest.mark.django_db
def test_get_element_workflow_actions(data_fixture):
page = data_fixture.create_builder_page()
element = data_fixture.create_builder_button_element()
workflow_action_one = data_fixture.create_notification_workflow_action(
page=page, element=element
)
workflow_action_two = data_fixture.create_notification_workflow_action(
page=page, element=element
)
[
workflow_action_one_returned,
workflow_action_two_returned,
] = ElementHandler().get_element_workflow_actions(element)
assert workflow_action_one.id == workflow_action_one_returned.id
assert workflow_action_two.id == workflow_action_two_returned.id
@pytest.mark.django_db
def test_duplicate_element_with_workflow_action_in_container(data_fixture):
page = data_fixture.create_builder_page()
container_element = data_fixture.create_builder_column_element(
column_amount=2, page=page
)
first_child = data_fixture.create_builder_button_element(
parent_element=container_element, page=page
)
second_child = data_fixture.create_builder_button_element(
parent_element=container_element, page=page
)
workflow_action1 = data_fixture.create_notification_workflow_action(
page=page, element=first_child
)
workflow_action2 = data_fixture.create_notification_workflow_action(
page=page, element=second_child
)
result = ElementHandler().duplicate_element(container_element)
[duplicated_workflow_action1, duplicated_workflow_action2] = result[
"workflow_actions"
]
assert duplicated_workflow_action1.page_id == workflow_action1.page_id
assert duplicated_workflow_action2.page_id == workflow_action2.page_id

View file

@ -340,7 +340,7 @@ def test_duplicate_element(elements_created_mock, data_fixture):
user = data_fixture.create_user()
element = data_fixture.create_builder_heading_element(user=user)
elements_duplicated = ElementService().duplicate_element(user, element)
elements_duplicated = ElementService().duplicate_element(user, element)["elements"]
assert elements_created_mock.called_with(
elements=elements_duplicated, user=user, page=element.page

View file

@ -11,6 +11,10 @@ from baserow.contrib.builder.elements.models import (
from baserow.contrib.builder.elements.registries import element_type_registry
from baserow.contrib.builder.models import Builder
from baserow.contrib.builder.pages.models import Page
from baserow.contrib.builder.workflow_actions.handler import (
BuilderWorkflowActionHandler,
)
from baserow.contrib.builder.workflow_actions.models import EventTypes
from baserow.core.db import specific_iterator
from baserow.core.registries import ImportExportConfig
from baserow.core.trash.handler import TrashHandler
@ -66,6 +70,14 @@ def test_builder_application_export(data_fixture):
page=page2, data_source=datasource3
)
workflow_action_1 = data_fixture.create_notification_workflow_action(
page=page1,
element=element1,
event=EventTypes.CLICK,
description="hello",
title="there",
)
serialized = BuilderApplicationType().export_serialized(
builder, ImportExportConfig(include_permission_data=True)
)
@ -78,6 +90,17 @@ def test_builder_application_export(data_fixture):
"order": page1.order,
"path": page1.path,
"path_params": page1.path_params,
"workflow_actions": [
{
"id": workflow_action_1.id,
"type": "notification",
"element_id": element1.id,
"event": EventTypes.CLICK.value,
"page_id": page1.id,
"description": "hello",
"title": "there",
}
],
"data_sources": [
{
"id": datasource1.id,
@ -146,6 +169,7 @@ def test_builder_application_export(data_fixture):
"order": page2.order,
"path": page2.path,
"path_params": page2.path_params,
"workflow_actions": [],
"data_sources": [
{
"id": datasource2.id,
@ -240,6 +264,16 @@ IMPORT_REFERENCE = {
"order": 1,
"path": "/test",
"path_params": {},
"workflow_actions": [
{
"id": 123,
"page_id": 999,
"element_id": 998,
"type": "notification",
"description": "hello",
"title": "there",
}
],
"elements": [
{
"id": 998,
@ -331,6 +365,7 @@ IMPORT_REFERENCE = {
"order": 2,
"path": "/test2",
"path_params": {},
"workflow_actions": [],
"elements": [
{
"id": 997,
@ -460,6 +495,12 @@ def test_builder_application_import(data_fixture):
assert element_inside_container.parent_element.specific == container_element
assert element_inside_container2.parent_element.specific == container_element
[workflow_action] = BuilderWorkflowActionHandler().get_workflow_actions(page1)
assert workflow_action.element_id == element1.id
assert workflow_action.description == "hello"
assert workflow_action.title == "there"
@pytest.mark.django_db
def test_delete_builder_application_with_published_builder(data_fixture):

View file

@ -0,0 +1,56 @@
import pytest
from baserow.contrib.builder.workflow_actions.registries import (
builder_workflow_action_type_registry,
)
from baserow.core.workflow_actions.registries import WorkflowActionType
def pytest_generate_tests(metafunc):
if "workflow_action_type" in metafunc.fixturenames:
metafunc.parametrize(
"workflow_action_type",
[
pytest.param(e, id=e.type)
for e in builder_workflow_action_type_registry.get_all()
],
)
@pytest.mark.django_db
def test_export_workflow_action(data_fixture, workflow_action_type: WorkflowActionType):
page = data_fixture.create_builder_page()
sample_params = workflow_action_type.get_sample_params()
workflow_action = data_fixture.create_workflow_action(
workflow_action_type.model_class, page=page, **sample_params
)
exported = workflow_action_type.export_serialized(workflow_action)
assert exported["id"] == workflow_action.id
assert exported["type"] == workflow_action_type.type
for key, value in sample_params.items():
assert exported[key] == value
@pytest.mark.django_db
def test_import_workflow_action(data_fixture, workflow_action_type: WorkflowActionType):
page = data_fixture.create_builder_page()
sample_params = workflow_action_type.get_sample_params()
page_after_import = data_fixture.create_builder_page()
serialized = {"id": 9999, "type": workflow_action_type.type, "page_id": page.id}
serialized.update(workflow_action_type.get_sample_params())
id_mapping = {"builder_pages": {page.id: page_after_import.id}}
workflow_action = workflow_action_type.import_serialized(
page, serialized, id_mapping
)
assert workflow_action.id != 9999
assert isinstance(workflow_action, workflow_action_type.model_class)
for key, value in sample_params.items():
assert getattr(workflow_action, key) == value

View file

@ -0,0 +1,124 @@
import pytest
from baserow.contrib.builder.workflow_actions.handler import (
BuilderWorkflowActionHandler,
)
from baserow.contrib.builder.workflow_actions.models import (
BuilderWorkflowAction,
EventTypes,
)
from baserow.contrib.builder.workflow_actions.workflow_action_types import (
NotificationWorkflowActionType,
OpenPageWorkflowActionType,
)
@pytest.mark.django_db
def test_create_workflow_action(data_fixture):
page = data_fixture.create_builder_page()
element = data_fixture.create_builder_button_element(page=page)
event = EventTypes.CLICK
workflow_action_type = NotificationWorkflowActionType()
workflow_action = (
BuilderWorkflowActionHandler()
.create_workflow_action(
workflow_action_type, page=page, element=element, event=event
)
.specific
)
assert workflow_action is not None
assert workflow_action.element is element
assert BuilderWorkflowAction.objects.count() == 1
@pytest.mark.django_db
def test_delete_workflow_action(data_fixture):
page = data_fixture.create_builder_page()
element = data_fixture.create_builder_button_element(page=page)
event = EventTypes.CLICK
workflow_action = data_fixture.create_notification_workflow_action(
page=page, element=element, event=event
)
assert BuilderWorkflowAction.objects.count() == 1
BuilderWorkflowActionHandler().delete_workflow_action(workflow_action)
assert BuilderWorkflowAction.objects.count() == 0
@pytest.mark.django_db
def test_update_workflow_action(data_fixture):
page = data_fixture.create_builder_page()
element = data_fixture.create_builder_button_element(page=page)
event = EventTypes.CLICK
workflow_action = data_fixture.create_notification_workflow_action(
page=page, element=element, event=event
)
element_changed = data_fixture.create_builder_button_element()
workflow_action = BuilderWorkflowActionHandler().update_workflow_action(
workflow_action, element=element_changed
)
workflow_action.refresh_from_db()
assert workflow_action.element_id == element_changed.id
@pytest.mark.django_db
def test_update_workflow_action_type_switching(data_fixture):
page = data_fixture.create_builder_page()
element = data_fixture.create_builder_button_element(page=page)
event = EventTypes.CLICK
workflow_action = data_fixture.create_notification_workflow_action(
page=page, element=element, event=event
)
workflow_action_changed = BuilderWorkflowActionHandler().update_workflow_action(
workflow_action, type=OpenPageWorkflowActionType.type, url="'hello'"
)
assert workflow_action_changed.get_type().type == OpenPageWorkflowActionType.type
assert workflow_action_changed.url == "'hello'"
assert workflow_action_changed.event == event
assert workflow_action_changed.page_id == page.id
assert workflow_action_changed.element_id == element.id
@pytest.mark.django_db
def test_get_workflow_action(data_fixture):
page = data_fixture.create_builder_page()
element = data_fixture.create_builder_button_element(page=page)
event = EventTypes.CLICK
workflow_action = data_fixture.create_notification_workflow_action(
page=page, element=element, event=event
)
workflow_action_fetched = BuilderWorkflowActionHandler().get_workflow_action(
workflow_action.id
)
assert workflow_action_fetched.id == workflow_action.id
@pytest.mark.django_db
def test_get_workflow_actions(data_fixture):
page = data_fixture.create_builder_page()
element = data_fixture.create_builder_button_element(page=page)
event = EventTypes.CLICK
workflow_action_one = data_fixture.create_notification_workflow_action(
page=page, element=element, event=event
)
workflow_action_two = data_fixture.create_notification_workflow_action(
page=page, element=element, event=event
)
[
workflow_action_one_fetched,
workflow_action_two_fetched,
] = BuilderWorkflowActionHandler().get_workflow_actions(page)
assert workflow_action_one_fetched.id == workflow_action_one.id
assert workflow_action_two_fetched.id == workflow_action_two.id

View file

@ -0,0 +1,186 @@
import pytest
from baserow.contrib.builder.workflow_actions.models import (
BuilderWorkflowAction,
EventTypes,
)
from baserow.contrib.builder.workflow_actions.service import (
BuilderWorkflowActionService,
)
from baserow.contrib.builder.workflow_actions.workflow_action_types import (
NotificationWorkflowActionType,
)
from baserow.core.exceptions import UserNotInWorkspace
@pytest.mark.django_db
def test_create_workflow_action(data_fixture):
user = data_fixture.create_user()
page = data_fixture.create_builder_page(user=user)
element = data_fixture.create_builder_button_element(page=page)
event = EventTypes.CLICK
workflow_action_type = NotificationWorkflowActionType()
workflow_action = (
BuilderWorkflowActionService()
.create_workflow_action(
user, workflow_action_type, page=page, element=element, event=event
)
.specific
)
assert workflow_action is not None
assert workflow_action.element is element
assert BuilderWorkflowAction.objects.count() == 1
@pytest.mark.django_db
def test_create_workflow_action_no_permissions(data_fixture):
user = data_fixture.create_user()
page = data_fixture.create_builder_page()
element = data_fixture.create_builder_button_element(page=page)
event = EventTypes.CLICK
workflow_action_type = NotificationWorkflowActionType()
with pytest.raises(UserNotInWorkspace):
BuilderWorkflowActionService().create_workflow_action(
user, workflow_action_type, page=page, element=element, event=event
)
@pytest.mark.django_db
def test_delete_workflow_action(data_fixture):
user = data_fixture.create_user()
page = data_fixture.create_builder_page(user=user)
element = data_fixture.create_builder_button_element(page=page)
event = EventTypes.CLICK
workflow_action = data_fixture.create_notification_workflow_action(
page=page, element=element, event=event
)
assert BuilderWorkflowAction.objects.count() == 1
BuilderWorkflowActionService().delete_workflow_action(user, workflow_action)
assert BuilderWorkflowAction.objects.count() == 0
@pytest.mark.django_db
def test_delete_workflow_action_no_permissions(data_fixture):
user = data_fixture.create_user()
page = data_fixture.create_builder_page()
element = data_fixture.create_builder_button_element(page=page)
event = EventTypes.CLICK
workflow_action = data_fixture.create_notification_workflow_action(
page=page, element=element, event=event
)
with pytest.raises(UserNotInWorkspace):
BuilderWorkflowActionService().delete_workflow_action(user, workflow_action)
@pytest.mark.django_db
def test_update_workflow_action(data_fixture):
user = data_fixture.create_user()
page = data_fixture.create_builder_page(user=user)
element = data_fixture.create_builder_button_element(page=page)
event = EventTypes.CLICK
workflow_action = data_fixture.create_notification_workflow_action(
page=page, element=element, event=event
)
element_changed = data_fixture.create_builder_button_element()
workflow_action = BuilderWorkflowActionService().update_workflow_action(
user, workflow_action, element=element_changed
)
workflow_action.refresh_from_db()
assert workflow_action.element_id == element_changed.id
@pytest.mark.django_db
def test_update_workflow_action_no_permissions(data_fixture):
user = data_fixture.create_user()
page = data_fixture.create_builder_page()
element = data_fixture.create_builder_button_element(page=page)
event = EventTypes.CLICK
workflow_action = data_fixture.create_notification_workflow_action(
page=page, element=element, event=event
)
element_changed = data_fixture.create_builder_button_element()
with pytest.raises(UserNotInWorkspace):
BuilderWorkflowActionService().update_workflow_action(
user, workflow_action, element=element_changed
)
@pytest.mark.django_db
def test_get_workflow_action(data_fixture):
user = data_fixture.create_user()
page = data_fixture.create_builder_page(user=user)
element = data_fixture.create_builder_button_element(page=page)
event = EventTypes.CLICK
workflow_action = data_fixture.create_notification_workflow_action(
page=page, element=element, event=event
)
workflow_action_fetched = BuilderWorkflowActionService().get_workflow_action(
user, workflow_action.id
)
assert workflow_action_fetched.id == workflow_action.id
@pytest.mark.django_db
def test_get_workflow_action_no_permissions(data_fixture):
user = data_fixture.create_user()
page = data_fixture.create_builder_page()
element = data_fixture.create_builder_button_element(page=page)
event = EventTypes.CLICK
workflow_action = data_fixture.create_notification_workflow_action(
page=page, element=element, event=event
)
with pytest.raises(UserNotInWorkspace):
BuilderWorkflowActionService().get_workflow_action(user, workflow_action.id)
@pytest.mark.django_db
def test_get_workflow_actions(data_fixture):
user = data_fixture.create_user()
page = data_fixture.create_builder_page(user=user)
element = data_fixture.create_builder_button_element(page=page)
event = EventTypes.CLICK
workflow_action_one = data_fixture.create_notification_workflow_action(
page=page, element=element, event=event
)
workflow_action_two = data_fixture.create_notification_workflow_action(
page=page, element=element, event=event
)
[
workflow_action_one_fetched,
workflow_action_two_fetched,
] = BuilderWorkflowActionService().get_workflow_actions(user, page)
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_get_workflow_actions_no_permissions(data_fixture):
user = data_fixture.create_user()
page = data_fixture.create_builder_page()
element = data_fixture.create_builder_button_element(page=page)
event = EventTypes.CLICK
workflow_action_one = data_fixture.create_notification_workflow_action(
page=page, element=element, event=event
)
workflow_action_two = data_fixture.create_notification_workflow_action(
page=page, element=element, event=event
)
with pytest.raises(UserNotInWorkspace):
BuilderWorkflowActionService().get_workflow_actions(user, page)

View file

@ -48,6 +48,13 @@ from baserow.contrib.builder.pages.operations import (
UpdatePageOperationType,
)
from baserow.contrib.builder.theme.operations import UpdateThemeOperationType
from baserow.contrib.builder.workflow_actions.operations import (
CreateBuilderWorkflowActionOperationType,
DeleteBuilderWorkflowActionOperationType,
ListBuilderWorkflowActionsPageOperationType,
ReadBuilderWorkflowActionOperationType,
UpdateBuilderWorkflowActionOperationType,
)
from baserow.contrib.database.airtable.operations import (
RunAirtableImportJobOperationType,
)
@ -278,6 +285,8 @@ default_roles[VIEWER_ROLE_UID].extend(
ReadViewSortOperationType,
ListViewGroupByOperationType,
ReadViewGroupByOperationType,
ListBuilderWorkflowActionsPageOperationType,
ReadBuilderWorkflowActionOperationType,
ReadViewFilterGroupOperationType,
]
)
@ -305,6 +314,7 @@ default_roles[EDITOR_ROLE_UID].extend(
RestoreDatabaseRowOperationType,
ListTeamSubjectsOperationType,
ReadTeamSubjectOperationType,
UpdateBuilderWorkflowActionOperationType,
]
)
default_roles[BUILDER_ROLE_UID].extend(
@ -397,6 +407,8 @@ default_roles[BUILDER_ROLE_UID].extend(
ReadDataSourceOperationType,
UpdateDataSourceOperationType,
DispatchDataSourceOperationType,
DeleteBuilderWorkflowActionOperationType,
CreateBuilderWorkflowActionOperationType,
]
)
default_roles[ADMIN_ROLE_UID].extend(

View file

@ -1,34 +1,118 @@
<template>
<Expandable toggle-on-click>
<template #header="{ toggle, expanded }">
<template #header="{ expanded }">
<div class="event__header">
<div class="event__label">{{ event.label }}</div>
<a class="event__toggle" @click.stop="toggle">
<div class="event__header-left">
<div class="event__label">
{{ event.label }}
</div>
<div
v-if="workflowActions.length"
class="margin-left-1 event__amount-actions"
>
{{ workflowActions.length }}
</div>
</div>
<a class="event__toggle">
<i :class="getIcon(expanded)"></i>
</a>
</div>
</template>
<template #default>
This is where you will be able to define your actions in the future :)
<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)"
/>
<Button
size="tiny"
type="link"
prepend-icon="baserow-icon-plus"
:loading="addingAction"
class="margin-top-1"
@click="addWorkflowAction"
>
{{ $t('event.addAction') }}
</Button>
</template>
</Expandable>
</template>
<script>
import { Event } from '@baserow/modules/builder/eventTypes'
import WorkflowAction from '@baserow/modules/core/components/workflowActions/WorkflowAction.vue'
import { NotificationWorkflowActionType } from '@baserow/modules/builder/workflowActionTypes'
import { mapActions } from 'vuex'
import { notifyIf } from '@baserow/modules/core/utils/error'
const DEFAULT_WORKFLOW_ACTION_TYPE = NotificationWorkflowActionType.getType()
export default {
name: 'Event',
components: { WorkflowAction },
inject: ['page'],
props: {
event: {
type: Event,
required: true,
},
element: {
type: Object,
required: true,
},
workflowActions: {
type: Array,
required: false,
default: () => [],
},
availableWorkflowActionTypes: {
type: Array,
required: true,
},
},
data() {
return {
addingAction: false,
}
},
methods: {
...mapActions({
actionCreateWorkflowAction: 'workflowAction/create',
actionDeleteWorkflowAction: 'workflowAction/delete',
}),
getIcon(expanded) {
return expanded ? 'iconoir-nav-arrow-down' : 'iconoir-nav-arrow-right'
},
async addWorkflowAction() {
this.addingAction = true
try {
await this.actionCreateWorkflowAction({
page: this.page,
workflowActionType: DEFAULT_WORKFLOW_ACTION_TYPE,
eventType: this.event.getType(),
configuration: {
element_id: this.element.id,
},
})
} catch (error) {
notifyIf(error)
}
this.addingAction = false
},
async deleteWorkflowAction(workflowAction) {
try {
await this.actionDeleteWorkflowAction({
page: this.page,
workflowAction,
})
} catch (error) {
notifyIf(error)
}
},
},
}
</script>

View file

@ -3,7 +3,10 @@
<Event
v-for="event in elementType.getEvents()"
:key="event.getType()"
:element="element"
:event="event"
:available-workflow-action-types="availableWorkflowActionTypes"
:workflow-actions="getWorkflowActionsForEvent(event)"
class="margin-bottom-2"
></Event>
</div>
@ -17,5 +20,23 @@ export default {
name: 'EventSidePanel',
components: { Event },
mixins: [elementSidePanel],
computed: {
availableWorkflowActionTypes() {
return Object.values(this.$registry.getAll('workflowAction'))
},
workflowActions() {
return this.$store.getters['workflowAction/getElementWorkflowActions'](
this.page,
this.element.id
)
},
},
methods: {
getWorkflowActionsForEvent(event) {
return this.workflowActions.filter(
(workflowAction) => workflowAction.event === event.getType()
)
},
},
}
</script>

View file

@ -0,0 +1,35 @@
<template>
<form @submit.prevent="submit">
<ApplicationBuilderFormulaInputGroup
v-model="values.title"
:placeholder="$t('notificationWorkflowActionForm.titlePlaceholder')"
:data-providers-allowed="dataProvidersAllowed"
:label="$t('notificationWorkflowActionForm.titleLabel')"
/>
<ApplicationBuilderFormulaInputGroup
v-model="values.description"
:placeholder="$t('notificationWorkflowActionForm.descriptionPlaceholder')"
:data-providers-allowed="dataProvidersAllowed"
:label="$t('notificationWorkflowActionForm.descriptionLabel')"
/>
</form>
</template>
<script>
import workflowActionForm from '@baserow/modules/builder/mixins/workflowActionForm'
import ApplicationBuilderFormulaInputGroup from '@baserow/modules/builder/components/ApplicationBuilderFormulaInputGroup.vue'
export default {
name: 'NotificationWorkflowActionForm',
components: { ApplicationBuilderFormulaInputGroup },
mixins: [workflowActionForm],
data() {
return {
values: {
title: '',
description: '',
},
}
},
}
</script>

View file

@ -0,0 +1,28 @@
<template>
<form @submit.prevent="submit">
<ApplicationBuilderFormulaInputGroup
v-model="values.url"
:placeholder="$t('openPageWorkflowActionForm.urlPlaceholder')"
:data-providers-allowed="dataProvidersAllowed"
:label="$t('openPageWorkflowActionForm.urlLabel')"
/>
</form>
</template>
<script>
import workflowActionForm from '@baserow/modules/builder/mixins/workflowActionForm'
import ApplicationBuilderFormulaInputGroup from '@baserow/modules/builder/components/ApplicationBuilderFormulaInputGroup'
export default {
name: 'OpenPageWorkflowActionForm',
components: { ApplicationBuilderFormulaInputGroup },
mixins: [workflowActionForm],
data() {
return {
values: {
url: '',
},
}
},
}
</script>

View file

@ -318,5 +318,22 @@
"currentRecordDataProviderType": {
"index": "index",
"firstPartName": "Data source: {name}"
},
"workflowActionTypes": {
"notificationLabel": "Show Notification",
"openPageLabel": "Open Page"
},
"notificationWorkflowActionForm": {
"titleLabel": "Title",
"titlePlaceholder": "Enter text...",
"descriptionLabel": "Description",
"descriptionPlaceholder": "Enter text..."
},
"openPageWorkflowActionForm": {
"urlLabel": "URL",
"urlPlaceholder": "Enter text..."
},
"event": {
"addAction": "Add action"
}
}

View file

@ -0,0 +1,11 @@
import form from '@baserow/modules/core/mixins/form'
import { DATA_PROVIDERS_ALLOWED_ELEMENTS } from '@baserow/modules/builder/enums'
export default {
mixins: [form],
computed: {
dataProvidersAllowed() {
return DATA_PROVIDERS_ALLOWED_ELEMENTS
},
},
}

View file

@ -55,6 +55,7 @@ export default {
page,
}),
store.dispatch('element/fetch', { page }),
store.dispatch('workflowAction/fetch', { page }),
])
await DataProviderType.initAll($registry.getAll('builderDataProvider'), {

View file

@ -19,6 +19,7 @@ import dataSourceStore from '@baserow/modules/builder/store/dataSource'
import pageParameterStore from '@baserow/modules/builder/store/pageParameter'
import dataSourceContentStore from '@baserow/modules/builder/store/dataSourceContent'
import themeStore from '@baserow/modules/builder/store/theme'
import workflowActionStore from '@baserow/modules/builder/store/workflowAction'
import { registerRealtimeEvents } from '@baserow/modules/builder/realtime'
import {
@ -73,6 +74,10 @@ import {
} from '@baserow/modules/builder/dataProviderTypes'
import { MainThemeConfigBlock } from '@baserow/modules/builder/themeConfigBlockTypes'
import {
NotificationWorkflowActionType,
OpenPageWorkflowActionType,
} from '@baserow/modules/builder/workflowActionTypes'
export default (context) => {
const { store, app, isDev } = context
@ -99,6 +104,7 @@ export default (context) => {
store.registerModule('pageParameter', pageParameterStore)
store.registerModule('dataSourceContent', dataSourceContentStore)
store.registerModule('theme', themeStore)
store.registerModule('workflowAction', workflowActionStore)
app.$registry.registerNamespace('builderSettings')
app.$registry.registerNamespace('element')
@ -189,4 +195,13 @@ export default (context) => {
new PageParameterDataProviderType(context)
)
app.$registry.register('themeConfigBlock', new MainThemeConfigBlock(context))
app.$registry.register(
'workflowAction',
new NotificationWorkflowActionType(context)
)
app.$registry.register(
'workflowAction',
new OpenPageWorkflowActionType(context)
)
}

View file

@ -0,0 +1,25 @@
export default (client) => {
return {
create(pageId, workflowActionType, eventType, configuration = null) {
const payload = {
type: workflowActionType,
event: eventType,
...configuration,
}
return client.post(`builder/page/${pageId}/workflow_actions/`, payload)
},
fetchAll(pageId) {
return client.get(`builder/page/${pageId}/workflow_actions/`)
},
delete(workflowActionId) {
return client.delete(`builder/workflow_action/${workflowActionId}/`)
},
update(workflowActionId, values) {
return client.patch(
`builder/workflow_action/${workflowActionId}/`,
values
)
},
}
}

View file

@ -272,17 +272,24 @@ const actions = {
}
},
async duplicate({ dispatch }, { page, elementId }) {
const { data: elementsCreated } = await ElementService(
this.$client
).duplicate(elementId)
const {
data: { elements, workflow_actions: workflowActions },
} = await ElementService(this.$client).duplicate(elementId)
await Promise.all(
elementsCreated.map((element) =>
dispatch('forceCreate', { page, element })
const elementPromises = elements.map((element) =>
dispatch('forceCreate', { page, element })
)
const workflowActionPromises = workflowActions.map((workflowAction) =>
dispatch(
'workflowAction/forceCreate',
{ page, workflowAction },
{ root: true }
)
)
return elementsCreated
await Promise.all(elementPromises.concat(workflowActionPromises))
return elements
},
emitElementEvent({ getters }, { event, page, ...rest }) {
const elements = getters.getElements(page)

View file

@ -12,6 +12,7 @@ export function populatePage(page) {
page.dataSources = []
page.elements = []
page.workflowActions = []
return page
}

View file

@ -0,0 +1,172 @@
import WorkflowActionService from '@baserow/modules/builder/services/workflowAction'
const updateContext = {
updateTimeout: null,
promiseResolve: null,
lastUpdatedValues: null,
}
const state = {}
const mutations = {
ADD_ITEM(state, { page, workflowAction }) {
page.workflowActions.push(workflowAction)
},
SET_ITEMS(state, { page, workflowActions }) {
page.workflowActions = workflowActions
},
DELETE_ITEM(state, { page, workflowActionId }) {
const index = page.workflowActions.findIndex(
(workflowAction) => workflowAction.id === workflowActionId
)
if (index > -1) {
page.workflowActions.splice(index, 1)
}
},
UPDATE_ITEM(state, { page, workflowAction: workflowActionToUpdate, values }) {
page.workflowActions.forEach((workflowAction) => {
if (workflowAction.id === workflowActionToUpdate.id) {
Object.assign(workflowAction, values)
}
})
},
SET_ITEM(state, { page, workflowAction: workflowActionToSet, values }) {
page.workflowActions = page.workflowActions.map((workflowAction) =>
workflowAction.id === workflowActionToSet.id ? values : workflowAction
)
},
}
const actions = {
forceCreate({ commit }, { page, workflowAction }) {
commit('ADD_ITEM', { page, workflowAction })
},
forceDelete({ commit }, { page, workflowActionId }) {
commit('DELETE_ITEM', { page, workflowActionId })
},
forceUpdate({ commit }, { page, workflowAction, values }) {
commit('UPDATE_ITEM', { page, workflowAction, values })
},
forceSet({ commit }, { page, workflowAction, values }) {
commit('SET_ITEM', { page, workflowAction, values })
},
async create(
{ dispatch },
{ page, workflowActionType, eventType, configuration = null }
) {
const { data: workflowAction } = await WorkflowActionService(
this.$client
).create(page.id, workflowActionType, eventType, configuration)
await dispatch('forceCreate', { page, workflowAction })
return workflowAction
},
async fetch({ commit }, { page }) {
const { data: workflowActions } = await WorkflowActionService(
this.$client
).fetchAll(page.id)
commit('SET_ITEMS', { page, workflowActions })
},
async delete({ dispatch }, { page, workflowAction }) {
dispatch('forceDelete', { page, workflowActionId: workflowAction.id })
try {
await WorkflowActionService(this.$client).delete(workflowAction.id)
} catch (error) {
await dispatch('forceCreate', { page, workflowAction })
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 }) {
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 })
return new Promise((resolve, reject) => {
const fire = async () => {
try {
const { data } = await WorkflowActionService(this.$client).update(
workflowAction.id,
values
)
await dispatch('forceSet', {
page,
workflowAction,
values: data,
})
resolve()
} catch (error) {
await dispatch('forceUpdate', {
page,
workflowAction,
values: updateContext.lastUpdatedValues,
})
reject(error)
}
updateContext.lastUpdatedValues = null
}
if (updateContext.promiseResolve) {
updateContext.promiseResolve()
updateContext.promiseResolve = null
}
clearTimeout(updateContext.updateTimeout)
if (!updateContext.lastUpdatedValues) {
updateContext.lastUpdatedValues = oldValues
}
updateContext.updateTimeout = setTimeout(fire, 500)
updateContext.promiseResolve = resolve
})
},
}
const getters = {
getElementWorkflowActions: (state) => (page, elementId) => {
return page.workflowActions.filter(
(workflowAction) => workflowAction.element_id === elementId
)
},
}
export default {
namespaced: true,
state,
mutations,
actions,
getters,
}

View file

@ -0,0 +1,31 @@
import { WorkflowActionType } from '@baserow/modules/core/workflowActionTypes'
import NotificationWorkflowActionForm from '@baserow/modules/builder/components/workflowAction/NotificationWorkflowActionForm.vue'
import OpenPageWorkflowActionForm from '@baserow/modules/builder/components/workflowAction/OpenPageWorkflowActionForm'
export class NotificationWorkflowActionType extends WorkflowActionType {
static getType() {
return 'notification'
}
get form() {
return NotificationWorkflowActionForm
}
get label() {
return this.app.i18n.t('workflowActionTypes.notificationLabel')
}
}
export class OpenPageWorkflowActionType extends WorkflowActionType {
static getType() {
return 'open_page'
}
get form() {
return OpenPageWorkflowActionForm
}
get label() {
return this.app.i18n.t('workflowActionTypes.openPageLabel')
}
}

View file

@ -140,3 +140,4 @@
@import 'data_explorer/node';
@import 'data_explorer/root_node';
@import 'data_explorer/data_explorer';
@import 'anchor';

View file

@ -0,0 +1,7 @@
.anchor {
color: $color-primary-900;
&:hover {
text-decoration: none;
}
}

View file

@ -23,3 +23,4 @@
@import 'preview_navigation_bar';
@import 'add_element_zone';
@import 'event';
@import 'workflow_action_selector';

View file

@ -3,11 +3,26 @@
justify-content: space-between;
}
.event__header-left {
display: flex;
}
.event__label {
font-weight: bold;
font-size: 14px;
}
.event__amount-actions {
background-color: $color-neutral-100;
padding: 1px 7px;
border-radius: 3px;
}
.event__toggle {
color: $color-neutral-900;
}
.event__workflow-action:not(:first-child) {
border-top: 1px solid $color-neutral-200;
padding-top: 20px;
}

View file

@ -0,0 +1,8 @@
.workflow-action-selector {
display: flex;
align-items: center;
}
.workflow-action-selector__options {
flex-basis: 90%;
}

View file

@ -0,0 +1,75 @@
<template>
<div>
<WorkflowActionSelector
:available-workflow-action-types="availableWorkflowActionTypes"
:workflow-action="workflowAction"
@change="updateWorkflowAction({ type: $event })"
@delete="$emit('delete')"
/>
<component
:is="workflowActionType.form"
ref="actionForm"
:default-values="workflowAction"
class="margin-top-2"
@values-changed="updateWorkflowAction($event)"
></component>
</div>
</template>
<script>
import WorkflowActionSelector from '@baserow/modules/core/components/workflowActions/WorkflowActionSelector.vue'
import _ from 'lodash'
import { notifyIf } from '@baserow/modules/core/utils/error'
import { mapActions } from 'vuex'
export default {
name: 'WorkflowAction',
components: { WorkflowActionSelector },
inject: ['page'],
props: {
availableWorkflowActionTypes: {
type: Array,
required: true,
},
workflowAction: {
type: Object,
required: false,
default: null,
},
},
computed: {
workflowActionType() {
return this.availableWorkflowActionTypes.find(
(workflowActionType) =>
workflowActionType.getType() === this.workflowAction.type
)
},
},
methods: {
...mapActions({
actionUpdateWorkflowAction: 'workflowAction/updateDebounced',
}),
async updateWorkflowAction(values) {
if (!this.$refs.actionForm.isFormValid()) {
return
}
// In this case there weren't any actual changes
if (_.isMatch(this.workflowAction, values)) {
return
}
try {
await this.actionUpdateWorkflowAction({
page: this.page,
workflowAction: this.workflowAction,
values,
})
} catch (error) {
this.$refs.actionForm.reset()
notifyIf(error)
}
},
},
}
</script>

View file

@ -0,0 +1,47 @@
<template>
<div class="workflow-action-selector">
<Dropdown
class="workflow-action-selector__options"
:value="workflowActionType.getType()"
:show-search="false"
@change="$emit('change', $event)"
>
<DropdownItem
v-for="availableWorkflowActionType in availableWorkflowActionTypes"
:key="availableWorkflowActionType.getType()"
:name="availableWorkflowActionType.label"
:value="availableWorkflowActionType.getType()"
></DropdownItem>
</Dropdown>
<div class="margin-left-2">
<a class="anchor" @click="$emit('delete')">
<i class="iconoir-trash"></i>
</a>
</div>
</div>
</template>
<script>
export default {
name: 'WorkflowActionSelector',
props: {
availableWorkflowActionTypes: {
type: Array,
required: true,
},
workflowAction: {
type: Object,
required: false,
default: null,
},
},
computed: {
workflowActionType() {
return this.availableWorkflowActionTypes.find(
(workflowActionType) =>
workflowActionType.getType() === this.workflowAction.type
)
},
},
}
</script>

View file

@ -99,6 +99,7 @@ export default (context, inject) => {
registry.registerNamespace('membersPagePlugins')
registry.registerNamespace('runtimeFormulaFunction')
registry.registerNamespace('notification')
registry.registerNamespace('workflowAction')
registry.register('settings', new AccountSettingsType(context))
registry.register('settings', new PasswordSettingsType(context))
registry.register('settings', new EmailNotificationsSettingsType(context))

View file

@ -0,0 +1,11 @@
import { Registerable } from '@baserow/modules/core/registry'
export class WorkflowActionType extends Registerable {
get form() {
return null
}
get label() {
return null
}
}