1
0
Fork 0
mirror of synced 2024-06-10 14:35:21 +12:00
budibase/packages/client/src/utils/buttonActions.js

435 lines
12 KiB
JavaScript

import { get } from "svelte/store"
import download from "downloadjs"
import {
routeStore,
builderStore,
confirmationStore,
authStore,
stateStore,
notificationStore,
dataSourceStore,
uploadStore,
rowSelectionStore,
} from "stores"
import { API } from "api"
import { ActionTypes } from "constants"
import { enrichDataBindings } from "./enrichDataBinding"
import { Helpers } from "@budibase/bbui"
const saveRowHandler = async (action, context) => {
const { fields, providerId, tableId } = action.parameters
let payload
if (providerId) {
payload = { ...context[providerId] }
} else {
payload = {}
}
if (fields) {
for (let [field, value] of Object.entries(fields)) {
Helpers.deepSet(payload, field, value)
}
}
if (tableId) {
payload.tableId = tableId
}
try {
const row = await API.saveRow(payload)
notificationStore.actions.success("Row saved")
// Refresh related datasources
await dataSourceStore.actions.invalidateDataSource(row.tableId, {
invalidateRelationships: true,
})
return { row }
} catch (error) {
// Abort next actions
return false
}
}
const duplicateRowHandler = async (action, context) => {
const { fields, providerId, tableId } = action.parameters
if (providerId) {
let payload = { ...context[providerId] }
if (fields) {
for (let [field, value] of Object.entries(fields)) {
Helpers.deepSet(payload, field, value)
}
}
if (tableId) {
payload.tableId = tableId
}
delete payload._id
delete payload._rev
try {
const row = await API.saveRow(payload)
notificationStore.actions.success("Row saved")
// Refresh related datasources
await dataSourceStore.actions.invalidateDataSource(row.tableId, {
invalidateRelationships: true,
})
return { row }
} catch (error) {
// Abort next actions
return false
}
}
}
const deleteRowHandler = async action => {
const { tableId, revId, rowId } = action.parameters
if (tableId && rowId) {
try {
await API.deleteRow({ tableId, rowId, revId })
notificationStore.actions.success("Row deleted")
// Refresh related datasources
await dataSourceStore.actions.invalidateDataSource(tableId, {
invalidateRelationships: true,
})
} catch (error) {
// Abort next actions
return false
}
}
}
const triggerAutomationHandler = async action => {
const { fields } = action.parameters
if (fields) {
try {
await API.triggerAutomation({
automationId: action.parameters.automationId,
fields,
})
notificationStore.actions.success("Automation triggered")
} catch (error) {
// Abort next actions
return false
}
}
}
const navigationHandler = action => {
const { url, peek } = action.parameters
routeStore.actions.navigate(url, peek)
}
const queryExecutionHandler = async action => {
const { datasourceId, queryId, queryParams } = action.parameters
try {
const query = await API.fetchQueryDefinition(queryId)
if (query?.datasourceId == null) {
notificationStore.actions.error("That query couldn't be found")
return false
}
const result = await API.executeQuery({
datasourceId,
queryId,
parameters: queryParams,
})
// Trigger a notification and invalidate the datasource as long as this
// was not a readable query
if (!query.readable) {
notificationStore.actions.success("Query executed successfully")
await dataSourceStore.actions.invalidateDataSource(query.datasourceId)
}
return { result }
} catch (error) {
notificationStore.actions.error(
"An error occurred while executing the query"
)
// Abort next actions
return false
}
}
const executeActionHandler = async (
context,
componentId,
actionType,
params
) => {
const fn = context[`${componentId}_${actionType}`]
if (fn) {
return await fn(params)
}
}
const updateFieldValueHandler = async (action, context) => {
return await executeActionHandler(
context,
action.parameters.componentId,
ActionTypes.UpdateFieldValue,
{
type: action.parameters.type,
field: action.parameters.field,
value: action.parameters.value,
}
)
}
const validateFormHandler = async (action, context) => {
return await executeActionHandler(
context,
action.parameters.componentId,
ActionTypes.ValidateForm
)
}
const refreshDataProviderHandler = async (action, context) => {
return await executeActionHandler(
context,
action.parameters.componentId,
ActionTypes.RefreshDatasource
)
}
const logoutHandler = async action => {
await authStore.actions.logOut()
let redirectUrl = "/builder/auth/login"
let internal = false
if (action.parameters.redirectUrl) {
internal = action.parameters.redirectUrl?.startsWith("/")
redirectUrl = routeStore.actions.createFullURL(
action.parameters.redirectUrl
)
}
window.location.href = redirectUrl
if (internal) {
window.location.reload()
}
}
const clearFormHandler = async (action, context) => {
return await executeActionHandler(
context,
action.parameters.componentId,
ActionTypes.ClearForm
)
}
const changeFormStepHandler = async (action, context) => {
return await executeActionHandler(
context,
action.parameters.componentId,
ActionTypes.ChangeFormStep,
action.parameters
)
}
const closeScreenModalHandler = () => {
// Emit this as a window event, so parent screens which are iframing us in
// can close the modal
window.parent.postMessage({ type: "close-screen-modal" })
}
const updateStateHandler = action => {
const { type, key, value, persist } = action.parameters
if (type === "set") {
stateStore.actions.setValue(key, value, persist)
} else if (type === "delete") {
stateStore.actions.deleteValue(key)
}
// Emit this as an event so that parent windows which are iframing us in
// can also update their state
if (get(routeStore).queryParams?.peek) {
window.parent.postMessage({
type: "update-state",
detail: { type, key, value, persist },
})
}
}
const s3UploadHandler = async action => {
const { componentId } = action.parameters
if (!componentId) {
return
}
const res = await uploadStore.actions.processFileUpload(componentId)
return {
publicUrl: res?.publicUrl,
}
}
const exportDataHandler = async action => {
let selection = rowSelectionStore.actions.getSelection(
action.parameters.tableComponentId
)
if (selection.selectedRows && selection.selectedRows.length > 0) {
try {
const data = await API.exportRows({
tableId: selection.tableId,
rows: selection.selectedRows,
format: action.parameters.type,
columns: action.parameters.columns,
})
download(data, `${selection.tableId}.${action.parameters.type}`)
} catch (error) {
notificationStore.actions.error("There was an error exporting the data")
}
} else {
notificationStore.actions.error("Please select at least one row")
}
}
const continueIfHandler = action => {
const { type, value, operator, referenceValue } = action.parameters
if (!type || !operator) {
return
}
let match = false
if (value == null && referenceValue == null) {
match = true
} else if (value === referenceValue) {
match = true
} else {
match = JSON.stringify(value) === JSON.stringify(referenceValue)
}
if (type === "continue") {
return operator === "equal" ? match : !match
} else {
return operator === "equal" ? !match : match
}
}
const showNotificationHandler = action => {
const { message, type, autoDismiss } = action.parameters
if (!message || !type) {
return
}
notificationStore.actions[type]?.(message, autoDismiss)
}
const handlerMap = {
["Save Row"]: saveRowHandler,
["Duplicate Row"]: duplicateRowHandler,
["Delete Row"]: deleteRowHandler,
["Navigate To"]: navigationHandler,
["Execute Query"]: queryExecutionHandler,
["Trigger Automation"]: triggerAutomationHandler,
["Validate Form"]: validateFormHandler,
["Update Field Value"]: updateFieldValueHandler,
["Refresh Data Provider"]: refreshDataProviderHandler,
["Log Out"]: logoutHandler,
["Clear Form"]: clearFormHandler,
["Close Screen Modal"]: closeScreenModalHandler,
["Change Form Step"]: changeFormStepHandler,
["Update State"]: updateStateHandler,
["Upload File to S3"]: s3UploadHandler,
["Export Data"]: exportDataHandler,
["Continue if / Stop if"]: continueIfHandler,
["Show Notification"]: showNotificationHandler,
}
const confirmTextMap = {
["Delete Row"]: "Are you sure you want to delete this row?",
["Save Row"]: "Are you sure you want to save this row?",
["Execute Query"]: "Are you sure you want to execute this query?",
["Trigger Automation"]: "Are you sure you want to trigger this automation?",
}
/**
* Parses an array of actions and returns a function which will execute the
* actions in the current context.
* A handler returning `false` is a flag to stop execution of handlers
*/
export const enrichButtonActions = (actions, context) => {
// Prevent button actions in the builder preview
if (!actions?.length || get(builderStore).inBuilder) {
return null
}
// If this is a function then it has already been enriched
if (typeof actions === "function") {
return actions
}
const handlers = actions.map(def => handlerMap[def["##eventHandlerType"]])
return async eventContext => {
// Button context is built up as actions are executed.
// Inherit any previous button context which may have come from actions
// before a confirmable action since this breaks the chain.
let buttonContext = context.actions || []
for (let i = 0; i < handlers.length; i++) {
try {
// Skip any non-existent action definitions
if (!handlers[i]) {
continue
}
// Built total context for this action
const totalContext = {
...context,
state: get(stateStore),
actions: buttonContext,
eventContext,
}
// Get and enrich this button action with the total context
let action = actions[i]
action = enrichDataBindings(action, totalContext)
const callback = async () => handlers[i](action, totalContext)
// If this action is confirmable, show confirmation and await a
// callback to execute further actions
if (action.parameters?.confirm) {
return new Promise(resolve => {
const defaultText = confirmTextMap[action["##eventHandlerType"]]
const confirmText = action.parameters?.confirmText || defaultText
confirmationStore.actions.showConfirmation(
action["##eventHandlerType"],
confirmText,
async () => {
// When confirmed, execute this action immediately,
// then execute the rest of the actions in the chain
const result = await callback()
if (result !== false) {
// Generate a new total context to pass into the next enrichment
buttonContext.push(result)
const newContext = { ...context, actions: buttonContext }
// Enrich and call the next button action
const next = enrichButtonActions(
actions.slice(i + 1),
newContext
)
resolve(await next())
} else {
resolve(false)
}
},
() => {
resolve(false)
}
)
})
}
// For non-confirmable actions, execute the handler immediately
else {
const result = await callback()
if (result === false) {
return
} else {
buttonContext.push(result)
}
}
} catch (error) {
console.error("Error while executing button handler")
console.error(error)
// Stop executing further actions on error
return
}
}
}
}