1254 lines
38 KiB
Svelte
1254 lines
38 KiB
Svelte
<script>
|
|
import TableSelector from "./TableSelector.svelte"
|
|
import FieldSelector from "./FieldSelector.svelte"
|
|
import SchemaSetup from "./SchemaSetup.svelte"
|
|
import RowSelector from "./RowSelector.svelte"
|
|
import {
|
|
Button,
|
|
Select,
|
|
Label,
|
|
ActionButton,
|
|
Drawer,
|
|
Modal,
|
|
notifications,
|
|
Checkbox,
|
|
DatePicker,
|
|
DrawerContent,
|
|
Helpers,
|
|
Toggle,
|
|
Divider,
|
|
Icon,
|
|
} from "@budibase/bbui"
|
|
|
|
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
|
import { automationStore, selectedAutomation, tables } from "stores/builder"
|
|
import { environment, licensing } from "stores/portal"
|
|
import WebhookDisplay from "../Shared/WebhookDisplay.svelte"
|
|
import {
|
|
BindingSidePanel,
|
|
DrawerBindableSlot,
|
|
DrawerBindableInput,
|
|
ServerBindingPanel as AutomationBindingPanel,
|
|
ModalBindableInput,
|
|
} from "components/common/bindings"
|
|
import CodeEditorModal from "./CodeEditorModal.svelte"
|
|
import QueryParamSelector from "./QueryParamSelector.svelte"
|
|
import AutomationSelector from "./AutomationSelector.svelte"
|
|
import CronBuilder from "./CronBuilder.svelte"
|
|
import Editor from "components/integration/QueryEditor.svelte"
|
|
import CodeEditor from "components/common/CodeEditor/CodeEditor.svelte"
|
|
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
|
|
import { BindingHelpers, BindingType } from "components/common/bindings/utils"
|
|
import {
|
|
bindingsToCompletions,
|
|
hbAutocomplete,
|
|
EditorModes,
|
|
} from "components/common/CodeEditor"
|
|
import FilterBuilder from "components/design/settings/controls/FilterEditor/FilterBuilder.svelte"
|
|
import { QueryUtils, Utils, search, memo } from "@budibase/frontend-core"
|
|
import {
|
|
getSchemaForDatasourcePlus,
|
|
getEnvironmentBindings,
|
|
} from "dataBinding"
|
|
import { TriggerStepID, ActionStepID } from "constants/backend/automations"
|
|
import { onMount } from "svelte"
|
|
import { writable } from "svelte/store"
|
|
import { cloneDeep } from "lodash/fp"
|
|
import {
|
|
AutomationEventType,
|
|
AutomationStepType,
|
|
AutomationActionStepId,
|
|
AutomationCustomIOType,
|
|
} from "@budibase/types"
|
|
import { FIELDS } from "constants/backend"
|
|
import PropField from "./PropField.svelte"
|
|
import { utils } from "@budibase/shared-core"
|
|
|
|
export let block
|
|
export let testData
|
|
export let schemaProperties
|
|
export let isTestModal = false
|
|
|
|
// Stop unnecessary rendering
|
|
const memoBlock = memo(block)
|
|
const memoEnvVariables = memo($environment.variables)
|
|
|
|
const rowTriggers = [
|
|
TriggerStepID.ROW_UPDATED,
|
|
TriggerStepID.ROW_SAVED,
|
|
TriggerStepID.ROW_DELETED,
|
|
TriggerStepID.ROW_ACTION,
|
|
]
|
|
|
|
const rowEvents = [
|
|
AutomationEventType.ROW_DELETE,
|
|
AutomationEventType.ROW_SAVE,
|
|
AutomationEventType.ROW_UPDATE,
|
|
]
|
|
|
|
const rowSteps = [ActionStepID.UPDATE_ROW, ActionStepID.CREATE_ROW]
|
|
|
|
let webhookModal
|
|
let drawer
|
|
let inputData
|
|
let insertAtPos, getCaretPosition
|
|
let stepLayouts = {}
|
|
|
|
$: memoEnvVariables.set($environment.variables)
|
|
$: memoBlock.set(block)
|
|
|
|
$: filters = lookForFilters(schemaProperties)
|
|
$: filterCount =
|
|
filters?.groups?.reduce((acc, group) => {
|
|
acc = acc += group?.filters?.length || 0
|
|
return acc
|
|
}, 0) || 0
|
|
|
|
$: tempFilters = cloneDeep(filters)
|
|
$: stepId = $memoBlock.stepId
|
|
|
|
$: automationBindings = getAvailableBindings(
|
|
$memoBlock,
|
|
$selectedAutomation?.definition
|
|
)
|
|
$: environmentBindings = buildEnvironmentBindings($memoEnvVariables)
|
|
$: bindings = [...automationBindings, ...environmentBindings]
|
|
|
|
$: getInputData(testData, $memoBlock.inputs)
|
|
$: tableId = inputData ? inputData.tableId : null
|
|
$: table = tableId
|
|
? $tables.list.find(table => table._id === inputData.tableId)
|
|
: { schema: {} }
|
|
$: schema = getSchemaForDatasourcePlus(tableId, {
|
|
searchableSchema: true,
|
|
}).schema
|
|
$: schemaFields = search.getFields(
|
|
$tables.list,
|
|
Object.values(schema || {}),
|
|
{ allowLinks: true }
|
|
)
|
|
$: queryLimit = tableId?.includes("datasource") ? "∞" : "1000"
|
|
$: isTrigger = $memoBlock?.type === AutomationStepType.TRIGGER
|
|
$: codeMode =
|
|
stepId === AutomationActionStepId.EXECUTE_BASH
|
|
? EditorModes.Handlebars
|
|
: EditorModes.JS
|
|
$: bindingsHelpers = new BindingHelpers(getCaretPosition, insertAtPos, {
|
|
disableWrapping: true,
|
|
})
|
|
$: editingJs = codeMode === EditorModes.JS
|
|
$: requiredProperties = isTestModal
|
|
? []
|
|
: $memoBlock.schema["inputs"].required
|
|
|
|
$: stepCompletions =
|
|
codeMode === EditorModes.Handlebars
|
|
? [hbAutocomplete([...bindingsToCompletions(bindings, codeMode)])]
|
|
: []
|
|
|
|
const buildEnvironmentBindings = () => {
|
|
if ($licensing.environmentVariablesEnabled) {
|
|
return getEnvironmentBindings().map(binding => {
|
|
return {
|
|
...binding,
|
|
display: {
|
|
...binding.display,
|
|
rank: 98,
|
|
},
|
|
}
|
|
})
|
|
}
|
|
return []
|
|
}
|
|
|
|
const getInputData = (testData, blockInputs) => {
|
|
// Test data is not cloned for reactivity
|
|
let newInputData = testData || cloneDeep(blockInputs)
|
|
|
|
// Ensures the app action fields are populated
|
|
if (
|
|
block.event === AutomationEventType.APP_TRIGGER &&
|
|
!newInputData?.fields
|
|
) {
|
|
newInputData = cloneDeep(blockInputs)
|
|
}
|
|
inputData = newInputData
|
|
setDefaultEnumValues()
|
|
}
|
|
|
|
const setDefaultEnumValues = () => {
|
|
for (const [key, value] of schemaProperties) {
|
|
if (value.type === "string" && value.enum && inputData[key] == null) {
|
|
inputData[key] = value.enum[0]
|
|
}
|
|
}
|
|
}
|
|
|
|
// Store for any UX related data
|
|
const stepStore = writable({})
|
|
$: stepState = $stepStore?.[block.id]
|
|
|
|
$: customStepLayouts($memoBlock, schemaProperties, stepState)
|
|
|
|
const customStepLayouts = block => {
|
|
if (
|
|
rowSteps.includes(block.stepId) ||
|
|
(rowTriggers.includes(block.stepId) && isTestModal)
|
|
) {
|
|
const schema = schemaProperties.reduce((acc, entry) => {
|
|
const [key, val] = entry
|
|
acc[key] = val
|
|
return acc
|
|
}, {})
|
|
|
|
// Optionally build the rev field config when its needed.
|
|
const getRevConfig = () => {
|
|
const rowRevEntry = schema["revision"]
|
|
if (!rowRevEntry) {
|
|
return []
|
|
}
|
|
const rowRevlabel = getFieldLabel("revision", rowRevEntry)
|
|
|
|
return isTestModal
|
|
? [
|
|
{
|
|
type: DrawerBindableInput,
|
|
title: rowRevlabel,
|
|
props: {
|
|
panel: AutomationBindingPanel,
|
|
value: inputData["revision"],
|
|
onChange: e => {
|
|
onChange({ ["revision"]: e.detail })
|
|
},
|
|
updateOnChange: false,
|
|
forceModal: true,
|
|
},
|
|
},
|
|
]
|
|
: []
|
|
}
|
|
|
|
const getIdConfig = () => {
|
|
const rowIdentifier = isTestModal ? "id" : "rowId"
|
|
|
|
const rowIdEntry = schema[rowIdentifier]
|
|
if (!rowIdEntry) {
|
|
return []
|
|
}
|
|
|
|
const rowIdlabel = getFieldLabel(rowIdentifier, rowIdEntry)
|
|
|
|
return [
|
|
{
|
|
type: DrawerBindableInput,
|
|
title: rowIdlabel,
|
|
props: {
|
|
panel: AutomationBindingPanel,
|
|
value: inputData[rowIdentifier],
|
|
onChange: e => {
|
|
onChange({ [rowIdentifier]: e.detail })
|
|
},
|
|
updateOnChange: false,
|
|
forceModal: true,
|
|
},
|
|
},
|
|
]
|
|
}
|
|
|
|
// A select to switch from `row` to `oldRow`
|
|
const getRowTypeConfig = () => {
|
|
if (!isTestModal || block.event !== AutomationEventType.ROW_UPDATE) {
|
|
return []
|
|
}
|
|
|
|
if (!$stepStore?.[block.id]) {
|
|
stepStore.update(state => ({
|
|
...state,
|
|
[block.id]: {
|
|
rowType: "row",
|
|
},
|
|
}))
|
|
}
|
|
|
|
return [
|
|
{
|
|
type: Select,
|
|
tooltip: `You can configure test data for both the updated row and
|
|
the old row, if you need it. Just select the one you wish to alter`,
|
|
title: "Row data",
|
|
props: {
|
|
value: $stepStore?.[block.id].rowType,
|
|
onChange: e => {
|
|
stepStore.update(state => ({
|
|
...state,
|
|
[block.id]: {
|
|
rowType: e.detail,
|
|
},
|
|
}))
|
|
},
|
|
placeholder: false,
|
|
getOptionLabel: type => type.name,
|
|
getOptionValue: type => type.id,
|
|
options: [
|
|
{
|
|
id: "row",
|
|
name: "Updated row",
|
|
},
|
|
{ id: "oldRow", name: "Old row" },
|
|
],
|
|
},
|
|
},
|
|
]
|
|
}
|
|
|
|
const getRowSelector = () => {
|
|
const baseProps = {
|
|
bindings,
|
|
isTestModal,
|
|
isUpdateRow: block.stepId === ActionStepID.UPDATE_ROW,
|
|
}
|
|
|
|
if (isTestModal && stepState?.rowType === "oldRow") {
|
|
return [
|
|
{
|
|
type: RowSelector,
|
|
props: {
|
|
row: inputData["oldRow"] || {
|
|
tableId: inputData["row"].tableId,
|
|
},
|
|
meta: {
|
|
fields: inputData["meta"]?.oldFields || {},
|
|
},
|
|
onChange: e => {
|
|
onChange({
|
|
oldRow: e.detail.row,
|
|
meta: {
|
|
fields: inputData["meta"]?.fields || {},
|
|
oldFields: e.detail?.meta?.fields || {},
|
|
},
|
|
})
|
|
},
|
|
...baseProps,
|
|
},
|
|
},
|
|
]
|
|
}
|
|
|
|
return [
|
|
{
|
|
type: RowSelector,
|
|
props: {
|
|
row: inputData["row"],
|
|
meta: inputData["meta"] || {},
|
|
onChange: e => {
|
|
onChange({
|
|
row: e.detail.row,
|
|
meta: {
|
|
fields: e.detail?.meta?.fields || {},
|
|
...(isTestModal
|
|
? { oldFields: inputData["meta"]?.oldFields || {} }
|
|
: {}),
|
|
},
|
|
})
|
|
},
|
|
...baseProps,
|
|
},
|
|
},
|
|
]
|
|
}
|
|
|
|
stepLayouts[block.stepId] = {
|
|
row: {
|
|
schema: schema["row"],
|
|
//?layout: RowLayoutStepComponent.
|
|
content: [
|
|
{
|
|
type: TableSelector,
|
|
title: "Table",
|
|
props: {
|
|
isTrigger,
|
|
value: inputData["row"]?.tableId ?? "",
|
|
onChange: e => {
|
|
const rowKey = $stepStore?.[block.id]?.rowType || "row"
|
|
onChange({
|
|
_tableId: e.detail,
|
|
meta: {},
|
|
[rowKey]: e.detail
|
|
? {
|
|
tableId: e.detail,
|
|
}
|
|
: {},
|
|
})
|
|
},
|
|
disabled: isTestModal,
|
|
},
|
|
},
|
|
...getIdConfig(),
|
|
...getRevConfig(),
|
|
...getRowTypeConfig(),
|
|
{
|
|
type: Divider,
|
|
props: {
|
|
noMargin: true,
|
|
},
|
|
},
|
|
...getRowSelector(),
|
|
],
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handler for row trigger automation updates.
|
|
* @param {object} update - An automation block.inputs update object
|
|
* @param {string} [update.tableId] - The ID of the table
|
|
* @param {object} [update.filters] - Filter configuration for the row trigger
|
|
* @param {object} [update.filters-def] - Filter definitions for the row trigger
|
|
* @example
|
|
* // Example with tableId
|
|
* onRowTriggerUpdate({
|
|
* "tableId" : "ta_bb_employee"
|
|
* })
|
|
* @example
|
|
* // Example with filters
|
|
* onRowTriggerUpdate({
|
|
* filters: {
|
|
* equal: { "1:Approved": "true" }
|
|
* },
|
|
* "filters-def": [{
|
|
* id: "oH1T4S49n",
|
|
* field: "1:Approved",
|
|
* operator: "equal",
|
|
* value: "true",
|
|
* valueType: "Value",
|
|
* type: "string"
|
|
* }]
|
|
* })
|
|
*/
|
|
const onRowTriggerUpdate = async update => {
|
|
if (
|
|
["tableId", AutomationCustomIOType.FILTERS, "meta"].some(key =>
|
|
Object.hasOwn(update, key)
|
|
)
|
|
) {
|
|
try {
|
|
let updatedAutomation
|
|
|
|
if (
|
|
Object.hasOwn(update, "tableId") &&
|
|
$selectedAutomation.testData?.row?.tableId !== update.tableId
|
|
) {
|
|
const reqSchema = getSchemaForDatasourcePlus(update.tableId, {
|
|
searchableSchema: true,
|
|
}).schema
|
|
|
|
updatedAutomation = await automationStore.actions.processBlockInputs(
|
|
block,
|
|
{
|
|
schema: reqSchema,
|
|
...update,
|
|
}
|
|
)
|
|
|
|
// Reset testData when tableId changes
|
|
updatedAutomation = {
|
|
...updatedAutomation,
|
|
testData: {
|
|
row: { tableId: update.tableId },
|
|
oldRow: { tableId: update.tableId },
|
|
meta: {},
|
|
id: "",
|
|
revision: "",
|
|
},
|
|
}
|
|
} else {
|
|
// For filters update, just process block inputs without resetting testData
|
|
updatedAutomation = await automationStore.actions.processBlockInputs(
|
|
block,
|
|
update
|
|
)
|
|
}
|
|
|
|
await automationStore.actions.save(updatedAutomation)
|
|
|
|
return
|
|
} catch (e) {
|
|
console.error("Error saving automation", e)
|
|
notifications.error("Error saving automation")
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Handler for App trigger automation updates.
|
|
* Ensure updates to the field list are reflected in testData
|
|
@param {object} update - An app trigger update object
|
|
@example
|
|
onAppTriggerUpdate({
|
|
"fields" : {"myField": "123", "myArray": "cat,dog,badger"}
|
|
})
|
|
*/
|
|
const onAppTriggerUpdate = async update => {
|
|
try {
|
|
// Parse the block inputs as usual
|
|
const updatedAutomation =
|
|
await automationStore.actions.processBlockInputs(block, {
|
|
schema: {},
|
|
...update,
|
|
})
|
|
|
|
// Exclude default or invalid data from the test data
|
|
let updatedFields = {}
|
|
for (const key of Object.keys(block?.inputs?.fields || {})) {
|
|
if (Object.hasOwn(update.fields, key)) {
|
|
if (key !== "") {
|
|
updatedFields[key] = updatedAutomation.testData?.fields?.[key]
|
|
}
|
|
}
|
|
}
|
|
|
|
// Save the entire automation and reset the testData
|
|
await automationStore.actions.save({
|
|
...updatedAutomation,
|
|
testData: {
|
|
fields: updatedFields,
|
|
},
|
|
})
|
|
} catch (e) {
|
|
console.error("Error saving automation", e)
|
|
notifications.error("Error saving automation")
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handler for automation block input updates.
|
|
@param {object} update - An automation inputs update object
|
|
@example
|
|
onChange({
|
|
meta: { fields : { "Photo": { useAttachmentBinding: false }} }
|
|
row: { "Active": true, "Order Id" : 14, ... }
|
|
})
|
|
*/
|
|
const onChange = Utils.sequential(async update => {
|
|
const request = cloneDeep(update)
|
|
// Process app trigger updates
|
|
if (isTrigger && !isTestModal) {
|
|
// Row trigger
|
|
if (rowEvents.includes(block.event)) {
|
|
await onRowTriggerUpdate(request)
|
|
return
|
|
}
|
|
// App trigger
|
|
if (block.event === AutomationEventType.APP_TRIGGER) {
|
|
await onAppTriggerUpdate(request)
|
|
return
|
|
}
|
|
}
|
|
|
|
// We need to cache the schema as part of the definition because it is
|
|
// used in the server to detect relationships. It would be far better to
|
|
// instead fetch the schema in the backend at runtime.
|
|
// If _tableId is explicitly included in the update request, the schema will be requested
|
|
let schema
|
|
if (request?._tableId) {
|
|
schema = getSchemaForDatasourcePlus(request._tableId, {
|
|
searchableSchema: true,
|
|
}).schema
|
|
delete request._tableId
|
|
}
|
|
try {
|
|
if (isTestModal) {
|
|
let newTestData = { schema }
|
|
|
|
// Special case for webhook, as it requires a body, but the schema already brings back the body's contents
|
|
if (stepId === TriggerStepID.WEBHOOK) {
|
|
newTestData = {
|
|
...newTestData,
|
|
body: {
|
|
...update,
|
|
...$selectedAutomation.testData?.body,
|
|
},
|
|
}
|
|
}
|
|
newTestData = {
|
|
...newTestData,
|
|
...request,
|
|
}
|
|
await automationStore.actions.addTestDataToAutomation(newTestData)
|
|
} else {
|
|
const data = { schema, ...request }
|
|
await automationStore.actions.updateBlockInputs(block, data)
|
|
}
|
|
} catch (error) {
|
|
console.error("Error saving automation", error)
|
|
notifications.error("Error saving automation")
|
|
}
|
|
})
|
|
|
|
function getAvailableBindings(block, automation) {
|
|
if (!block || !automation) {
|
|
return []
|
|
}
|
|
|
|
// Find previous steps to the selected one
|
|
let allSteps = [...automation.steps]
|
|
|
|
if (automation.trigger) {
|
|
allSteps = [automation.trigger, ...allSteps]
|
|
}
|
|
let blockIdx = allSteps.findIndex(step => step.id === block.id)
|
|
|
|
// Extract all outputs from all previous steps as available bindingsx§x
|
|
let bindings = []
|
|
let loopBlockCount = 0
|
|
const addBinding = (name, value, icon, idx, isLoopBlock, bindingName) => {
|
|
if (!name) return
|
|
const runtimeBinding = determineRuntimeBinding(
|
|
name,
|
|
idx,
|
|
isLoopBlock,
|
|
bindingName
|
|
)
|
|
const categoryName = determineCategoryName(idx, isLoopBlock, bindingName)
|
|
bindings.push(
|
|
createBindingObject(
|
|
name,
|
|
value,
|
|
icon,
|
|
idx,
|
|
loopBlockCount,
|
|
isLoopBlock,
|
|
runtimeBinding,
|
|
categoryName,
|
|
bindingName
|
|
)
|
|
)
|
|
}
|
|
|
|
const determineRuntimeBinding = (name, idx, isLoopBlock, bindingName) => {
|
|
let runtimeName
|
|
|
|
/* Begin special cases for generating custom schemas based on triggers */
|
|
if (
|
|
idx === 0 &&
|
|
automation.trigger?.event === AutomationEventType.APP_TRIGGER
|
|
) {
|
|
return `trigger.fields.${name}`
|
|
}
|
|
|
|
if (
|
|
idx === 0 &&
|
|
(automation.trigger?.event === AutomationEventType.ROW_UPDATE ||
|
|
automation.trigger?.event === AutomationEventType.ROW_SAVE)
|
|
) {
|
|
let noRowKeywordBindings = ["id", "revision", "oldRow"]
|
|
if (!noRowKeywordBindings.includes(name)) return `trigger.row.${name}`
|
|
}
|
|
/* End special cases for generating custom schemas based on triggers */
|
|
|
|
let hasUserDefinedName = automation.stepNames?.[allSteps[idx]?.id]
|
|
if (isLoopBlock) {
|
|
runtimeName = `loop.${name}`
|
|
} else if (idx === 0) {
|
|
runtimeName = `trigger.${name}`
|
|
} else if (block.name.startsWith("JS")) {
|
|
runtimeName = hasUserDefinedName
|
|
? `stepsByName["${bindingName}"].${name}`
|
|
: `steps["${idx - loopBlockCount}"].${name}`
|
|
} else {
|
|
runtimeName = hasUserDefinedName
|
|
? `stepsByName.${bindingName}.${name}`
|
|
: `steps.${idx - loopBlockCount}.${name}`
|
|
}
|
|
return runtimeName
|
|
}
|
|
|
|
const determineCategoryName = (idx, isLoopBlock, bindingName) => {
|
|
if (idx === 0) return "Trigger outputs"
|
|
if (isLoopBlock) return "Loop Outputs"
|
|
return bindingName
|
|
? `${bindingName} outputs`
|
|
: `Step ${idx - loopBlockCount} outputs`
|
|
}
|
|
|
|
const createBindingObject = (
|
|
name,
|
|
value,
|
|
icon,
|
|
idx,
|
|
loopBlockCount,
|
|
isLoopBlock,
|
|
runtimeBinding,
|
|
categoryName,
|
|
bindingName
|
|
) => {
|
|
const field = Object.values(FIELDS).find(
|
|
field => field.type === value.type && field.subtype === value.subtype
|
|
)
|
|
return {
|
|
readableBinding:
|
|
bindingName && !isLoopBlock && idx !== 0
|
|
? `steps.${bindingName}.${name}`
|
|
: runtimeBinding,
|
|
runtimeBinding,
|
|
type: value.type,
|
|
description: value.description,
|
|
icon,
|
|
category: categoryName,
|
|
display: {
|
|
type: field?.name || value.type,
|
|
name,
|
|
rank: isLoopBlock ? idx + 1 : idx - loopBlockCount,
|
|
},
|
|
}
|
|
}
|
|
|
|
for (let idx = 0; idx < blockIdx; idx++) {
|
|
let wasLoopBlock = allSteps[idx - 1]?.stepId === ActionStepID.LOOP
|
|
let isLoopBlock =
|
|
allSteps[idx]?.stepId === ActionStepID.LOOP &&
|
|
allSteps.some(x => x.blockToLoop === block.id)
|
|
let schema = cloneDeep(allSteps[idx]?.schema?.outputs?.properties) ?? {}
|
|
if (allSteps[idx]?.name.includes("Looping")) {
|
|
isLoopBlock = true
|
|
loopBlockCount++
|
|
}
|
|
let bindingName =
|
|
automation.stepNames?.[allSteps[idx].id] || allSteps[idx].name
|
|
|
|
if (isLoopBlock) {
|
|
schema = {
|
|
currentItem: {
|
|
type: "string",
|
|
description: "the item currently being executed",
|
|
},
|
|
}
|
|
}
|
|
|
|
if (
|
|
idx === 0 &&
|
|
automation.trigger?.event === AutomationEventType.APP_TRIGGER
|
|
) {
|
|
schema = Object.fromEntries(
|
|
Object.keys(automation.trigger.inputs.fields || []).map(key => [
|
|
key,
|
|
{ type: automation.trigger.inputs.fields[key] },
|
|
])
|
|
)
|
|
}
|
|
if (
|
|
(idx === 0 &&
|
|
automation.trigger.event === AutomationEventType.ROW_UPDATE) ||
|
|
(idx === 0 && automation.trigger.event === AutomationEventType.ROW_SAVE)
|
|
) {
|
|
let table = $tables.list.find(
|
|
table => table._id === automation.trigger.inputs.tableId
|
|
)
|
|
// We want to generate our own schema for the bindings from the table schema itself
|
|
for (const key in table?.schema) {
|
|
schema[key] = {
|
|
type: table.schema[key].type,
|
|
subtype: table.schema[key].subtype,
|
|
}
|
|
}
|
|
// remove the original binding
|
|
delete schema.row
|
|
}
|
|
let icon =
|
|
idx === 0
|
|
? automation.trigger.icon
|
|
: isLoopBlock
|
|
? "Reuse"
|
|
: allSteps[idx].icon
|
|
|
|
if (wasLoopBlock) {
|
|
schema = cloneDeep(allSteps[idx - 1]?.schema?.outputs?.properties)
|
|
}
|
|
Object.entries(schema).forEach(([name, value]) => {
|
|
addBinding(name, value, icon, idx, isLoopBlock, bindingName)
|
|
})
|
|
}
|
|
|
|
if (
|
|
allSteps[blockIdx - 1]?.stepId !== ActionStepID.LOOP &&
|
|
allSteps
|
|
.slice(0, blockIdx)
|
|
.some(step => step.stepId === ActionStepID.LOOP)
|
|
) {
|
|
bindings = bindings.filter(x => !x.readableBinding.includes("loop"))
|
|
}
|
|
return bindings
|
|
}
|
|
|
|
function lookForFilters(properties) {
|
|
if (!properties) {
|
|
return []
|
|
}
|
|
let filters
|
|
const inputs = testData ? testData : block.inputs
|
|
for (let [key, field] of properties) {
|
|
// need to look for the builder definition (keyed separately, see saveFilters)
|
|
const defKey = `${key}-def`
|
|
if (
|
|
(field.customType === AutomationCustomIOType.FILTERS ||
|
|
field.customType === AutomationCustomIOType.TRIGGER_FILTER) &&
|
|
inputs?.[defKey]
|
|
) {
|
|
filters = inputs[defKey]
|
|
break
|
|
}
|
|
}
|
|
return utils.processSearchFilters(filters)
|
|
}
|
|
|
|
function saveFilters(key) {
|
|
const query = QueryUtils.buildQuery(tempFilters)
|
|
onChange({
|
|
[key]: query,
|
|
[`${key}-def`]: tempFilters, // need to store the builder definition in the automation
|
|
})
|
|
|
|
drawer.hide()
|
|
}
|
|
|
|
function canShowField(value) {
|
|
const dependsOn = value?.dependsOn
|
|
return !dependsOn || !!inputData[dependsOn]
|
|
}
|
|
|
|
function shouldRenderField(value) {
|
|
return (
|
|
value.customType !== "row" &&
|
|
value.customType !== "code" &&
|
|
value.customType !== "queryParams" &&
|
|
value.customType !== "cron" &&
|
|
value.customType !== "triggerSchema" &&
|
|
value.customType !== "automationFields" &&
|
|
value.customType !== "fields" &&
|
|
value.customType !== "trigger_filter_setting" &&
|
|
value.type !== "signature_single" &&
|
|
value.type !== "attachment" &&
|
|
value.type !== "attachment_single"
|
|
)
|
|
}
|
|
|
|
function getFieldLabel(key, value) {
|
|
const requiredSuffix = requiredProperties.includes(key) ? "*" : ""
|
|
const label = `${
|
|
value.title || (key === "row" ? "Row" : key)
|
|
} ${requiredSuffix}`
|
|
return Helpers.capitalise(label)
|
|
}
|
|
|
|
function handleAttachmentParams(keyValueObj) {
|
|
let params = {}
|
|
if (keyValueObj?.length) {
|
|
for (let param of keyValueObj) {
|
|
params[param.url] = param.filename
|
|
}
|
|
}
|
|
return params
|
|
}
|
|
|
|
onMount(async () => {
|
|
try {
|
|
await environment.loadVariables()
|
|
} catch (error) {
|
|
console.error(error)
|
|
}
|
|
})
|
|
</script>
|
|
|
|
<div class="step-fields">
|
|
<!-- Custom Layouts -->
|
|
{#if stepLayouts[block.stepId]}
|
|
{#each Object.keys(stepLayouts[block.stepId] || {}) as key}
|
|
{#if canShowField(key, stepLayouts[block.stepId].schema)}
|
|
{#each stepLayouts[block.stepId][key].content as config}
|
|
{#if config.title}
|
|
<PropField label={config.title} labelTooltip={config.tooltip}>
|
|
<svelte:component
|
|
this={config.type}
|
|
{...config.props}
|
|
{bindings}
|
|
on:change={config.props.onChange}
|
|
/>
|
|
</PropField>
|
|
{:else}
|
|
<svelte:component
|
|
this={config.type}
|
|
{...config.props}
|
|
{bindings}
|
|
on:change={config.props.onChange}
|
|
/>
|
|
{/if}
|
|
{/each}
|
|
{/if}
|
|
{/each}
|
|
{:else}
|
|
<!-- Default Schema Property Layout -->
|
|
{#each schemaProperties as [key, value]}
|
|
{#if canShowField(key, value)}
|
|
{@const label = getFieldLabel(key, value)}
|
|
<div class:block-field={shouldRenderField(value)}>
|
|
{#if key !== "fields" && value.type !== "boolean" && shouldRenderField(value)}
|
|
<div class="label-container">
|
|
<Label>
|
|
{label}
|
|
</Label>
|
|
{#if value.customType === AutomationCustomIOType.TRIGGER_FILTER}
|
|
<Icon
|
|
hoverable
|
|
on:click={() =>
|
|
window.open(
|
|
"https://docs.budibase.com/docs/row-trigger-filters",
|
|
"_blank"
|
|
)}
|
|
size="XS"
|
|
name="InfoOutline"
|
|
/>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
<div class:field-width={shouldRenderField(value)}>
|
|
{#if value.type === "string" && value.enum && canShowField(key, value)}
|
|
<Select
|
|
on:change={e => onChange({ [key]: e.detail })}
|
|
value={inputData[key]}
|
|
placeholder={false}
|
|
options={value.enum}
|
|
getOptionLabel={(x, idx) =>
|
|
value.pretty ? value.pretty[idx] : x}
|
|
disabled={value.readonly}
|
|
/>
|
|
{:else if value.type === "json"}
|
|
<Editor
|
|
editorHeight="250"
|
|
editorWidth="448"
|
|
mode="json"
|
|
value={inputData[key]?.value}
|
|
on:change={e => onChange({ [key]: e.detail })}
|
|
readOnly={value.readonly}
|
|
/>
|
|
{:else if value.type === "boolean"}
|
|
<div style="margin-top: 10px">
|
|
<Checkbox
|
|
text={value.title}
|
|
value={inputData[key]}
|
|
on:change={e => onChange({ [key]: e.detail })}
|
|
disabled={value.readonly}
|
|
/>
|
|
</div>
|
|
{:else if value.type === "date"}
|
|
<DrawerBindableSlot
|
|
title={value.title ?? label}
|
|
panel={AutomationBindingPanel}
|
|
type={"date"}
|
|
value={inputData[key]}
|
|
on:change={e => onChange({ [key]: e.detail })}
|
|
{bindings}
|
|
allowJS={true}
|
|
updateOnChange={false}
|
|
drawerLeft="260px"
|
|
disabled={value.readonly}
|
|
>
|
|
<DatePicker
|
|
value={inputData[key]}
|
|
on:change={e => onChange({ [key]: e.detail })}
|
|
/>
|
|
</DrawerBindableSlot>
|
|
{:else if value.customType === "column"}
|
|
<Select
|
|
on:change={e => onChange({ [key]: e.detail })}
|
|
value={inputData[key]}
|
|
options={Object.keys(table?.schema || {})}
|
|
disabled={value.readonly}
|
|
/>
|
|
{:else if value.type === "attachment" || value.type === "signature_single"}
|
|
<div class="attachment-field-wrapper">
|
|
<div class="label-wrapper">
|
|
<Label>{label}</Label>
|
|
</div>
|
|
<div class="toggle-container">
|
|
<Toggle
|
|
value={inputData?.meta?.useAttachmentBinding}
|
|
text={"Use bindings"}
|
|
size={"XS"}
|
|
on:change={e => {
|
|
onChange({
|
|
[key]: null,
|
|
meta: {
|
|
useAttachmentBinding: e.detail,
|
|
},
|
|
})
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
<div class="attachment-field-width">
|
|
{#if !inputData?.meta?.useAttachmentBinding}
|
|
<KeyValueBuilder
|
|
on:change={e =>
|
|
onChange({
|
|
[key]: e.detail.map(({ name, value }) => ({
|
|
url: name,
|
|
filename: value,
|
|
})),
|
|
})}
|
|
object={handleAttachmentParams(inputData[key])}
|
|
allowJS
|
|
{bindings}
|
|
keyBindings
|
|
customButtonText={value.type === "attachment"
|
|
? "Add attachment"
|
|
: "Add signature"}
|
|
keyPlaceholder={"URL"}
|
|
valuePlaceholder={"Filename"}
|
|
/>
|
|
{:else if isTestModal}
|
|
<ModalBindableInput
|
|
title={value.title || label}
|
|
value={inputData[key]}
|
|
panel={AutomationBindingPanel}
|
|
type={value.customType}
|
|
on:change={e => onChange({ [key]: e.detail })}
|
|
{bindings}
|
|
updateOnChange={false}
|
|
/>
|
|
{:else}
|
|
<DrawerBindableInput
|
|
title={value.title ?? label}
|
|
panel={AutomationBindingPanel}
|
|
type={value.customType}
|
|
value={inputData[key]}
|
|
on:change={e => onChange({ [key]: e.detail })}
|
|
{bindings}
|
|
updateOnChange={false}
|
|
placeholder={value.customType === "queryLimit"
|
|
? queryLimit
|
|
: ""}
|
|
drawerLeft="260px"
|
|
/>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
{:else if value.customType === AutomationCustomIOType.FILTERS || value.customType === AutomationCustomIOType.TRIGGER_FILTER}
|
|
<ActionButton fullWidth on:click={drawer.show}>
|
|
{filterCount > 0 ? "Update Filter" : "No Filter set"}
|
|
</ActionButton>
|
|
<Drawer
|
|
bind:this={drawer}
|
|
title="Filtering"
|
|
forceModal
|
|
on:drawerShow={() => {
|
|
tempFilters = filters
|
|
}}
|
|
>
|
|
<Button cta slot="buttons" on:click={() => saveFilters(key)}>
|
|
Save
|
|
</Button>
|
|
|
|
<DrawerContent slot="body">
|
|
<FilterBuilder
|
|
filters={tempFilters}
|
|
{bindings}
|
|
{schemaFields}
|
|
datasource={{ type: "table", tableId }}
|
|
panel={AutomationBindingPanel}
|
|
showFilterEmptyDropdown={!rowTriggers.includes(stepId)}
|
|
on:change={e => (tempFilters = e.detail)}
|
|
/>
|
|
</DrawerContent>
|
|
</Drawer>
|
|
{:else if value.customType === "cron"}
|
|
<CronBuilder
|
|
on:change={e => onChange({ [key]: e.detail })}
|
|
cronExpression={inputData[key]}
|
|
/>
|
|
{:else if value.customType === "automationFields"}
|
|
<AutomationSelector
|
|
on:change={e => onChange({ [key]: e.detail })}
|
|
value={inputData[key]}
|
|
{bindings}
|
|
/>
|
|
{:else if value.customType === "queryParams"}
|
|
<QueryParamSelector
|
|
on:change={e => onChange({ [key]: e.detail })}
|
|
value={inputData[key]}
|
|
{bindings}
|
|
/>
|
|
{:else if value.customType === "table"}
|
|
<TableSelector
|
|
{isTrigger}
|
|
value={inputData[key]}
|
|
on:change={e => onChange({ [key]: e.detail })}
|
|
disabled={value.readonly}
|
|
/>
|
|
{:else if value.customType === "webhookUrl"}
|
|
<WebhookDisplay value={inputData[key]} />
|
|
{:else if value.customType === "fields"}
|
|
<FieldSelector
|
|
{block}
|
|
value={inputData[key]}
|
|
on:change={e => onChange({ [key]: e.detail })}
|
|
{bindings}
|
|
{isTestModal}
|
|
/>
|
|
{:else if value.customType === "triggerSchema"}
|
|
<SchemaSetup
|
|
on:change={e => onChange({ [key]: e.detail })}
|
|
value={inputData[key]}
|
|
/>
|
|
{:else if value.customType === "code"}
|
|
<CodeEditorModal
|
|
on:hide={() => {
|
|
// Push any pending changes when the window closes
|
|
onChange({ [key]: inputData[key] })
|
|
}}
|
|
>
|
|
<div class:js-editor={editingJs}>
|
|
<div
|
|
class:js-code={editingJs}
|
|
style="width:100%;height:500px;"
|
|
>
|
|
<CodeEditor
|
|
value={inputData[key]}
|
|
on:change={e => {
|
|
// need to pass without the value inside
|
|
inputData[key] = e.detail
|
|
}}
|
|
completions={stepCompletions}
|
|
mode={codeMode}
|
|
autocompleteEnabled={codeMode !== EditorModes.JS}
|
|
bind:getCaretPosition
|
|
bind:insertAtPos
|
|
placeholder={codeMode === EditorModes.Handlebars
|
|
? "Add bindings by typing {{"
|
|
: null}
|
|
/>
|
|
</div>
|
|
{#if editingJs}
|
|
<div class="js-binding-picker">
|
|
<BindingSidePanel
|
|
{bindings}
|
|
allowHelpers={false}
|
|
addBinding={binding =>
|
|
bindingsHelpers.onSelectBinding(
|
|
inputData[key],
|
|
binding,
|
|
{
|
|
js: true,
|
|
dontDecode: true,
|
|
type: BindingType.RUNTIME,
|
|
}
|
|
)}
|
|
mode="javascript"
|
|
/>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</CodeEditorModal>
|
|
{:else if value.customType === "loopOption"}
|
|
<Select
|
|
on:change={e => onChange({ [key]: e.detail })}
|
|
autoWidth
|
|
value={inputData[key]}
|
|
options={["Array", "String"]}
|
|
defaultValue={"Array"}
|
|
/>
|
|
{:else if value.type === "string" || value.type === "number" || value.type === "integer"}
|
|
{#if isTestModal}
|
|
<ModalBindableInput
|
|
title={value.title || label}
|
|
value={inputData[key]}
|
|
panel={AutomationBindingPanel}
|
|
type={value.customType}
|
|
on:change={e => onChange({ [key]: e.detail })}
|
|
{bindings}
|
|
updateOnChange={false}
|
|
/>
|
|
{:else}
|
|
<div>
|
|
<DrawerBindableInput
|
|
title={value.title ?? label}
|
|
panel={AutomationBindingPanel}
|
|
type={value.customType}
|
|
value={inputData[key]}
|
|
on:change={e => onChange({ [key]: e.detail })}
|
|
{bindings}
|
|
updateOnChange={false}
|
|
placeholder={value.customType === "queryLimit"
|
|
? queryLimit
|
|
: ""}
|
|
drawerLeft="260px"
|
|
/>
|
|
</div>
|
|
{/if}
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
{/each}
|
|
{/if}
|
|
</div>
|
|
|
|
<Modal bind:this={webhookModal} width="30%">
|
|
<CreateWebhookModal />
|
|
</Modal>
|
|
|
|
{#if stepId === TriggerStepID.WEBHOOK && !isTestModal}
|
|
<Button secondary on:click={() => webhookModal.show()}>Set Up Webhook</Button>
|
|
{/if}
|
|
|
|
<style>
|
|
.label-container {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-s);
|
|
}
|
|
.field-width {
|
|
width: 320px;
|
|
}
|
|
|
|
.step-fields {
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: flex-start;
|
|
align-items: stretch;
|
|
gap: var(--spacing-l);
|
|
}
|
|
|
|
.block-field {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
gap: 10px;
|
|
flex: 1;
|
|
}
|
|
|
|
.attachment-field-width {
|
|
margin-top: var(--spacing-xs);
|
|
}
|
|
|
|
.label-wrapper {
|
|
margin-top: var(--spacing-s);
|
|
}
|
|
|
|
.js-editor {
|
|
display: flex;
|
|
flex-direction: row;
|
|
flex-grow: 1;
|
|
width: 100%;
|
|
}
|
|
|
|
.js-code {
|
|
flex: 7;
|
|
}
|
|
|
|
.js-binding-picker {
|
|
flex: 3;
|
|
margin-top: calc((var(--spacing-xl) * -1) + 1px);
|
|
}
|
|
</style>
|