mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-03-13 04:03:22 +00:00
Resolve "Allow to use the result from the previous action in the next action"
This commit is contained in:
parent
a602c3b9e4
commit
67f77c8515
29 changed files with 476 additions and 67 deletions
backend
src/baserow/contrib
builder
integrations/local_baserow
tests/baserow/contrib
builder
integrations/local_baserow
changelog/entries/unreleased/feature
web-frontend/modules
builder
components
dataProviderTypes.jsenums.jseventTypes.jslocales
mixins
pages
plugin.jsstore
workflowActionTypes.jscore
|
@ -206,6 +206,7 @@ class BuilderConfig(AppConfig):
|
|||
DataSourceDataProviderType,
|
||||
FormDataProviderType,
|
||||
PageParameterDataProviderType,
|
||||
PreviousActionProviderType,
|
||||
UserDataProviderType,
|
||||
)
|
||||
|
||||
|
@ -213,6 +214,7 @@ class BuilderConfig(AppConfig):
|
|||
builder_data_provider_type_registry.register(PageParameterDataProviderType())
|
||||
builder_data_provider_type_registry.register(CurrentRecordDataProviderType())
|
||||
builder_data_provider_type_registry.register(FormDataProviderType())
|
||||
builder_data_provider_type_registry.register(PreviousActionProviderType())
|
||||
builder_data_provider_type_registry.register(UserDataProviderType())
|
||||
|
||||
from baserow.contrib.builder.theme.operations import UpdateThemeOperationType
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from typing import Any, List, Type, Union
|
||||
|
||||
from baserow.contrib.builder.data_providers.exceptions import (
|
||||
DataProviderChunkInvalidException,
|
||||
FormDataProviderChunkInvalidException,
|
||||
)
|
||||
from baserow.contrib.builder.data_sources.builder_dispatch_context import (
|
||||
|
@ -14,10 +15,14 @@ from baserow.contrib.builder.data_sources.handler import DataSourceHandler
|
|||
from baserow.contrib.builder.elements.element_types import FormElementType
|
||||
from baserow.contrib.builder.elements.handler import ElementHandler
|
||||
from baserow.contrib.builder.elements.models import FormElement
|
||||
from baserow.contrib.builder.workflow_actions.handler import (
|
||||
BuilderWorkflowActionHandler,
|
||||
)
|
||||
from baserow.core.formula.exceptions import FormulaRecursion
|
||||
from baserow.core.formula.registries import DataProviderType
|
||||
from baserow.core.services.dispatch_context import DispatchContext
|
||||
from baserow.core.utils import get_value_at_path
|
||||
from baserow.core.workflow_actions.exceptions import WorkflowActionDoesNotExist
|
||||
|
||||
|
||||
class PageParameterDataProviderType(DataProviderType):
|
||||
|
@ -206,6 +211,42 @@ class CurrentRecordDataProviderType(DataProviderType):
|
|||
return rest
|
||||
|
||||
|
||||
class PreviousActionProviderType(DataProviderType):
|
||||
"""
|
||||
The previous action provider can read data from registered page workflow actions.
|
||||
"""
|
||||
|
||||
type = "previous_action"
|
||||
|
||||
def get_data_chunk(self, dispatch_context: DispatchContext, path: List[str]):
|
||||
previous_action_id, *rest = path
|
||||
previous_action = dispatch_context.request.data.get("previous_action", {})
|
||||
|
||||
if previous_action_id not in previous_action:
|
||||
message = "The previous action id is not present in the dispatch context"
|
||||
raise DataProviderChunkInvalidException(message)
|
||||
return get_value_at_path(previous_action, path)
|
||||
|
||||
def import_path(self, path, id_mapping, **kwargs):
|
||||
workflow_action_id, *rest = path
|
||||
|
||||
if "builder_workflow_actions" in id_mapping:
|
||||
try:
|
||||
workflow_action_id = id_mapping["builder_workflow_actions"][
|
||||
int(workflow_action_id)
|
||||
]
|
||||
workflow_action = BuilderWorkflowActionHandler().get_workflow_action(
|
||||
workflow_action_id
|
||||
)
|
||||
except (KeyError, WorkflowActionDoesNotExist):
|
||||
return [str(workflow_action_id), *rest]
|
||||
|
||||
service_type = workflow_action.service.specific.get_type()
|
||||
rest = service_type.import_path(rest, id_mapping)
|
||||
|
||||
return [str(workflow_action_id), *rest]
|
||||
|
||||
|
||||
class UserDataProviderType(DataProviderType):
|
||||
"""
|
||||
This data provider user the user in `request.user_source_user` to resolve formula
|
||||
|
|
|
@ -655,6 +655,9 @@ class PageHandler:
|
|||
:return: the newly created instance list.
|
||||
"""
|
||||
|
||||
# Sort action because we might have formula that use previous actions
|
||||
serialized_workflow_actions.sort(key=lambda action: action["order"])
|
||||
|
||||
for serialized_workflow_action in serialized_workflow_actions:
|
||||
BuilderWorkflowActionHandler().import_workflow_action(
|
||||
page, serialized_workflow_action, id_mapping
|
||||
|
|
|
@ -22,6 +22,9 @@ from rest_framework.fields import (
|
|||
)
|
||||
from rest_framework.serializers import ListSerializer, Serializer
|
||||
|
||||
from baserow.contrib.builder.data_providers.exceptions import (
|
||||
DataProviderChunkInvalidException,
|
||||
)
|
||||
from baserow.contrib.builder.formula_importer import import_formula
|
||||
from baserow.contrib.database.api.fields.serializers import (
|
||||
DurationFieldSerializer,
|
||||
|
@ -1207,6 +1210,26 @@ class LocalBaserowUpsertRowServiceType(LocalBaserowTableServiceType):
|
|||
"""
|
||||
|
||||
resolved_values = super().resolve_service_formulas(service, dispatch_context)
|
||||
field_mappings = service.field_mappings.select_related("field").all()
|
||||
for field_mapping in field_mappings:
|
||||
try:
|
||||
resolved_values[field_mapping.id] = resolve_formula(
|
||||
field_mapping.value,
|
||||
formula_runtime_function_registry,
|
||||
dispatch_context,
|
||||
)
|
||||
except DataProviderChunkInvalidException as e:
|
||||
message = (
|
||||
"Path error in formula for "
|
||||
f"field {field_mapping.field.name}({field_mapping.field.id})"
|
||||
)
|
||||
raise ServiceImproperlyConfigured(message) from e
|
||||
except Exception as e:
|
||||
message = (
|
||||
"Unknown error in formula for "
|
||||
f"field {field_mapping.field.name}({field_mapping.field.id})"
|
||||
)
|
||||
raise ServiceImproperlyConfigured(message) from e
|
||||
|
||||
if not service.row_id:
|
||||
# We've received no `row_id` as we're creating a new row.
|
||||
|
@ -1226,11 +1249,13 @@ class LocalBaserowUpsertRowServiceType(LocalBaserowTableServiceType):
|
|||
"The result of the `row_id` formula must be an integer or convertible "
|
||||
"to an integer."
|
||||
)
|
||||
except DataProviderChunkInvalidException as e:
|
||||
message = f"Formula for row {service.row_id} could not be resolved."
|
||||
raise ServiceImproperlyConfigured(message) from e
|
||||
except Exception as e:
|
||||
raise ServiceImproperlyConfigured(
|
||||
f"The `row_id` formula can't be resolved: {e}"
|
||||
)
|
||||
|
||||
return resolved_values
|
||||
|
||||
def dispatch_data(
|
||||
|
@ -1256,6 +1281,9 @@ class LocalBaserowUpsertRowServiceType(LocalBaserowTableServiceType):
|
|||
field_values = {}
|
||||
field_mappings = service.field_mappings.select_related("field").all()
|
||||
for field_mapping in field_mappings:
|
||||
if field_mapping.id not in resolved_values:
|
||||
continue
|
||||
|
||||
field = field_mapping.field
|
||||
field_type = field_type_registry.get_by_model(field.specific_class)
|
||||
|
||||
|
@ -1309,3 +1337,27 @@ class LocalBaserowUpsertRowServiceType(LocalBaserowTableServiceType):
|
|||
)
|
||||
|
||||
return {"data": row, "baserow_table_model": table.get_model()}
|
||||
|
||||
def import_path(self, path, id_mapping):
|
||||
"""
|
||||
Updates the field ids in the path.
|
||||
"""
|
||||
|
||||
# If the path length is greater or equal to one, then we have
|
||||
# the current data source formula format of row, and field.
|
||||
if len(path) >= 1:
|
||||
field_dbname, *rest = path
|
||||
else:
|
||||
# In any other scenario, we have a formula that is not a format we
|
||||
# can currently import properly, so we return the path as is.
|
||||
return path
|
||||
|
||||
if field_dbname == "id":
|
||||
return path
|
||||
|
||||
original_field_id = int(field_dbname[6:])
|
||||
field_id = id_mapping.get("database_fields", {}).get(
|
||||
original_field_id, original_field_id
|
||||
)
|
||||
|
||||
return [f"field_{field_id}", *rest]
|
||||
|
|
|
@ -621,7 +621,9 @@ def test_dispatch_workflow_action_with_invalid_form_data(
|
|||
service = workflow_action.service.specific
|
||||
service.table = table
|
||||
service.save()
|
||||
service.field_mappings.create(field=field, value="get('form_data.17')")
|
||||
field_mapping = service.field_mappings.create(
|
||||
field=field, value="get('form_data.17')"
|
||||
)
|
||||
|
||||
url = reverse(
|
||||
"api:builder:workflow_action:dispatch",
|
||||
|
@ -641,7 +643,7 @@ def test_dispatch_workflow_action_with_invalid_form_data(
|
|||
|
||||
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||
assert response.json() == {
|
||||
"error": "ERROR_WORKFLOW_ACTION_FORM_DATA_INVALID",
|
||||
"detail": "The form data provided to the workflow action "
|
||||
"contained invalid values.",
|
||||
"error": "ERROR_WORKFLOW_ACTION_IMPROPERLY_CONFIGURED",
|
||||
"detail": "The workflow_action configuration is incorrect: "
|
||||
f"Path error in formula for field {field.name}({field.id})",
|
||||
}
|
||||
|
|
|
@ -10,9 +10,11 @@ from baserow.contrib.builder.data_providers.data_provider_types import (
|
|||
DataSourceDataProviderType,
|
||||
FormDataProviderType,
|
||||
PageParameterDataProviderType,
|
||||
PreviousActionProviderType,
|
||||
UserDataProviderType,
|
||||
)
|
||||
from baserow.contrib.builder.data_providers.exceptions import (
|
||||
DataProviderChunkInvalidException,
|
||||
FormDataProviderChunkInvalidException,
|
||||
)
|
||||
from baserow.contrib.builder.data_sources.builder_dispatch_context import (
|
||||
|
@ -692,6 +694,37 @@ def test_form_data_provider_type_import_path(data_fixture):
|
|||
assert path_imported == [str(element_duplicated.id), "test"]
|
||||
|
||||
|
||||
def test_previous_action_data_provider_get_data_chunk():
|
||||
previous_action_data_provider = PreviousActionProviderType()
|
||||
|
||||
fake_request = MagicMock()
|
||||
fake_request.data = {"previous_action": {"id": 42}}
|
||||
dispatch_context = BuilderDispatchContext(fake_request, None)
|
||||
|
||||
assert previous_action_data_provider.get_data_chunk(dispatch_context, ["id"]) == 42
|
||||
with pytest.raises(DataProviderChunkInvalidException):
|
||||
previous_action_data_provider.get_data_chunk(dispatch_context, ["invalid"])
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_previous_action_data_provider_import_path():
|
||||
previous_action_data_provider = PreviousActionProviderType()
|
||||
path = ["1", "field"]
|
||||
|
||||
valid_id_mapping = {"builder_workflow_actions": {1: 2}}
|
||||
invalid_id_mapping = {"builder_workflow_actions": {0: 1}}
|
||||
|
||||
assert previous_action_data_provider.import_path(path, {}) == ["1", "field"]
|
||||
assert previous_action_data_provider.import_path(path, invalid_id_mapping) == [
|
||||
"1",
|
||||
"field",
|
||||
]
|
||||
assert previous_action_data_provider.import_path(path, valid_id_mapping) == [
|
||||
"2",
|
||||
"field",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_user_data_provider_get_data_chunk(data_fixture):
|
||||
user = data_fixture.create_user()
|
||||
|
|
|
@ -2150,15 +2150,21 @@ def test_local_baserow_upsert_row_service_dispatch_data_incompatible_value(
|
|||
service_type = service.get_type()
|
||||
dispatch_context = BuilderDispatchContext(Mock(), page)
|
||||
|
||||
service.field_mappings.create(field=boolean_field, value="'Horse'")
|
||||
field_mapping = service.field_mappings.create(field=boolean_field, value="'Horse'")
|
||||
with pytest.raises(DRFValidationError) as exc:
|
||||
service_type.dispatch_data(service, {"table": table}, dispatch_context)
|
||||
service_type.dispatch_data(
|
||||
service, {"table": table, field_mapping.id: "Horse"}, dispatch_context
|
||||
)
|
||||
|
||||
service.field_mappings.all().delete()
|
||||
|
||||
service.field_mappings.create(field=single_field, value="'99999999999'")
|
||||
field_mapping = service.field_mappings.create(
|
||||
field=single_field, value="'99999999999'"
|
||||
)
|
||||
with pytest.raises(ServiceImproperlyConfigured) as exc:
|
||||
service_type.dispatch_data(service, {"table": table}, dispatch_context)
|
||||
service_type.dispatch_data(
|
||||
service, {"table": table, field_mapping.id: "99999999999"}, dispatch_context
|
||||
)
|
||||
|
||||
assert exc.value.args[0] == (
|
||||
"The result value of the formula is not valid for the "
|
||||
|
@ -2349,6 +2355,17 @@ def test_local_baserow_upsert_row_service_after_update(data_fixture):
|
|||
assert service.field_mappings.count() == 0
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_local_baserow_upsert_row_service_type_import_path(data_fixture):
|
||||
imported_upsert_row_service_type = LocalBaserowUpsertRowServiceType()
|
||||
|
||||
assert imported_upsert_row_service_type.import_path(["id"], {}) == ["id"]
|
||||
assert imported_upsert_row_service_type.import_path(["field_1"], {}) == ["field_1"]
|
||||
assert imported_upsert_row_service_type.import_path(
|
||||
["field_1"], {"database_fields": {1: 2}}
|
||||
) == ["field_2"]
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_import_datasource_provider_formula_using_list_rows_service_containing_no_row_or_field_fails_silently(
|
||||
data_fixture,
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"type": "feature",
|
||||
"message": "Add a new data provider that allows to use values from previous actions in formulas",
|
||||
"issue_number": 2224,
|
||||
"bullet_points": [],
|
||||
"created_at": "2024-03-06"
|
||||
}
|
|
@ -3,12 +3,14 @@
|
|||
v-bind="$attrs"
|
||||
:data-explorer-loading="dataExplorerLoading"
|
||||
:data-providers="dataProviders"
|
||||
:application-context="{
|
||||
:application-context="
|
||||
applicationContext ?? {
|
||||
page,
|
||||
builder,
|
||||
mode,
|
||||
...applicationContextAdditions,
|
||||
}"
|
||||
}
|
||||
"
|
||||
v-on="$listeners"
|
||||
></FormulaInputGroup>
|
||||
</template>
|
||||
|
@ -19,7 +21,21 @@ import { DataSourceDataProviderType } from '@baserow/modules/builder/dataProvide
|
|||
export default {
|
||||
name: 'ApplicationBuilderFormulaInputGroup',
|
||||
components: { FormulaInputGroup },
|
||||
inject: ['page', 'builder', 'mode'],
|
||||
inject: {
|
||||
page: {
|
||||
from: 'page',
|
||||
},
|
||||
builder: {
|
||||
from: 'builder',
|
||||
},
|
||||
mode: {
|
||||
from: 'mode',
|
||||
},
|
||||
applicationContext: {
|
||||
from: 'applicationContext',
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
dataProvidersAllowed: {
|
||||
type: Array,
|
||||
|
|
|
@ -130,7 +130,7 @@ export default {
|
|||
return this.$registry.getAll('collectionField')
|
||||
},
|
||||
dispatchContext() {
|
||||
return DataProviderType.getAllDispatchContext(
|
||||
return DataProviderType.getAllDataSourceDispatchContext(
|
||||
this.$registry.getAll('builderDataProvider'),
|
||||
this.applicationContext
|
||||
)
|
||||
|
|
|
@ -20,9 +20,18 @@
|
|||
</template>
|
||||
<template #default>
|
||||
<div>
|
||||
<!--
|
||||
By setting the WorkflowAction 'key' property to '$id_$order_$workflow.length'
|
||||
we ensure that the component is re-rendered once that value changes.
|
||||
This value will change after an action is ordered, thus triggering the
|
||||
rendering engine, which can be useful when we want instant visual
|
||||
feedback.
|
||||
One example would be to highlight formulas that become invalid after
|
||||
action ordering.
|
||||
-->
|
||||
<WorkflowAction
|
||||
v-for="(workflowAction, index) in workflowActions"
|
||||
:key="workflowAction.id"
|
||||
:key="`${workflowAction.id}_${workflowAction.order}_${workflowActions.length}`"
|
||||
v-sortable="{
|
||||
id: workflowAction.id,
|
||||
handle: '[data-sortable-handle]',
|
||||
|
@ -30,8 +39,10 @@
|
|||
}"
|
||||
class="event__workflow-action"
|
||||
:class="{ 'event__workflow-action--first': index === 0 }"
|
||||
:element="element"
|
||||
:available-workflow-action-types="availableWorkflowActionTypes"
|
||||
:workflow-action="workflowAction"
|
||||
:workflow-action-index="index"
|
||||
@delete="deleteWorkflowAction(workflowAction)"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -12,15 +12,11 @@
|
|||
import UpsertRowWorkflowActionForm from '@baserow/modules/integrations/localBaserow/components/services/LocalBaserowUpsertRowServiceForm'
|
||||
import form from '@baserow/modules/core/mixins/form'
|
||||
import _ from 'lodash'
|
||||
import { DATA_PROVIDERS_ALLOWED_ELEMENTS } from '@baserow/modules/builder/enums'
|
||||
|
||||
export default {
|
||||
name: 'CreateRowWorkflowAction',
|
||||
components: { UpsertRowWorkflowActionForm },
|
||||
mixins: [form],
|
||||
provide() {
|
||||
return { dataProvidersAllowed: DATA_PROVIDERS_ALLOWED_ELEMENTS }
|
||||
},
|
||||
inject: ['builder'],
|
||||
props: {
|
||||
workflowAction: {
|
||||
|
|
|
@ -16,13 +16,14 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import workflowActionForm from '@baserow/modules/builder/mixins/workflowActionForm'
|
||||
import ApplicationBuilderFormulaInputGroup from '@baserow/modules/builder/components/ApplicationBuilderFormulaInputGroup.vue'
|
||||
import form from '@baserow/modules/core/mixins/form'
|
||||
|
||||
export default {
|
||||
name: 'NotificationWorkflowActionForm',
|
||||
components: { ApplicationBuilderFormulaInputGroup },
|
||||
mixins: [workflowActionForm],
|
||||
mixins: [form],
|
||||
inject: ['dataProvidersAllowed'],
|
||||
data() {
|
||||
return {
|
||||
values: {
|
||||
|
|
|
@ -10,13 +10,14 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import workflowActionForm from '@baserow/modules/builder/mixins/workflowActionForm'
|
||||
import ApplicationBuilderFormulaInputGroup from '@baserow/modules/builder/components/ApplicationBuilderFormulaInputGroup'
|
||||
import form from '@baserow/modules/core/mixins/form'
|
||||
|
||||
export default {
|
||||
name: 'OpenPageWorkflowActionForm',
|
||||
components: { ApplicationBuilderFormulaInputGroup },
|
||||
mixins: [workflowActionForm],
|
||||
mixins: [form],
|
||||
inject: ['dataProvidersAllowed'],
|
||||
data() {
|
||||
return {
|
||||
values: {
|
||||
|
|
|
@ -13,15 +13,11 @@
|
|||
import UpsertRowWorkflowActionForm from '@baserow/modules/integrations/localBaserow/components/services/LocalBaserowUpsertRowServiceForm.vue'
|
||||
import form from '@baserow/modules/core/mixins/form'
|
||||
import _ from 'lodash'
|
||||
import { DATA_PROVIDERS_ALLOWED_ELEMENTS } from '@baserow/modules/builder/enums'
|
||||
|
||||
export default {
|
||||
name: 'UpdateRowWorkflowAction',
|
||||
components: { UpsertRowWorkflowActionForm },
|
||||
mixins: [form],
|
||||
provide() {
|
||||
return { dataProvidersAllowed: DATA_PROVIDERS_ALLOWED_ELEMENTS }
|
||||
},
|
||||
inject: ['builder'],
|
||||
props: {
|
||||
workflowAction: {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import _ from 'lodash'
|
||||
import { DataProviderType } from '@baserow/modules/core/dataProviderTypes'
|
||||
import { getValueAtPath } from '@baserow/modules/core/utils/object'
|
||||
|
||||
|
@ -31,7 +32,7 @@ export class DataSourceDataProviderType extends DataProviderType {
|
|||
'dataSourceContent/fetchPageDataSourceContent',
|
||||
{
|
||||
page: applicationContext.page,
|
||||
data: DataProviderType.getAllDispatchContext(
|
||||
data: DataProviderType.getAllDataSourceDispatchContext(
|
||||
this.app.$registry.getAll('builderDataProvider'),
|
||||
applicationContext
|
||||
),
|
||||
|
@ -165,7 +166,7 @@ export class PageParameterDataProviderType extends DataProviderType {
|
|||
return getValueAtPath(content, path.join('.'))
|
||||
}
|
||||
|
||||
getDispatchContext(applicationContext) {
|
||||
getDataSourceDispatchContext(applicationContext) {
|
||||
return this.getDataContent(applicationContext)
|
||||
}
|
||||
|
||||
|
@ -225,7 +226,8 @@ export class CurrentRecordDataProviderType extends DataProviderType {
|
|||
'dataSource/getPageDataSourceById'
|
||||
](page, element.data_source_id)
|
||||
|
||||
const dispatchContext = DataProviderType.getAllDispatchContext(
|
||||
const dispatchContext =
|
||||
DataProviderType.getAllDataSourceDispatchContext(
|
||||
this.app.$registry.getAll('builderDataProvider'),
|
||||
{ ...applicationContext, element }
|
||||
)
|
||||
|
@ -370,7 +372,7 @@ export class FormDataProviderType extends DataProviderType {
|
|||
})
|
||||
}
|
||||
|
||||
getDispatchContext(applicationContext) {
|
||||
getActionDispatchContext(applicationContext) {
|
||||
return this.getDataContent(applicationContext)
|
||||
}
|
||||
|
||||
|
@ -431,6 +433,92 @@ export class FormDataProviderType extends DataProviderType {
|
|||
}
|
||||
}
|
||||
|
||||
export class PreviousActionDataProviderType extends DataProviderType {
|
||||
static getType() {
|
||||
return 'previous_action'
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.app.i18n.t('dataProviderType.previousAction')
|
||||
}
|
||||
|
||||
get needBackendContext() {
|
||||
return true
|
||||
}
|
||||
|
||||
getActionDispatchContext(applicationContext) {
|
||||
return this.getDataContent(applicationContext)
|
||||
}
|
||||
|
||||
getDataChunk(applicationContext, path) {
|
||||
const content = this.getDataContent(applicationContext)
|
||||
return _.get(content, path.join('.'))
|
||||
}
|
||||
|
||||
getWorkflowActionSchema(workflowAction) {
|
||||
if (workflowAction?.type) {
|
||||
const actionType = this.app.$registry.get(
|
||||
'workflowAction',
|
||||
workflowAction.type
|
||||
)
|
||||
return actionType.getDataSchema(workflowAction)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
getDataContent(applicationContext) {
|
||||
return applicationContext.previousActionResults
|
||||
}
|
||||
|
||||
getDataSchema(applicationContext) {
|
||||
const page = applicationContext.page
|
||||
|
||||
const previousActions = this.app.store.getters[
|
||||
'workflowAction/getElementPreviousWorkflowActions'
|
||||
](page, applicationContext.element.id, applicationContext.workflowAction.id)
|
||||
|
||||
const previousActionSchema = _.chain(previousActions)
|
||||
// Retrieve the associated schema for each action
|
||||
.map((workflowAction) => [
|
||||
workflowAction,
|
||||
this.getWorkflowActionSchema(workflowAction),
|
||||
])
|
||||
// Remove actions without schema
|
||||
.filter(([_, schema]) => schema)
|
||||
// Add an index number to the schema title for each workflow action of
|
||||
// the same type.
|
||||
// For example if we have 2 update and create row actions we want their
|
||||
// titles to be: [Update row, Create row, Update row 2, Create row 2]
|
||||
.groupBy('0.type')
|
||||
.flatMap((workflowActions) =>
|
||||
workflowActions.map(([workflowAction, schema], index) => [
|
||||
workflowAction.id,
|
||||
{ ...schema, title: `${schema.title} ${index ? index + 1 : ''}` },
|
||||
])
|
||||
)
|
||||
// Create the schema object
|
||||
.fromPairs()
|
||||
.value()
|
||||
return { type: 'object', properties: previousActionSchema }
|
||||
}
|
||||
|
||||
getPathTitle(applicationContext, pathParts) {
|
||||
if (pathParts.length === 2) {
|
||||
const page = applicationContext?.page
|
||||
const workflowActionId = parseInt(pathParts[1])
|
||||
|
||||
const action = this.app.store.getters[
|
||||
'workflowAction/getWorkflowActionById'
|
||||
](page, workflowActionId)
|
||||
|
||||
if (!action) {
|
||||
return `action_${workflowActionId}`
|
||||
}
|
||||
}
|
||||
return super.getPathTitle(applicationContext, pathParts)
|
||||
}
|
||||
}
|
||||
|
||||
export class UserDataProviderType extends DataProviderType {
|
||||
static getType() {
|
||||
return 'user'
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
PageParameterDataProviderType,
|
||||
CurrentRecordDataProviderType,
|
||||
FormDataProviderType,
|
||||
PreviousActionDataProviderType,
|
||||
UserDataProviderType,
|
||||
} from '@baserow/modules/builder/dataProviderTypes'
|
||||
|
||||
|
@ -120,6 +121,16 @@ export const DATA_PROVIDERS_ALLOWED_DATA_SOURCES = [
|
|||
PageParameterDataProviderType.getType(),
|
||||
]
|
||||
|
||||
/**
|
||||
* A list of all the data providers that can be used to configure workflow actions.
|
||||
*
|
||||
* @type {String[]}
|
||||
*/
|
||||
export const DATA_PROVIDERS_ALLOWED_WORKFLOW_ACTIONS = [
|
||||
PreviousActionDataProviderType.getType(),
|
||||
...DATA_PROVIDERS_ALLOWED_ELEMENTS,
|
||||
]
|
||||
|
||||
export const ELEMENT_EVENTS = {
|
||||
DATA_SOURCE_REMOVED: 'DATA_SOURCE_REMOVED',
|
||||
DATA_SOURCE_AFTER_UPDATE: 'DATA_SOURCE_AFTER_UPDATE',
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
import RuntimeFormulaContext from '@baserow/modules/core/runtimeFormulaContext'
|
||||
import { resolveFormula } from '@baserow/modules/core/formula'
|
||||
|
||||
/**
|
||||
* This might look like something that belongs in a registry, but it does not.
|
||||
*
|
||||
|
@ -23,7 +26,7 @@ export class Event {
|
|||
return null
|
||||
}
|
||||
|
||||
async fire({ workflowActions, applicationContext, resolveFormula }) {
|
||||
async fire({ workflowActions, applicationContext }) {
|
||||
const additionalContext = {}
|
||||
for (let i = 0; i < workflowActions.length; i += 1) {
|
||||
const workflowAction = workflowActions[i]
|
||||
|
@ -31,6 +34,33 @@ export class Event {
|
|||
'workflowAction',
|
||||
workflowAction.type
|
||||
)
|
||||
const localResolveFormula = (formula) => {
|
||||
const formulaFunctions = {
|
||||
get: (name) => {
|
||||
return this.registry.get('runtimeFormulaFunction', name)
|
||||
},
|
||||
}
|
||||
const runtimeFormulaContext = new Proxy(
|
||||
new RuntimeFormulaContext(
|
||||
this.registry.getAll('builderDataProvider'),
|
||||
{ ...applicationContext, previousActionResults: additionalContext }
|
||||
),
|
||||
{
|
||||
get(target, prop) {
|
||||
return target.get(prop)
|
||||
},
|
||||
}
|
||||
)
|
||||
try {
|
||||
return resolveFormula(
|
||||
formula,
|
||||
formulaFunctions,
|
||||
runtimeFormulaContext
|
||||
)
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
this.store.dispatch('workflowAction/setDispatching', {
|
||||
workflowAction,
|
||||
|
@ -41,8 +71,11 @@ export class Event {
|
|||
{
|
||||
workflowAction,
|
||||
additionalContext,
|
||||
applicationContext,
|
||||
resolveFormula,
|
||||
applicationContext: {
|
||||
...applicationContext,
|
||||
previousActionResults: additionalContext,
|
||||
},
|
||||
resolveFormula: localResolveFormula,
|
||||
}
|
||||
)
|
||||
} finally {
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
"pageParameter": "Parameter",
|
||||
"currentRecord": "Data source",
|
||||
"formData": "Form data",
|
||||
"previousAction": "Previous action",
|
||||
"user": "User"
|
||||
},
|
||||
"formDataProviderType": {
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
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
|
||||
},
|
||||
},
|
||||
}
|
|
@ -139,7 +139,7 @@ export default {
|
|||
return this.$store.getters['dataSource/getPageDataSources'](this.page)
|
||||
},
|
||||
dispatchContext() {
|
||||
return DataProviderType.getAllDispatchContext(
|
||||
return DataProviderType.getAllDataSourceDispatchContext(
|
||||
this.$registry.getAll('builderDataProvider'),
|
||||
this.applicationContext
|
||||
)
|
||||
|
|
|
@ -140,7 +140,7 @@ export default {
|
|||
}
|
||||
},
|
||||
dispatchContext() {
|
||||
return DataProviderType.getAllDispatchContext(
|
||||
return DataProviderType.getAllDataSourceDispatchContext(
|
||||
this.$registry.getAll('builderDataProvider'),
|
||||
this.applicationContext
|
||||
)
|
||||
|
|
|
@ -78,6 +78,7 @@ import {
|
|||
DataSourceDataProviderType,
|
||||
CurrentRecordDataProviderType,
|
||||
FormDataProviderType,
|
||||
PreviousActionDataProviderType,
|
||||
UserDataProviderType,
|
||||
} from '@baserow/modules/builder/dataProviderTypes'
|
||||
|
||||
|
@ -225,6 +226,10 @@ export default (context) => {
|
|||
'builderDataProvider',
|
||||
new FormDataProviderType(context)
|
||||
)
|
||||
app.$registry.register(
|
||||
'builderDataProvider',
|
||||
new PreviousActionDataProviderType(context)
|
||||
)
|
||||
app.$registry.register('themeConfigBlock', new MainThemeConfigBlock(context))
|
||||
|
||||
app.$registry.register(
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import WorkflowActionService from '@baserow/modules/builder/services/workflowAction'
|
||||
import PublishedBuilderService from '@baserow/modules/builder/services/publishedBuilder'
|
||||
import _ from 'lodash'
|
||||
|
||||
const updateContext = {
|
||||
updateTimeout: null,
|
||||
|
@ -195,8 +196,12 @@ const actions = {
|
|||
updateContext.promiseResolve = resolve
|
||||
})
|
||||
},
|
||||
dispatchAction({ dispatch }, { workflowActionId, data }) {
|
||||
return WorkflowActionService(this.$client).dispatch(workflowActionId, data)
|
||||
async dispatchAction({ dispatch }, { workflowActionId, data }) {
|
||||
const { data: result } = await WorkflowActionService(this.$client).dispatch(
|
||||
workflowActionId,
|
||||
data
|
||||
)
|
||||
return result
|
||||
},
|
||||
async order({ commit, getters }, { page, order, element = null }) {
|
||||
const workflowActions =
|
||||
|
@ -228,11 +233,23 @@ const getters = {
|
|||
getWorkflowActions: (state) => (page) => {
|
||||
return page.workflowActions.map((w) => w).sort((a, b) => a.order - b.order)
|
||||
},
|
||||
getWorkflowActionById: (state, getters) => (page, workflowActionId) => {
|
||||
return getters
|
||||
.getWorkflowActions(page)
|
||||
.find((workflowAction) => workflowAction.id === workflowActionId)
|
||||
},
|
||||
getElementWorkflowActions: (state) => (page, elementId) => {
|
||||
return page.workflowActions
|
||||
.filter((workflowAction) => workflowAction.element_id === elementId)
|
||||
.sort((a, b) => a.order - b.order)
|
||||
},
|
||||
getElementPreviousWorkflowActions:
|
||||
(state, getters) => (page, elementId, workflowActionId) => {
|
||||
return _.takeWhile(
|
||||
getters.getElementWorkflowActions(page, elementId),
|
||||
(workflowAction) => workflowAction.id !== workflowActionId
|
||||
)
|
||||
},
|
||||
getLoading: (state) => (workflowAction) => {
|
||||
return workflowAction._?.loading
|
||||
},
|
||||
|
|
|
@ -25,6 +25,10 @@ export class NotificationWorkflowActionType extends WorkflowActionType {
|
|||
message: ensureString(resolveFormula(description)),
|
||||
})
|
||||
}
|
||||
|
||||
getDataSchema(applicationContext, workflowAction) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export class OpenPageWorkflowActionType extends WorkflowActionType {
|
||||
|
@ -60,6 +64,10 @@ export class OpenPageWorkflowActionType extends WorkflowActionType {
|
|||
|
||||
window.location.replace(urlParsed)
|
||||
}
|
||||
|
||||
getDataSchema(applicationContext, workflowAction) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export class LogoutWorkflowActionType extends WorkflowActionType {
|
||||
|
@ -81,19 +89,26 @@ export class LogoutWorkflowActionType extends WorkflowActionType {
|
|||
}
|
||||
|
||||
export class WorkflowActionServiceType extends WorkflowActionType {
|
||||
async execute({
|
||||
workflowAction: { id },
|
||||
applicationContext,
|
||||
resolveFormula,
|
||||
}) {
|
||||
return await this.app.store.dispatch('workflowAction/dispatchAction', {
|
||||
execute({ workflowAction: { id }, applicationContext, resolveFormula }) {
|
||||
return this.app.store.dispatch('workflowAction/dispatchAction', {
|
||||
workflowActionId: id,
|
||||
data: DataProviderType.getAllDispatchContext(
|
||||
data: DataProviderType.getAllActionDispatchContext(
|
||||
this.app.$registry.getAll('builderDataProvider'),
|
||||
applicationContext
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
getDataSchema(workflowAction) {
|
||||
if (!workflowAction?.service?.schema) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
title: this.label,
|
||||
type: 'object',
|
||||
properties: workflowAction?.service?.schema?.properties,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class CreateRowWorkflowActionType extends WorkflowActionServiceType {
|
||||
|
|
|
@ -32,11 +32,19 @@ import WorkflowActionSelector from '@baserow/modules/core/components/workflowAct
|
|||
import _ from 'lodash'
|
||||
import { notifyIf } from '@baserow/modules/core/utils/error'
|
||||
import { mapActions } from 'vuex'
|
||||
import { DATA_PROVIDERS_ALLOWED_WORKFLOW_ACTIONS } from '@baserow/modules/builder/enums'
|
||||
import { fixPropertyReactivityForProvide } from '@baserow/modules/core/utils/object'
|
||||
|
||||
export default {
|
||||
name: 'WorkflowAction',
|
||||
components: { WorkflowActionSelector },
|
||||
inject: ['page'],
|
||||
inject: ['page', 'builder', 'mode'],
|
||||
provide() {
|
||||
return {
|
||||
dataProvidersAllowed: DATA_PROVIDERS_ALLOWED_WORKFLOW_ACTIONS,
|
||||
applicationContext: this.applicationContext,
|
||||
}
|
||||
},
|
||||
props: {
|
||||
availableWorkflowActionTypes: {
|
||||
type: Array,
|
||||
|
@ -47,6 +55,10 @@ export default {
|
|||
required: false,
|
||||
default: null,
|
||||
},
|
||||
element: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return { loading: false }
|
||||
|
@ -58,6 +70,17 @@ export default {
|
|||
workflowActionType.getType() === this.workflowAction.type
|
||||
)
|
||||
},
|
||||
applicationContext() {
|
||||
const context = {
|
||||
page: this.page,
|
||||
builder: this.builder,
|
||||
mode: this.mode,
|
||||
element: this.element,
|
||||
}
|
||||
return fixPropertyReactivityForProvide(context, {
|
||||
workflowAction: () => this.workflowAction,
|
||||
})
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
|
|
|
@ -45,27 +45,42 @@ export class DataProviderType extends Registerable {
|
|||
)
|
||||
}
|
||||
|
||||
static getAllDispatchContext(dataProviders, applicationContext) {
|
||||
static getAllActionDispatchContext(dataProviders, applicationContext) {
|
||||
return Object.fromEntries(
|
||||
Object.values(dataProviders).map((dataProvider) => {
|
||||
return [
|
||||
dataProvider.type,
|
||||
dataProvider.getDispatchContext(applicationContext),
|
||||
dataProvider.getActionDispatchContext(applicationContext),
|
||||
]
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
static getAllDataSourceDispatchContext(dataProviders, applicationContext) {
|
||||
return Object.fromEntries(
|
||||
Object.values(dataProviders).map((dataProvider) => {
|
||||
return [
|
||||
dataProvider.type,
|
||||
dataProvider.getDataSourceDispatchContext(applicationContext),
|
||||
]
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Should return the context needed to be send to the backend for each dataProvider
|
||||
* Should return the context needed to be sent to the backend for each dataProvider
|
||||
* to be able to solve the formulas on the backend.
|
||||
* @param {Object} applicationContext the application context.
|
||||
* @returns An object if the dataProvider wants to send something to the backend.
|
||||
*/
|
||||
getDispatchContext(applicationContext) {
|
||||
getDataSourceDispatchContext(applicationContext) {
|
||||
return null
|
||||
}
|
||||
|
||||
getActionDispatchContext(applicationContext) {
|
||||
return this.getDataSourceDispatchContext(applicationContext)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the actual data for the given path.
|
||||
* @param {object} applicationContext the application context.
|
||||
|
|
|
@ -113,3 +113,30 @@ export function getValueAtPath(obj, path) {
|
|||
const keys = typeof path === 'string' ? _.toPath(path) : path
|
||||
return _getValueAtPath(obj, keys)
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses Object.defineProperty to make Vue provide/inject reactive.
|
||||
*
|
||||
* @param staticProperties The original object
|
||||
* @param reactiveProperties An object containing the properties and values to
|
||||
* become reactive
|
||||
* @return {object} The original object with the updated properties
|
||||
* @see https://stackoverflow.com/questions/65718651/how-do-i-make-vue-2-provide-inject-api-reactive
|
||||
*
|
||||
* @example
|
||||
* const obj = { a: "A", b: "B" }
|
||||
* fixPropertyReactivityForProvide(obj, { c: () => "C" }
|
||||
* console.log(obj.c) // "c" property is now reactive and will return "C"
|
||||
*/
|
||||
export function fixPropertyReactivityForProvide(
|
||||
staticProperties,
|
||||
reactiveProperties
|
||||
) {
|
||||
Object.entries(reactiveProperties).forEach(([propertyName, getValue]) => {
|
||||
Object.defineProperty(staticProperties, propertyName, {
|
||||
enumerable: true,
|
||||
get: () => getValue(),
|
||||
})
|
||||
})
|
||||
return staticProperties
|
||||
}
|
||||
|
|
|
@ -19,4 +19,11 @@ export class WorkflowActionType extends Registerable {
|
|||
async execute(context) {
|
||||
return await Promise.resolve()
|
||||
}
|
||||
|
||||
/**
|
||||
* Should return a JSON schema of the data returned by this workflow action.
|
||||
*/
|
||||
getDataSchema(applicationContext, workflowAction) {
|
||||
throw new Error('Must be set on the type.')
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue