1
0
Fork 0
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:
Afonso Silva 2024-04-03 13:40:20 +00:00
parent a602c3b9e4
commit 67f77c8515
29 changed files with 476 additions and 67 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -130,7 +130,7 @@ export default {
return this.$registry.getAll('collectionField')
},
dispatchContext() {
return DataProviderType.getAllDispatchContext(
return DataProviderType.getAllDataSourceDispatchContext(
this.$registry.getAll('builderDataProvider'),
this.applicationContext
)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,6 +4,7 @@
"pageParameter": "Parameter",
"currentRecord": "Data source",
"formData": "Form data",
"previousAction": "Previous action",
"user": "User"
},
"formDataProviderType": {

View file

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

View file

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

View file

@ -140,7 +140,7 @@ export default {
}
},
dispatchContext() {
return DataProviderType.getAllDispatchContext(
return DataProviderType.getAllDataSourceDispatchContext(
this.$registry.getAll('builderDataProvider'),
this.applicationContext
)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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.')
}
}