1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-04-14 17:18:33 +00:00

Resolve "Add Button collection field"

This commit is contained in:
Afonso Silva 2024-05-24 18:18:11 +00:00 committed by Jérémie Pardou
parent 189c87327c
commit ce868c341d
41 changed files with 583 additions and 136 deletions

View file

@ -212,7 +212,7 @@ class CollectionFieldSerializer(serializers.ModelSerializer):
object.
"""
default_allowed_fields = ["name", "type", "id"]
default_allowed_fields = ["name", "type", "id", "uid"]
config = serializers.DictField(
required=False,

View file

@ -260,6 +260,7 @@ class BuilderConfig(AppConfig):
from .elements.collection_field_types import (
BooleanCollectionFieldType,
ButtonCollectionFieldType,
LinkCollectionFieldType,
TagsCollectionFieldType,
TextCollectionFieldType,
@ -270,6 +271,7 @@ class BuilderConfig(AppConfig):
collection_field_type_registry.register(TextCollectionFieldType())
collection_field_type_registry.register(LinkCollectionFieldType())
collection_field_type_registry.register(TagsCollectionFieldType())
collection_field_type_registry.register(ButtonCollectionFieldType())
from .domains.receivers import connect_to_domain_pre_delete_signal

View file

@ -3,8 +3,10 @@ from typing import Any, Dict, Optional, TypedDict
from rest_framework import serializers
from baserow.contrib.builder.elements.element_types import NavigationElementManager
from baserow.contrib.builder.elements.models import CollectionField
from baserow.contrib.builder.elements.registries import CollectionFieldType
from baserow.contrib.builder.formula_importer import import_formula
from baserow.contrib.builder.workflow_actions.models import BuilderWorkflowAction
from baserow.core.formula.serializers import FormulaSerializerField
@ -185,3 +187,41 @@ class TagsCollectionFieldType(CollectionFieldType):
return super().deserialize_property(
prop_name, value, id_mapping, data_source_id
)
class ButtonCollectionFieldType(CollectionFieldType):
type = "button"
allowed_fields = ["label"]
serializer_field_names = ["label"]
class SerializedDict(TypedDict):
label: str
@property
def serializer_field_overrides(self):
return {
"label": FormulaSerializerField(
help_text="The string value.",
required=False,
allow_blank=True,
default="",
),
}
def deserialize_property(
self,
prop_name: str,
value: Any,
id_mapping: Dict[str, Any],
data_source_id: Optional[int] = None,
) -> Any:
if prop_name == "label" and data_source_id:
return import_formula(value, id_mapping, data_source_id=data_source_id)
return super().deserialize_property(
prop_name, value, id_mapping, data_source_id
)
def before_delete(self, instance: CollectionField):
# We delete the related workflow actions
BuilderWorkflowAction.objects.filter(event__startswith=instance.uid).delete()

View file

@ -135,9 +135,11 @@ class ElementHandler:
"""
element = ElementHandler().get_element(element_id)
if isinstance(element.get_type(), target_type):
return element
for ancestor in self.get_ancestors(element_id, element.page):
ancestor_type = element_type_registry.get_by_model(ancestor.specific_class)
if isinstance(ancestor_type, target_type):
if isinstance(ancestor.get_type(), target_type):
return ancestor
def get_element_for_update(

View file

@ -1,6 +1,6 @@
from typing import Any, Dict, List, Optional, Type
from django.db.models import QuerySet
from django.db.models import Q, QuerySet
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
@ -311,6 +311,17 @@ class CollectionElementWithFieldsTypeMixin(CollectionElementTypeMixin):
def after_update(self, instance: CollectionElementSubClass, values):
if "fields" in values:
# If the collection element contains fields that are being deleted,
# we also need to delete the associated workflow actions.
query = Q()
for field in values["fields"]:
if "uid" in field:
query |= Q(uid=field["uid"])
# Call before delete hook of removed fields
for field in instance.fields.exclude(query):
field.get_type().before_delete(field)
# Remove previous fields
instance.fields.all().delete()
@ -323,6 +334,10 @@ class CollectionElementWithFieldsTypeMixin(CollectionElementTypeMixin):
instance.fields.add(*created_fields)
def before_delete(self, instance: CollectionElementSubClass):
# Call the before_delete hook of all fields
for field in instance.fields.all():
field.get_type().before_delete(field)
instance.fields.all().delete()
def create_instance_from_serialized(

View file

@ -1,3 +1,4 @@
import uuid
from typing import TYPE_CHECKING, Optional
from django.contrib.contenttypes.models import ContentType
@ -685,6 +686,7 @@ class CollectionField(models.Model):
A field of a Collection element
"""
uid = models.UUIDField(default=uuid.uuid4)
order = models.PositiveIntegerField()
name = models.CharField(
max_length=225,

View file

@ -203,6 +203,7 @@ class CollectionFieldType(
)
serialized = {
"uid": instance.uid,
"name": instance.name,
"type": instance.type,
"config": serialized_config,
@ -270,6 +271,7 @@ class CollectionFieldType(
)
deserialized_values = {
"uid": serialized_values["uid"],
"config": deserialized_config,
"type": serialized_values["type"],
"name": serialized_values["name"],
@ -310,6 +312,12 @@ class CollectionFieldType(
return serializer_class(model_instance_or_instances, context=context, **kwargs)
def before_delete(self, instance: CollectionField):
"""
This hooks is called before we delete a collection field and gives the
opportunity to clean up things.
"""
CollectionFieldTypeSubClass = TypeVar(
"CollectionFieldTypeSubClass", bound=CollectionFieldType

View file

@ -0,0 +1,38 @@
# Generated by Django 4.1.13 on 2024-05-22 14:50
import uuid
from django.db import migrations, models
from baserow.contrib.database.db.functions import RandomUUID
def gen_uuid(apps, schema_editor):
"""
Generates the uid for all user sources.
"""
CollectionField = apps.get_model("builder", "collectionfield")
CollectionField.objects.update(uid=RandomUUID())
class Migration(migrations.Migration):
dependencies = [
("builder", "0019_repeat_element_styling"),
]
operations = [
migrations.AddField(
model_name="collectionfield",
name="uid",
field=models.UUIDField(default=uuid.uuid4),
),
migrations.RunPython(gen_uuid, reverse_code=migrations.RunPython.noop),
migrations.AlterField(
model_name="builderworkflowaction",
name="event",
field=models.CharField(
help_text="The event that triggers the execution", max_length=60
),
),
]

View file

@ -28,8 +28,7 @@ class BuilderWorkflowAction(
on_delete=models.CASCADE,
)
event = models.CharField(
max_length=30,
choices=EventTypes.choices,
max_length=60,
help_text="The event that triggers the execution",
)
page = models.ForeignKey(Page, on_delete=models.CASCADE)

View file

@ -1332,6 +1332,9 @@ class LocalBaserowUpsertRowServiceType(LocalBaserowTableServiceType):
)
raise ServiceImproperlyConfigured(message) from e
except Exception as e:
import traceback
traceback.print_exc()
message = (
"Unknown error in formula for "
f"field {field_mapping.field.name}({field_mapping.field.id}): {str(e)}"

View file

@ -1,7 +1,8 @@
from django.contrib.auth import get_user_model
from django.db import transaction
from baserow.core.models import WorkspaceUser
from baserow.core.handler import CoreHandler
from baserow.core.models import Workspace, WorkspaceUser
from baserow.core.user.exceptions import UserAlreadyExist
from baserow.core.user.handler import UserHandler
@ -24,9 +25,18 @@ def load_test_data():
admin.is_staff = True
admin.save()
workspace = admin.workspaceuser_set.all().order_by("id").first().workspace
workspace.name = f"Acme Corp ({i +1})" if i > 0 else "Acme Corp"
workspace.save()
try:
workspace = Workspace.objects.get(
name=f"Acme Corp ({i +1})" if i > 0 else "Acme Corp"
)
except Workspace.DoesNotExist:
workspace = (
CoreHandler()
.create_workspace(
admin, name=f"Acme Corp ({i +1})" if i > 0 else "Acme Corp"
)
.workspace
)
# Create a second admin for the workspace
email = f"admin{i + 1}_bis@baserow.io" if i > 0 else "admin_bis@baserow.io"

View file

@ -1,3 +1,5 @@
import uuid
from django.urls import reverse
import pytest
@ -48,20 +50,32 @@ def test_can_update_a_table_element_fields(api_client, data_fixture):
table_element = data_fixture.create_builder_table_element(user=user)
url = reverse("api:builder:element:item", kwargs={"element_id": table_element.id})
uuids = [str(uuid.uuid4()), str(uuid.uuid4()), str(uuid.uuid4())]
response = api_client.patch(
url,
{
"fields": [
{"name": "Name", "type": "text", "value": "get('test1')"},
{
"name": "Name",
"type": "text",
"value": "get('test1')",
"uid": uuids[0],
},
{
"name": "Color",
"type": "link",
"navigate_to_url": "get('test2')",
"link_name": "get('test3')",
"target": "self",
"uid": uuids[1],
},
{
"name": "Question",
"type": "text",
"value": "get('test3')",
"uid": uuids[2],
},
{"name": "Question", "type": "text", "value": "get('test3')"},
],
},
format="json",
@ -70,10 +84,10 @@ def test_can_update_a_table_element_fields(api_client, data_fixture):
assert response.status_code == HTTP_200_OK
assert [
{key: value for key, value in f.items() if key != "id"}
{key: value for key, value in f.items() if key not in ["id"]}
for f in response.json()["fields"]
] == [
{"name": "Name", "type": "text", "value": "get('test1')"},
{"name": "Name", "type": "text", "value": "get('test1')", "uid": uuids[0]},
{
"name": "Color",
"type": "link",
@ -81,8 +95,9 @@ def test_can_update_a_table_element_fields(api_client, data_fixture):
"navigate_to_url": "get('test2')",
"link_name": "get('test3')",
"target": "self",
"uid": uuids[1],
},
{"name": "Question", "type": "text", "value": "get('test3')"},
{"name": "Question", "type": "text", "value": "get('test3')", "uid": uuids[2]},
]

View file

@ -96,7 +96,9 @@ def test_create_workflow_action_event_does_not_exist(api_client, data_fixture):
HTTP_AUTHORIZATION=f"JWT {token}",
)
assert response.status_code == HTTP_400_BAD_REQUEST
# NOTE: Event names are no longer bound to a list of choices, so
# the API will not raise a 400 Bad Request error
assert response.status_code == HTTP_404_NOT_FOUND
@pytest.mark.django_db

View file

@ -573,3 +573,9 @@ def test_get_first_ancestor_of_type(data_fixture, django_assert_num_queries):
)
assert nearest_column_ancestor.specific == grandparent
nearest_column_ancestor = ElementHandler().get_first_ancestor_of_type(
grandparent.id, ColumnElementType
)
assert nearest_column_ancestor.specific == grandparent

View file

@ -1,10 +1,19 @@
import uuid
from collections import defaultdict
import pytest
from rest_framework.exceptions import ValidationError
from baserow.contrib.builder.elements.handler import ElementHandler
from baserow.contrib.builder.elements.models import CollectionField, Element
from baserow.contrib.builder.elements.models import (
CollectionField,
Element,
TableElement,
)
from baserow.contrib.builder.elements.registries import element_type_registry
from baserow.contrib.builder.elements.service import ElementService
from baserow.contrib.builder.workflow_actions.models import NotificationWorkflowAction
from baserow.core.utils import MirrorDict
@pytest.mark.django_db
@ -137,6 +146,7 @@ def test_update_table_element_with_fields(data_fixture):
user = data_fixture.create_user()
page = data_fixture.create_builder_page(user=user)
table_element = data_fixture.create_builder_table_element(page=page)
uuids = [str(uuid.uuid4()), str(uuid.uuid4()), str(uuid.uuid4())]
ElementService().update_element(
user,
@ -146,11 +156,13 @@ def test_update_table_element_with_fields(data_fixture):
"name": "New field 1",
"type": "text",
"config": {"value": "get('test1')"},
"uid": uuids[0],
},
{
"name": "New field 2",
"type": "text",
"config": {"value": "get('test2')"},
"uid": uuids[1],
},
],
)
@ -172,11 +184,13 @@ def test_update_table_element_with_fields(data_fixture):
"name": "New field 3",
"type": "text",
"config": {"value": "get('test3')"},
"uid": uuids[0],
},
{
"name": "New field 4",
"type": "text",
"config": {"value": "get('test4')"},
"uid": uuids[1],
},
],
)
@ -277,6 +291,7 @@ def test_import_table_element_with_current_record_formulas_with_update(data_fixt
data_source1 = data_fixture.create_builder_local_baserow_list_rows_data_source(
table=table, page=page
)
uuids = [str(uuid.uuid4()), str(uuid.uuid4()), str(uuid.uuid4())]
IMPORT_REF = {
"id": 42,
@ -293,6 +308,7 @@ def test_import_table_element_with_current_record_formulas_with_update(data_fixt
"name": "Field 1",
"config": {"value": f"get('current_record.field_42')"},
"type": "text",
"uid": uuids[0],
},
{
"name": "Field 2",
@ -305,6 +321,7 @@ def test_import_table_element_with_current_record_formulas_with_update(data_fixt
"target": "self",
},
"type": "link",
"uid": uuids[1],
},
],
}
@ -331,3 +348,102 @@ def test_import_table_element_with_current_record_formulas_with_update(data_fixt
"target": "self",
},
]
@pytest.mark.django_db
def test_delete_table_element_removes_associated_workflow_actions(data_fixture):
user = data_fixture.create_user()
page = data_fixture.create_builder_page(user=user)
data_source1 = data_fixture.create_builder_local_baserow_list_rows_data_source(
page=page
)
table_element = data_fixture.create_builder_table_element(
page=page,
data_source=data_source1,
fields=[
{
"name": "Field",
"type": "button",
"config": {"label": "Click me"},
},
],
)
data_fixture.create_workflow_action(
NotificationWorkflowAction,
page=page,
element=table_element,
event=f"{table_element.fields.first().uid}_click",
)
assert NotificationWorkflowAction.objects.count() == 1
ElementService().delete_element(user, table_element)
assert NotificationWorkflowAction.objects.count() == 0
@pytest.mark.django_db
def test_delete_table_field_removes_associated_workflow_actions(data_fixture):
user = data_fixture.create_user()
page = data_fixture.create_builder_page(user=user)
data_source1 = data_fixture.create_builder_local_baserow_list_rows_data_source(
page=page
)
table_element = data_fixture.create_builder_table_element(
page=page,
data_source=data_source1,
fields=[
{
"name": "Field",
"type": "button",
"config": {"label": "Click me"},
},
],
)
workflow_action = data_fixture.create_workflow_action(
NotificationWorkflowAction,
page=page,
element=table_element,
event=f"{table_element.fields.first().uid}_click",
)
assert NotificationWorkflowAction.objects.count() == 1
ElementService().update_element(user, table_element, fields=[])
assert NotificationWorkflowAction.objects.count() == 0
@pytest.mark.django_db
def test_table_element_import_export(data_fixture):
user = data_fixture.create_user()
page = data_fixture.create_builder_page(user=user)
data_source1 = data_fixture.create_builder_local_baserow_list_rows_data_source(
page=page
)
table_element = data_fixture.create_builder_table_element(
page=page,
data_source=data_source1,
fields=[
{
"name": "Field",
"type": "button",
"config": {"label": "Click me"},
},
],
)
table_element_type = table_element.get_type()
# Export the table element and check there are no table elements after deleting it
exported = table_element_type.export_serialized(table_element)
ElementService().delete_element(user, table_element)
assert TableElement.objects.count() == 0
# After importing the table element the fields should be properly imported too
id_mapping = defaultdict(lambda: MirrorDict())
table_element_type.import_serialized(page, exported, id_mapping)
assert (
TableElement.objects.filter(
page=page,
data_source=data_source1,
fields__name="Field",
fields__type="button",
).count()
== 1
)

View file

@ -393,7 +393,12 @@ def test_builder_application_export(data_fixture):
"items_per_page": 42,
"data_source_id": element4.data_source.id,
"fields": [
{"name": f.name, "type": f.type, "config": f.config}
{
"name": f.name,
"type": f.type,
"config": f.config,
"uid": f.uid,
}
for f in element4.fields.all()
],
},
@ -523,10 +528,12 @@ IMPORT_REFERENCE = {
"name": "F 1",
"type": "text",
"config": {"value": "get('current_record.field_25')"},
"uid": uuid.uuid4(),
},
{
"name": "F 2",
"type": "link",
"uid": uuid.uuid4(),
"config": {
"page_parameters": [],
"navigation_type": "custom",

View file

@ -0,0 +1,7 @@
{
"type": "feature",
"message": "Add a new \"button\" field to table element that can trigger actions based on current rows.",
"issue_number": 2522,
"bullet_points": [],
"created_at": "2024-05-23"
}

View file

@ -156,7 +156,9 @@ export default {
this.values.password = ''
this.values.email = ''
this.$v.$reset()
this.fireAfterLoginEvent()
this.fireEvent(
this.elementType.getEventByName(this.element, 'after_login')
)
} catch (error) {
if (error.handler) {
const response = error.handler.response

View file

@ -28,8 +28,8 @@ export class AuthFormElementType extends ElementType {
return AuthFormElementForm
}
get events() {
return [AfterLoginEvent]
getEvents(element) {
return [new AfterLoginEvent({ ...this.app })]
}
isInError({ builder, element }) {

View file

@ -2,6 +2,8 @@ import { Registerable } from '@baserow/modules/core/registry'
import BooleanField from '@baserow/modules/builder/components/elements/components/collectionField/BooleanField'
import TextField from '@baserow/modules/builder/components/elements/components/collectionField/TextField'
import LinkField from '@baserow/modules/builder/components/elements/components/collectionField/LinkField'
import ButtonField from '@baserow/modules/builder/components/elements/components/collectionField/ButtonField.vue'
import ButtonFieldForm from '@baserow/modules/builder/components/elements/components/collectionField/form/ButtonFieldForm.vue'
import BooleanFieldForm from '@baserow/modules/builder/components/elements/components/collectionField/form/BooleanFieldForm'
import TagsField from '@baserow/modules/builder/components/elements/components/collectionField/TagsField.vue'
import TextFieldForm from '@baserow/modules/builder/components/elements/components/collectionField/form/TextFieldForm'
@ -14,6 +16,7 @@ import {
} from '@baserow/modules/core/utils/validator'
import resolveElementUrl from '@baserow/modules/builder/utils/urlResolution'
import { pathParametersInError } from '@baserow/modules/builder/utils/params'
import { ClickEvent } from '@baserow/modules/builder/eventTypes'
export class CollectionFieldType extends Registerable {
get name() {
@ -28,6 +31,10 @@ export class CollectionFieldType extends Registerable {
return null
}
get events() {
return []
}
getProps(field, { resolveFormula, applicationContext }) {
return {}
}
@ -183,3 +190,29 @@ export class TagsCollectionFieldType extends CollectionFieldType {
return 10
}
}
export class ButtonCollectionFieldType extends CollectionFieldType {
static getType() {
return 'button'
}
get name() {
return 'Button'
}
get component() {
return ButtonField
}
get formComponent() {
return ButtonFieldForm
}
get events() {
return [ClickEvent]
}
getProps(field, { resolveFormula, applicationContext }) {
return { label: ensureString(resolveFormula(field.label)) }
}
}

View file

@ -12,9 +12,18 @@
</tr>
</thead>
<tbody v-if="rows.length">
<tr v-for="row in rows" :key="row.__id__" class="baserow-table__row">
<tr
v-for="(row, index) in rows"
:key="row.__id__"
class="baserow-table__row"
>
<td v-for="field in fields" :key="field.id" class="baserow-table__cell">
<slot name="cell-content" :value="row[field.name]" :field="field">
<slot
name="cell-content"
:value="row[field.name]"
:field="field"
:row-index="index"
>
{{ value }}
</slot>
</td>

View file

@ -9,7 +9,7 @@
<ABButton
:full-width="element.width === WIDTHS.FULL.value"
:loading="workflowActionsInProgress"
@click="fireClickEvent"
@click="fireEvent(elementType.getEventByName(element, 'click'))"
>
{{ resolvedValue || $t('buttonElement.noValue') }}
</ABButton>

View file

@ -160,7 +160,7 @@ export default {
validateAndSubmitEvent() {
this.setFormElementDescendantsTouched(true)
if (!this.formElementChildrenAreInvalid) {
this.fireSubmitEvent()
this.fireEvent(this.elementType.getEventByName(this.element, 'submit'))
this.resetFormContainerElements()
}
},

View file

@ -6,9 +6,14 @@
class="table-element"
>
<BaserowTable :fields="element.fields" :rows="rows">
<template #cell-content="{ field, value }">
<template #cell-content="{ rowIndex, field, value }">
<component
:is="collectionFieldTypes[field.type].component"
:element="element"
:field="field"
:application-context-additions="{
recordIndex: rowIndex,
}"
v-bind="value"
/>
</template>

View file

@ -0,0 +1,53 @@
<template>
<ABButton
:loading="workflowActionsInProgress"
@click="fireEvent(elementType.getEventByName(element, eventName))"
>
{{ labelWithDefault }}
</ABButton>
</template>
<script>
import element from '@baserow/modules/builder/mixins/element'
export default {
name: 'ButtonField',
mixins: [element],
props: {
element: {
type: Object,
required: true,
},
label: {
type: String,
required: true,
},
field: {
type: Object,
required: true,
},
},
computed: {
eventName() {
return `${this.field.uid}_click`
},
workflowActionsInProgress() {
const workflowActions = this.$store.getters[
'workflowAction/getElementWorkflowActions'
](this.page, this.element.id)
return workflowActions
.filter((wa) => wa.event === this.eventName)
.some((workflowAction) =>
this.$store.getters['workflowAction/getDispatching'](workflowAction)
)
},
labelWithDefault() {
if (this.label) {
return this.label
} else {
return this.$t('buttonField.noLabel')
}
},
},
}
</script>

View file

@ -0,0 +1,49 @@
<template>
<form @submit.prevent @keydown.enter.prevent>
<ApplicationBuilderFormulaInputGroup
v-model="values.label"
small
:label="$t('generalForm.labelTitle')"
:placeholder="$t('buttonFieldForm.labelPlaceholder')"
:data-providers-allowed="DATA_PROVIDERS_ALLOWED_ELEMENTS"
:application-context-additions="{
element,
}"
horizontal
/>
<Alert>
{{ $t('buttonFieldForm.infoMessage') }}
</Alert>
</form>
</template>
<script>
import { DATA_PROVIDERS_ALLOWED_ELEMENTS } from '@baserow/modules/builder/enums'
import form from '@baserow/modules/core/mixins/form'
import ApplicationBuilderFormulaInputGroup from '@baserow/modules/builder/components/ApplicationBuilderFormulaInputGroup'
export default {
name: 'ButtonFieldForm',
components: { ApplicationBuilderFormulaInputGroup },
mixins: [form],
props: {
element: {
type: Object,
required: true,
},
},
data() {
return {
allowedValues: ['label'],
values: {
label: '',
},
}
},
computed: {
DATA_PROVIDERS_ALLOWED_ELEMENTS() {
return DATA_PROVIDERS_ALLOWED_ELEMENTS
},
},
}
</script>

View file

@ -212,12 +212,20 @@ export default {
value: '',
type: 'text',
id: uuid(), // Temporary id
uid: uuid(),
})
},
changeFieldType(fieldToUpdate, newType) {
this.values.fields = this.values.fields.map((field) => {
if (field.id === fieldToUpdate.id) {
return { id: field.id, name: field.name, type: newType }
// When the type of the workflow action changes we assign a new UID to
// trigger the backend workflow action removal
return {
id: field.id,
uid: uuid(),
name: field.name,
type: newType,
}
}
return field
})

View file

@ -127,7 +127,7 @@ export default {
await this.actionCreateWorkflowAction({
page: this.page,
workflowActionType: DEFAULT_WORKFLOW_ACTION_TYPE,
eventType: this.event.getType(),
eventType: this.event.name,
configuration: {
element_id: this.element.id,
},

View file

@ -17,6 +17,9 @@
:is="component"
:element="element"
:children="children"
:application-context-additions="{
element,
}"
class="element"
/>
</div>
@ -29,20 +32,12 @@ import { resolveColor } from '@baserow/modules/core/utils/colors'
import { themeToColorVariables } from '@baserow/modules/builder/utils/theme'
import { BACKGROUND_TYPES, WIDTH_TYPES } from '@baserow/modules/builder/enums'
import applicationContextMixin from '@baserow/modules/builder/mixins/applicationContext'
export default {
name: 'PageElement',
inject: ['builder', 'page', 'mode', 'applicationContext'],
provide() {
return {
mode: this.elementMode,
applicationContext: {
...this.applicationContext,
element: this.element,
...this.applicationContextAdditions,
},
}
},
mixins: [applicationContextMixin],
inject: ['builder', 'page', 'mode'],
props: {
element: {
type: Object,
@ -53,11 +48,6 @@ export default {
required: false,
default: null,
},
applicationContextAdditions: {
type: Object,
required: false,
default: null,
},
},
computed: {
BACKGROUND_TYPES: () => BACKGROUND_TYPES,

View file

@ -1,8 +1,8 @@
<template>
<div>
<Event
v-for="event in elementType.getEvents()"
:key="event.getType()"
v-for="event in elementType.getEvents(element)"
:key="event.name"
:element="element"
:event="event"
:available-workflow-action-types="availableWorkflowActionTypes"
@ -34,7 +34,7 @@ export default {
methods: {
getWorkflowActionsForEvent(event) {
return this.workflowActions.filter(
(workflowAction) => workflowAction.event === event.getType()
(workflowAction) => workflowAction.event === event.name
)
},
},

View file

@ -503,7 +503,7 @@ export class PreviousActionDataProviderType extends DataProviderType {
const previousActions = this.app.store.getters[
'workflowAction/getElementPreviousWorkflowActions'
](page, applicationContext.element.id, applicationContext.workflowAction.id)
](page, applicationContext.element.id, applicationContext.workflowAction)
const previousActionSchema = _.chain(previousActions)
// Retrieve the associated schema for each action

View file

@ -87,10 +87,6 @@ export class ElementType extends Registerable {
return this.stylesAll
}
get events() {
return []
}
/**
* Returns a display name for this element, so it can be distinguished from
* other elements of the same type.
@ -102,8 +98,12 @@ export class ElementType extends Registerable {
return this.name
}
getEvents() {
return this.events.map((EventType) => new EventType(this.app))
getEvents(element) {
return []
}
getEventByName(element, name) {
return this.getEvents(element).find((event) => event.name === name)
}
/**
@ -505,14 +505,14 @@ export class FormContainerElementType extends ContainerElementTypeMixin(
return this.elementTypesAll.filter((type) => !type.isFormElement)
}
get events() {
return [SubmitEvent]
}
get childStylesForbidden() {
return ['style_width']
}
getEvents(element) {
return [new SubmitEvent({ ...this.app })]
}
/**
* Return an array of placements that are disallowed for the elements to move
* in their container.
@ -701,6 +701,24 @@ export class TableElementType extends CollectionElementTypeMixin(ElementType) {
return TableElementForm
}
getEvents(element) {
return (element.fields || [])
.map(({ type, name, uid }) => {
const collectionFieldType = this.app.$registry.get(
'collectionField',
type
)
return collectionFieldType.events.map((EventType) => {
return new EventType({
...this.app,
namePrefix: uid,
labelSuffix: `- ${name}`,
})
})
})
.flat()
}
async onElementEvent(event, { page, element, dataSourceId }) {
if (event === ELEMENT_EVENTS.DATA_SOURCE_REMOVED) {
if (element.data_source_id === dataSourceId) {
@ -1148,8 +1166,8 @@ export class ButtonElementType extends ElementType {
return ButtonElementForm
}
get events() {
return [ClickEvent]
getEvents(element) {
return [new ClickEvent({ ...this.app })]
}
getDisplayName(element, applicationContext) {

View file

@ -8,41 +8,31 @@ import { resolveFormula } from '@baserow/modules/core/formula'
* registry required.
*/
export class Event {
constructor({ i18n, store, registry }) {
constructor({ i18n, store, $registry, name, label }) {
this.i18n = i18n
this.store = store
this.registry = registry
}
static getType() {
throw new Error('getType needs to be implemented')
}
getType() {
return this.constructor.getType()
}
get label() {
return null
this.$registry = $registry
this.name = name
this.label = label
}
async fire({ workflowActions, applicationContext }) {
const additionalContext = {}
for (let i = 0; i < workflowActions.length; i += 1) {
const workflowAction = workflowActions[i]
const workflowActionType = this.registry.get(
const workflowActionType = this.$registry.get(
'workflowAction',
workflowAction.type
)
const localResolveFormula = (formula) => {
const formulaFunctions = {
get: (name) => {
return this.registry.get('runtimeFormulaFunction', name)
return this.$registry.get('runtimeFormulaFunction', name)
},
}
const runtimeFormulaContext = new Proxy(
new RuntimeFormulaContext(
this.registry.getAll('builderDataProvider'),
this.$registry.getAll('builderDataProvider'),
{ ...applicationContext, previousActionResults: additionalContext }
),
{
@ -89,31 +79,33 @@ export class Event {
}
export class ClickEvent extends Event {
static getType() {
return 'click'
}
get label() {
return this.i18n.t('eventTypes.clickLabel')
constructor({ namePrefix, labelSuffix, ...rest }) {
super({
...rest,
name: namePrefix ? `${namePrefix}_click` : 'click',
label: labelSuffix
? `${rest.i18n.t('eventTypes.clickLabel')} ${labelSuffix}`
: rest.i18n.t('eventTypes.clickLabel'),
})
}
}
export class SubmitEvent extends Event {
static getType() {
return 'submit'
}
get label() {
return this.i18n.t('eventTypes.submitLabel')
constructor(args) {
super({
name: 'submit',
label: args.i18n.t('eventTypes.submitLabel'),
...args,
})
}
}
export class AfterLoginEvent extends Event {
static getType() {
return 'after_login'
}
get label() {
return this.i18n.t('eventTypes.afterLoginLabel')
constructor(args) {
super({
name: 'after_login',
label: args.i18n.t('eventTypes.afterLoginLabel'),
...args,
})
}
}

View file

@ -566,5 +566,12 @@
"id": "Id",
"email": "Email",
"username": "Username"
},
"buttonField": {
"noLabel": "Unnamed..."
},
"buttonFieldForm": {
"infoMessage": "To configure actions for this button, open the \"Events\" tab of the current element.",
"labelPlaceholder": "Enter a label..."
}
}

View file

@ -0,0 +1,23 @@
export default {
inject: { injectedApplicationContext: { from: 'applicationContext' } },
provide() {
return {
applicationContext: this.applicationContext,
}
},
props: {
applicationContextAdditions: {
type: Object,
required: false,
default: null,
},
},
computed: {
applicationContext() {
return {
...this.injectedApplicationContext,
...this.applicationContextAdditions,
}
},
},
}

View file

@ -1,34 +1,17 @@
import RuntimeFormulaContext from '@baserow/modules/core/runtimeFormulaContext'
import { resolveFormula } from '@baserow/modules/core/formula'
import {
ClickEvent,
SubmitEvent,
AfterLoginEvent,
} from '@baserow/modules/builder/eventTypes'
import { resolveColor } from '@baserow/modules/core/utils/colors'
import { themeToColorVariables } from '@baserow/modules/builder/utils/theme'
import applicationContextMixin from '@baserow/modules/builder/mixins/applicationContext'
export default {
inject: ['workspace', 'builder', 'page', 'mode', 'applicationContext'],
provide() {
return {
applicationContext: {
...this.applicationContext,
element: this.element,
...this.applicationContextAdditions,
},
}
},
inject: ['workspace', 'builder', 'page', 'mode'],
mixins: [applicationContextMixin],
props: {
element: {
type: Object,
required: true,
},
applicationContextAdditions: {
type: Object,
required: false,
default: null,
},
},
computed: {
workflowActionsInProgress() {
@ -84,7 +67,7 @@ export default {
return ''
}
},
async fireEvent(EventType) {
async fireEvent(event) {
if (this.mode !== 'editing') {
if (this.workflowActionsInProgress) {
return false
@ -92,14 +75,12 @@ export default {
const workflowActions = this.$store.getters[
'workflowAction/getElementWorkflowActions'
](this.page, this.element.id)
](this.page, this.element.id).filter(
({ event: eventName }) => eventName === event.name
)
try {
await new EventType({
i18n: this.$i18n,
store: this.$store,
registry: this.$registry,
}).fire({
await event.fire({
workflowActions,
resolveFormula: this.resolveFormula,
applicationContext: this.applicationContext,
@ -126,15 +107,6 @@ export default {
}
}
},
fireClickEvent() {
return this.fireEvent(ClickEvent)
},
fireSubmitEvent() {
this.fireEvent(SubmitEvent)
},
fireAfterLoginEvent() {
this.fireEvent(AfterLoginEvent)
},
resolveColor,
},

View file

@ -100,7 +100,7 @@ export class EventsPageSidePanelType extends pageSidePanelType {
isDeactivated(element) {
const elementType = this.app.$registry.get('element', element.type)
return elementType.getEvents().length === 0
return elementType.getEvents(element).length === 0
}
getOrder() {

View file

@ -98,6 +98,7 @@ import {
BooleanCollectionFieldType,
TextCollectionFieldType,
LinkCollectionFieldType,
ButtonCollectionFieldType,
TagsCollectionFieldType,
} from '@baserow/modules/builder/collectionFieldTypes'
@ -283,4 +284,8 @@ export default (context) => {
'collectionField',
new TagsCollectionFieldType(context)
)
app.$registry.register(
'collectionField',
new ButtonCollectionFieldType(context)
)
}

View file

@ -244,10 +244,15 @@ const getters = {
.sort((a, b) => a.order - b.order)
},
getElementPreviousWorkflowActions:
(state, getters) => (page, elementId, workflowActionId) => {
(state, getters) => (page, elementId, workflowAction) => {
return _.takeWhile(
getters.getElementWorkflowActions(page, elementId),
(workflowAction) => workflowAction.id !== workflowActionId
getters
.getElementWorkflowActions(page, elementId)
.filter(
(workflowActionToFilter) =>
workflowActionToFilter.event === workflowAction.event
),
(workflowActionAll) => workflowActionAll.id !== workflowAction.id
)
},
getLoading: (state) => (workflowAction) => {

View file

@ -142,6 +142,10 @@ export class RefreshDataSourceWorkflowActionType extends WorkflowActionType {
}
)
}
getDataSchema(workflowAction) {
return null
}
}
export class WorkflowActionServiceType extends WorkflowActionType {

View file

@ -11,7 +11,7 @@
<ul class="tabs__header">
<li
v-for="(tab, index) in tabs"
:key="tab.title"
:key="`${tab.title} ${tab.tooltip}`"
v-tooltip="tab.tooltip"
class="tabs__item"
:class="{