v2 JavaScript UX, automation bug fixes and general refactoring

This commit is contained in:
Dean 2025-01-29 11:51:12 +00:00
parent 8693cdc67f
commit 0463462b77
22 changed files with 614 additions and 87 deletions

View File

@ -18,8 +18,12 @@
import AutomationBindingPanel from "@/components/common/bindings/ServerBindingPanel.svelte" import AutomationBindingPanel from "@/components/common/bindings/ServerBindingPanel.svelte"
import FlowItemHeader from "./FlowItemHeader.svelte" import FlowItemHeader from "./FlowItemHeader.svelte"
import FlowItemActions from "./FlowItemActions.svelte" import FlowItemActions from "./FlowItemActions.svelte"
import { automationStore, selectedAutomation } from "@/stores/builder" import {
import { QueryUtils, Utils } from "@budibase/frontend-core" automationStore,
selectedAutomation,
evaluationContext,
} from "@/stores/builder"
import { QueryUtils, Utils, memo } from "@budibase/frontend-core"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import { createEventDispatcher, getContext } from "svelte" import { createEventDispatcher, getContext } from "svelte"
import DragZone from "./DragZone.svelte" import DragZone from "./DragZone.svelte"
@ -34,11 +38,14 @@
export let automation export let automation
const view = getContext("draggableView") const view = getContext("draggableView")
const memoContext = memo({})
let drawer let drawer
let open = true let open = true
let confirmDeleteModal let confirmDeleteModal
$: memoContext.set($evaluationContext)
$: branch = step.inputs?.branches?.[branchIdx] $: branch = step.inputs?.branches?.[branchIdx]
$: editableConditionUI = branch.conditionUI || {} $: editableConditionUI = branch.conditionUI || {}
@ -100,6 +107,7 @@
allowOnEmpty={false} allowOnEmpty={false}
builderType={"condition"} builderType={"condition"}
docsURL={null} docsURL={null}
evaluationContext={$memoContext}
/> />
</DrawerContent> </DrawerContent>
</Drawer> </Drawer>

View File

@ -21,7 +21,7 @@
} from "@budibase/bbui" } from "@budibase/bbui"
import CreateWebhookModal from "@/components/automation/Shared/CreateWebhookModal.svelte" import CreateWebhookModal from "@/components/automation/Shared/CreateWebhookModal.svelte"
import { automationStore, tables } from "@/stores/builder" import { automationStore, tables, evaluationContext } from "@/stores/builder"
import { environment } from "@/stores/portal" import { environment } from "@/stores/portal"
import WebhookDisplay from "../Shared/WebhookDisplay.svelte" import WebhookDisplay from "../Shared/WebhookDisplay.svelte"
import { import {
@ -62,6 +62,8 @@
} from "@budibase/types" } from "@budibase/types"
import PropField from "./PropField.svelte" import PropField from "./PropField.svelte"
import { utils } from "@budibase/shared-core" import { utils } from "@budibase/shared-core"
import { encodeJSBinding } from "@budibase/string-templates"
import CodeEditorField from "@/components/common/bindings/CodeEditorField.svelte"
export let automation export let automation
export let block export let block
@ -74,6 +76,7 @@
// Stop unnecessary rendering // Stop unnecessary rendering
const memoBlock = memo(block) const memoBlock = memo(block)
const memoContext = memo({})
const rowTriggers = [ const rowTriggers = [
TriggerStepID.ROW_UPDATED, TriggerStepID.ROW_UPDATED,
@ -97,6 +100,7 @@
let stepLayouts = {} let stepLayouts = {}
$: memoBlock.set(block) $: memoBlock.set(block)
$: memoContext.set($evaluationContext)
$: filters = lookForFilters(schemaProperties) $: filters = lookForFilters(schemaProperties)
$: filterCount = $: filterCount =
@ -140,6 +144,7 @@
? [hbAutocomplete([...bindingsToCompletions(bindings, codeMode)])] ? [hbAutocomplete([...bindingsToCompletions(bindings, codeMode)])]
: [] : []
// TODO: check if it inputData != newInputData (memo)
const getInputData = (testData, blockInputs) => { const getInputData = (testData, blockInputs) => {
// Test data is not cloned for reactivity // Test data is not cloned for reactivity
let newInputData = testData || cloneDeep(blockInputs) let newInputData = testData || cloneDeep(blockInputs)
@ -156,6 +161,7 @@
} }
const setDefaultEnumValues = () => { const setDefaultEnumValues = () => {
// TODO: Update this for memoisation
for (const [key, value] of schemaProperties) { for (const [key, value] of schemaProperties) {
if (value.type === "string" && value.enum && inputData[key] == null) { if (value.type === "string" && value.enum && inputData[key] == null) {
inputData[key] = value.enum[0] inputData[key] = value.enum[0]
@ -200,7 +206,6 @@
onChange({ ["revision"]: e.detail }) onChange({ ["revision"]: e.detail })
}, },
updateOnChange: false, updateOnChange: false,
forceModal: true,
}, },
}, },
] ]
@ -228,7 +233,6 @@
onChange({ [rowIdentifier]: e.detail }) onChange({ [rowIdentifier]: e.detail })
}, },
updateOnChange: false, updateOnChange: false,
forceModal: true,
}, },
}, },
] ]
@ -476,6 +480,10 @@
...update, ...update,
}) })
if (!updatedAutomation) {
return
}
// Exclude default or invalid data from the test data // Exclude default or invalid data from the test data
let updatedFields = {} let updatedFields = {}
for (const key of Object.keys(block?.inputs?.fields || {})) { for (const key of Object.keys(block?.inputs?.fields || {})) {
@ -547,7 +555,7 @@
...newTestData, ...newTestData,
body: { body: {
...update, ...update,
...automation.testData?.body, ...(automation?.testData?.body || {}),
}, },
} }
} }
@ -668,6 +676,7 @@
{...config.props} {...config.props}
{bindings} {bindings}
on:change={config.props.onChange} on:change={config.props.onChange}
context={$memoContext}
/> />
</PropField> </PropField>
{:else} {:else}
@ -676,6 +685,7 @@
{...config.props} {...config.props}
{bindings} {bindings}
on:change={config.props.onChange} on:change={config.props.onChange}
context={$memoContext}
/> />
{/if} {/if}
{/each} {/each}
@ -800,6 +810,7 @@
: "Add signature"} : "Add signature"}
keyPlaceholder={"URL"} keyPlaceholder={"URL"}
valuePlaceholder={"Filename"} valuePlaceholder={"Filename"}
context={$memoContext}
/> />
{:else if isTestModal} {:else if isTestModal}
<ModalBindableInput <ModalBindableInput
@ -824,6 +835,7 @@
? queryLimit ? queryLimit
: ""} : ""}
drawerLeft="260px" drawerLeft="260px"
context={$memoContext}
/> />
{/if} {/if}
</div> </div>
@ -853,6 +865,7 @@
panel={AutomationBindingPanel} panel={AutomationBindingPanel}
showFilterEmptyDropdown={!rowTriggers.includes(stepId)} showFilterEmptyDropdown={!rowTriggers.includes(stepId)}
on:change={e => (tempFilters = e.detail)} on:change={e => (tempFilters = e.detail)}
evaluationContext={$memoContext}
/> />
</DrawerContent> </DrawerContent>
</Drawer> </Drawer>
@ -895,7 +908,39 @@
on:change={e => onChange({ [key]: e.detail })} on:change={e => onChange({ [key]: e.detail })}
value={inputData[key]} value={inputData[key]}
/> />
{:else if value.customType === "code"} {:else if value.customType === "code" && stepId === ActionStepID.EXECUTE_SCRIPT_V2}
<div class="scriptv2-wrapper">
<DrawerBindableSlot
title={"Edit Code"}
panel={AutomationBindingPanel}
type={"longform"}
{schema}
on:change={e => onChange({ [key]: e.detail })}
value={inputData[key]}
{bindings}
allowJS={true}
allowHBS={false}
updateOnChange={false}
context={$memoContext}
>
<div class="field-wrap code-editor">
<CodeEditorField
value={inputData[key]}
{bindings}
context={$memoContext}
allowHBS={false}
allowJS
placeholder={codeMode === EditorModes.Handlebars
? "Add bindings by typing {{"
: null}
on:blur={e =>
onChange({ [key]: encodeJSBinding(e.detail) })}
/>
</div>
</DrawerBindableSlot>
</div>
{:else if value.customType === "code" && stepId === ActionStepID.EXECUTE_SCRIPT}
<!-- DEPRECATED -->
<CodeEditorModal <CodeEditorModal
on:hide={() => { on:hide={() => {
// Push any pending changes when the window closes // Push any pending changes when the window closes
@ -977,6 +1022,7 @@
? queryLimit ? queryLimit
: ""} : ""}
drawerLeft="260px" drawerLeft="260px"
context={$memoContext}
/> />
</div> </div>
{/if} {/if}
@ -1044,4 +1090,23 @@
flex: 3; flex: 3;
margin-top: calc((var(--spacing-xl) * -1) + 1px); margin-top: calc((var(--spacing-xl) * -1) + 1px);
} }
.field-wrap :global(.cm-editor),
.field-wrap :global(.cm-scroller) {
border-radius: 4px;
}
.field-wrap {
box-sizing: border-box;
border: 1px solid var(--spectrum-global-color-gray-400);
border-radius: 4px;
}
.field-wrap.code-editor {
height: 180px;
}
.scriptv2-wrapper :global(.icon.slot-icon) {
top: 1px;
border-bottom-left-radius: var(--spectrum-alias-border-radius-regular);
border-right: 0px;
border-bottom: 1px solid var(--spectrum-alias-border-color);
}
</style> </style>

View File

@ -25,6 +25,7 @@
export let meta export let meta
export let bindings export let bindings
export let isTestModal export let isTestModal
export let context = {}
const typeToField = Object.values(FIELDS).reduce((acc, field) => { const typeToField = Object.values(FIELDS).reduce((acc, field) => {
acc[field.type] = field acc[field.type] = field
@ -58,7 +59,7 @@
$: parsedBindings = bindings.map(binding => { $: parsedBindings = bindings.map(binding => {
let clone = Object.assign({}, binding) let clone = Object.assign({}, binding)
clone.icon = "ShareAndroid" clone.icon = clone.icon ?? "ShareAndroid"
return clone return clone
}) })
@ -258,6 +259,7 @@
fields: editableFields, fields: editableFields,
}} }}
{onChange} {onChange}
{context}
/> />
{:else} {:else}
<DrawerBindableSlot <DrawerBindableSlot
@ -276,6 +278,7 @@
allowJS={true} allowJS={true}
updateOnChange={false} updateOnChange={false}
drawerLeft="260px" drawerLeft="260px"
{context}
> >
<RowSelectorTypes <RowSelectorTypes
{isTestModal} {isTestModal}
@ -286,6 +289,7 @@
meta={{ meta={{
fields: editableFields, fields: editableFields,
}} }}
{context}
onChange={change => onChange(change)} onChange={change => onChange(change)}
/> />
</DrawerBindableSlot> </DrawerBindableSlot>

View File

@ -25,12 +25,13 @@
export let meta export let meta
export let bindings export let bindings
export let isTestModal export let isTestModal
export let context
$: fieldData = value[field] $: fieldData = value[field]
$: parsedBindings = bindings.map(binding => { $: parsedBindings = bindings.map(binding => {
let clone = Object.assign({}, binding) let clone = Object.assign({}, binding)
clone.icon = "ShareAndroid" clone.icon = clone.icon ?? "ShareAndroid"
return clone return clone
}) })
@ -232,6 +233,7 @@
actionButtonDisabled={(schema.type === FieldType.ATTACHMENT_SINGLE || actionButtonDisabled={(schema.type === FieldType.ATTACHMENT_SINGLE ||
schema.type === FieldType.SIGNATURE_SINGLE) && schema.type === FieldType.SIGNATURE_SINGLE) &&
fieldData} fieldData}
{context}
/> />
</div> </div>
{:else} {:else}

View File

@ -1,18 +1,11 @@
<script> <script>
import { Input, Select, Button } from "@budibase/bbui" import { Input, Select, Button } from "@budibase/bbui"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import { memo } from "@budibase/frontend-core"
const dispatch = createEventDispatcher() import { generate } from "shortid"
export let value = {} export let value = {}
$: fieldsArray = value
? Object.entries(value).map(([name, type]) => ({
name,
type,
}))
: []
const typeOptions = [ const typeOptions = [
{ {
label: "Text", label: "Text",
@ -36,16 +29,42 @@
}, },
] ]
const dispatch = createEventDispatcher()
const memoValue = memo({ data: {} })
$: memoValue.set({ data: value })
$: fieldsArray = $memoValue.data
? Object.entries($memoValue.data).map(([name, type]) => ({
name,
type,
id: generate(),
}))
: []
function addField() { function addField() {
const newValue = { ...value } const newValue = { ...$memoValue.data }
newValue[""] = "string" newValue[""] = "string"
dispatch("change", newValue) fieldsArray = [...fieldsArray, { name: "", type: "string", id: generate() }]
} }
function removeField(name) { function removeField(idx) {
const newValues = { ...value } const entries = [...fieldsArray]
delete newValues[name]
dispatch("change", newValues) // Remove empty field
if (!entries[idx]?.name) {
fieldsArray.splice(idx, 1)
fieldsArray = [...fieldsArray]
return
}
entries.splice(idx, 1)
const update = entries.reduce((newVals, current) => {
newVals[current.name.trim()] = current.type
return newVals
}, {})
dispatch("change", update)
} }
const fieldNameChanged = originalName => e => { const fieldNameChanged = originalName => e => {
@ -57,11 +76,16 @@
} else { } else {
entries = entries.filter(f => f.name !== originalName) entries = entries.filter(f => f.name !== originalName)
} }
value = entries.reduce((newVals, current) => {
newVals[current.name.trim()] = current.type const update = entries
return newVals .filter(entry => entry.name)
}, {}) .reduce((newVals, current) => {
dispatch("change", value) newVals[current.name.trim()] = current.type
return newVals
}, {})
if (Object.keys(update).length) {
dispatch("change", update)
}
} }
</script> </script>
@ -69,7 +93,7 @@
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="root"> <div class="root">
<div class="spacer" /> <div class="spacer" />
{#each fieldsArray as field} {#each fieldsArray as field, idx (field.id)}
<div class="field"> <div class="field">
<Input <Input
value={field.name} value={field.name}
@ -88,7 +112,9 @@
/> />
<i <i
class="remove-field ri-delete-bin-line" class="remove-field ri-delete-bin-line"
on:click={() => removeField(field.name)} on:click={() => {
removeField(idx)
}}
/> />
</div> </div>
{/each} {/each}
@ -115,4 +141,12 @@
align-items: center; align-items: center;
gap: var(--spacing-m); gap: var(--spacing-m);
} }
.remove-field {
cursor: pointer;
}
.remove-field:hover {
color: var(--spectrum-global-color-gray-900);
}
</style> </style>

View File

@ -1,3 +1,10 @@
<script context="module" lang="ts">
export const DropdownPosition = {
Relative: "top",
Absolute: "right",
}
</script>
<script lang="ts"> <script lang="ts">
import { Label } from "@budibase/bbui" import { Label } from "@budibase/bbui"
import { onMount, createEventDispatcher, onDestroy } from "svelte" import { onMount, createEventDispatcher, onDestroy } from "svelte"
@ -45,6 +52,7 @@
import { EditorModes } from "./" import { EditorModes } from "./"
import { themeStore } from "@/stores/portal" import { themeStore } from "@/stores/portal"
import type { EditorMode } from "@budibase/types" import type { EditorMode } from "@budibase/types"
import { tooltips } from "@codemirror/view"
export let label: string | undefined = undefined export let label: string | undefined = undefined
// TODO: work out what best type fits this // TODO: work out what best type fits this
@ -57,11 +65,13 @@
export let jsBindingWrapping = true export let jsBindingWrapping = true
export let readonly = false export let readonly = false
export let readonlyLineNumbers = false export let readonlyLineNumbers = false
export let dropdown = DropdownPosition.Relative
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let textarea: HTMLDivElement let textarea: HTMLDivElement
let editor: EditorView let editor: EditorView
let editorEle: HTMLDivElement
let mounted = false let mounted = false
let isEditorInitialised = false let isEditorInitialised = false
let queuedRefresh = false let queuedRefresh = false
@ -112,7 +122,6 @@
queuedRefresh = true queuedRefresh = true
return return
} }
if ( if (
editor && editor &&
value && value &&
@ -343,14 +352,25 @@
const baseExtensions = buildBaseExtensions() const baseExtensions = buildBaseExtensions()
editor = new EditorView({ editor = new EditorView({
doc: value?.toString(), doc: String(value),
extensions: buildExtensions(baseExtensions), extensions: buildExtensions([
...baseExtensions,
dropdown == DropdownPosition.Absolute
? tooltips({
position: "absolute",
})
: [],
]),
parent: textarea, parent: textarea,
}) })
} }
onMount(async () => { onMount(async () => {
mounted = true mounted = true
// Capture scrolling
editorEle.addEventListener("wheel", e => {
e.stopPropagation()
})
}) })
onDestroy(() => { onDestroy(() => {
@ -366,7 +386,7 @@
</div> </div>
{/if} {/if}
<div class={`code-editor ${mode?.name || ""}`}> <div class={`code-editor ${mode?.name || ""}`} bind:this={editorEle}>
<div tabindex="-1" bind:this={textarea} /> <div tabindex="-1" bind:this={textarea} />
</div> </div>
@ -534,12 +554,11 @@
/* Live binding value / helper container */ /* Live binding value / helper container */
.code-editor :global(.cm-completionInfo) { .code-editor :global(.cm-completionInfo) {
margin-left: var(--spacing-s); margin: 0px var(--spacing-s);
border: 1px solid var(--spectrum-global-color-gray-300); border: 1px solid var(--spectrum-global-color-gray-300);
border-radius: var(--border-radius-s); border-radius: var(--border-radius-s);
background-color: var(--spectrum-global-color-gray-50); background-color: var(--spectrum-global-color-gray-50);
padding: var(--spacing-m); padding: var(--spacing-m);
margin-top: -2px;
} }
/* Wrapper around helpers */ /* Wrapper around helpers */
@ -564,6 +583,7 @@
white-space: pre; white-space: pre;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
overflow-y: auto;
max-height: 480px; max-height: 480px;
} }
.code-editor :global(.binding__example.helper) { .code-editor :global(.binding__example.helper) {

View File

@ -61,8 +61,6 @@
let mode: BindingMode | null let mode: BindingMode | null
let sidePanel: SidePanel | null let sidePanel: SidePanel | null
let initialValueJS = value?.startsWith?.("{{ js ") let initialValueJS = value?.startsWith?.("{{ js ")
let jsValue: string | null = initialValueJS ? value : null
let hbsValue: string | null = initialValueJS ? null : value
let getCaretPosition: CaretPositionFn | undefined let getCaretPosition: CaretPositionFn | undefined
let insertAtPos: InsertAtPositionFn | undefined let insertAtPos: InsertAtPositionFn | undefined
let targetMode: BindingMode | null = null let targetMode: BindingMode | null = null
@ -71,6 +69,10 @@
let expressionError: string | undefined let expressionError: string | undefined
let evaluating = false let evaluating = false
// Ensure these values are not stale
$: jsValue = initialValueJS ? value : null
$: hbsValue = initialValueJS ? null : value
$: useSnippets = allowSnippets && !$licensing.isFreePlan $: useSnippets = allowSnippets && !$licensing.isFreePlan
$: editorModeOptions = getModeOptions(allowHBS, allowJS) $: editorModeOptions = getModeOptions(allowHBS, allowJS)
$: sidePanelOptions = getSidePanelOptions( $: sidePanelOptions = getSidePanelOptions(

View File

@ -0,0 +1,203 @@
<script lang="ts">
import { createEventDispatcher, onMount } from "svelte"
import {
decodeJSBinding,
encodeJSBinding,
processObjectSync,
} from "@budibase/string-templates"
import { runtimeToReadableBinding } from "@/dataBinding"
import CodeEditor, { DropdownPosition } from "../CodeEditor/CodeEditor.svelte"
import {
getHelperCompletions,
jsAutocomplete,
snippetAutoComplete,
EditorModes,
bindingsToCompletions,
} from "../CodeEditor"
import { JsonFormatter } from "@budibase/frontend-core"
import { licensing } from "@/stores/portal"
import { BindingMode } from "@budibase/types"
import type {
EnrichedBinding,
BindingCompletion,
Snippet,
CaretPositionFn,
InsertAtPositionFn,
JSONValue,
} from "@budibase/types"
import type { CompletionContext } from "@codemirror/autocomplete"
import { snippets } from "@/stores/builder"
const dispatch = createEventDispatcher()
export let bindings: EnrichedBinding[] = []
export let value: string = ""
export let allowHBS = true
export let allowJS = false
export let allowHelpers = true
export let allowSnippets = true
export let context = null
export let autofocusEditor = false
export let placeholder = null
let mode: BindingMode | null
let initialValueJS = value?.startsWith?.("{{ js ")
let getCaretPosition: CaretPositionFn | undefined
let insertAtPos: InsertAtPositionFn | undefined
// TO Switch the runtime
$: readable = runtimeToReadableBinding(bindings, value || "")
$: jsValue = decodeJSBinding(readable)
$: useSnippets = allowSnippets && !$licensing.isFreePlan
$: editorModeOptions = getModeOptions(allowHBS, allowJS)
$: enrichedBindings = enrichBindings(bindings, context, $snippets)
$: editorMode = EditorModes.JS
$: bindingCompletions = bindingsToCompletions(enrichedBindings, editorMode)
$: jsCompletions = getJSCompletions(
bindingCompletions,
$snippets,
useSnippets
)
const getJSCompletions = (
bindingCompletions: BindingCompletion[],
snippets: Snippet[] | null,
useSnippets?: boolean
) => {
const completions: ((_: CompletionContext) => any)[] = [
jsAutocomplete([
...bindingCompletions,
...(allowHelpers ? getHelperCompletions(EditorModes.JS) : []),
]),
]
if (useSnippets && snippets) {
completions.push(snippetAutoComplete(snippets))
}
return completions
}
const getModeOptions = (allowHBS: boolean, allowJS: boolean) => {
let options = []
if (allowHBS) {
options.push(BindingMode.Text)
}
if (allowJS) {
options.push(BindingMode.JavaScript)
}
return options
}
const highlightJSON = (json: JSONValue) => {
return JsonFormatter.format(json, {
keyColor: "#e06c75",
numberColor: "#e5c07b",
stringColor: "#98c379",
trueColor: "#d19a66",
falseColor: "#d19a66",
nullColor: "#c678dd",
})
}
const enrichBindings = (
bindings: EnrichedBinding[],
context: any,
snippets: Snippet[] | null
) => {
// Create a single big array to enrich in one go
const bindingStrings = bindings.map(binding => {
if (binding.runtimeBinding.startsWith('trim "')) {
// Account for nasty hardcoded HBS bindings for roles, for legacy
// compatibility
return `{{ ${binding.runtimeBinding} }}`
} else {
return `{{ literal ${binding.runtimeBinding} }}`
}
})
const bindingEvaluations = processObjectSync(bindingStrings, {
...context,
snippets,
})
// Enrich bindings with evaluations and highlighted HTML
return bindings.map((binding, idx) => {
if (!context || typeof bindingEvaluations !== "object") {
return binding
}
const evalObj: Record<any, any> = bindingEvaluations
const value = JSON.stringify(evalObj[idx], null, 2)
return {
...binding,
value,
valueHTML: highlightJSON(value),
}
})
}
const updateValue = (val: any) => {
dispatch("change", val)
}
const onChangeJSValue = (e: { detail: string }) => {
// if(typeof onChange === "function"){
// }
if (!e.detail?.trim()) {
// Don't bother saving empty values as JS
updateValue(null)
} else {
updateValue(encodeJSBinding(e.detail))
}
}
onMount(() => {
// Set the initial mode appropriately
const initialValueMode = initialValueJS
? BindingMode.JavaScript
: BindingMode.Text
if (editorModeOptions.includes(initialValueMode)) {
mode = initialValueMode
} else {
mode = editorModeOptions[0]
}
})
</script>
<div class="code-panel">
<div class="editor">
{#key jsCompletions}
<CodeEditor
value={jsValue}
on:change={onChangeJSValue}
on:blur
completions={jsCompletions}
mode={EditorModes.JS}
bind:getCaretPosition
bind:insertAtPos
autofocus={autofocusEditor}
placeholder={placeholder ||
"Add bindings by typing $ or use the menu on the right"}
jsBindingWrapping
dropdown={DropdownPosition.Absolute}
/>
{/key}
</div>
</div>
<style>
.code-panel {
height: 100%;
display: flex;
}
/* Editor */
.editor {
flex: 1 1 auto;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
}
</style>

View File

@ -23,6 +23,9 @@
export let type export let type
export let schema export let schema
export let allowHBS = true
export let context = {}
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let bindingDrawer let bindingDrawer
let currentVal = value let currentVal = value
@ -147,7 +150,7 @@
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="control" class:disabled> <div class="control" class:disabled>
{#if !isValid(value)} {#if !isValid(value) && !$$slots.default}
<Input <Input
{label} {label}
{disabled} {disabled}
@ -187,7 +190,6 @@
on:drawerShow on:drawerShow
bind:this={bindingDrawer} bind:this={bindingDrawer}
title={title ?? placeholder ?? "Bindings"} title={title ?? placeholder ?? "Bindings"}
forceModal={true}
> >
<Button cta slot="buttons" on:click={saveBinding}>Save</Button> <Button cta slot="buttons" on:click={saveBinding}>Save</Button>
<svelte:component <svelte:component
@ -197,7 +199,9 @@
on:change={event => (tempValue = event.detail)} on:change={event => (tempValue = event.detail)}
{bindings} {bindings}
{allowJS} {allowJS}
{allowHBS}
{allowHelpers} {allowHelpers}
{context}
/> />
</Drawer> </Drawer>
@ -208,22 +212,22 @@
} }
.slot-icon { .slot-icon {
right: 31px !important; right: 31px;
border-right: 1px solid var(--spectrum-alias-border-color); border-right: 1px solid var(--spectrum-alias-border-color);
border-top-right-radius: 0px !important; border-top-right-radius: 0px;
border-bottom-right-radius: 0px !important; border-bottom-right-radius: 0px;
} }
.text-area-slot-icon { .text-area-slot-icon {
border-bottom: 1px solid var(--spectrum-alias-border-color); border-bottom: 1px solid var(--spectrum-alias-border-color);
border-bottom-right-radius: 0px !important; border-bottom-right-radius: 0px;
top: 1px !important; top: 1px;
} }
.json-slot-icon { .json-slot-icon {
border-bottom: 1px solid var(--spectrum-alias-border-color); border-bottom: 1px solid var(--spectrum-alias-border-color);
border-bottom-right-radius: 0px !important; border-bottom-right-radius: 0px;
top: 1px !important; top: 1px;
right: 0px !important; right: 0px;
} }
.icon { .icon {

View File

@ -5,6 +5,7 @@
export let bindings = [] export let bindings = []
export let value = "" export let value = ""
export let allowJS = false export let allowJS = false
export let allowHBS = true
export let context = null export let context = null
$: enrichedBindings = enrichBindings(bindings) $: enrichedBindings = enrichBindings(bindings)
@ -22,8 +23,10 @@
<BindingPanel <BindingPanel
bindings={enrichedBindings} bindings={enrichedBindings}
snippets={$snippets} snippets={$snippets}
allowHelpers
{value} {value}
{allowJS} {allowJS}
{allowHBS}
{context} {context}
on:change on:change
/> />

View File

@ -16,6 +16,7 @@
export let datasource export let datasource
export let builderType export let builderType
export let docsURL export let docsURL
export let evaluationContext = {}
</script> </script>
<CoreFilterBuilder <CoreFilterBuilder
@ -32,5 +33,6 @@
{allowOnEmpty} {allowOnEmpty}
{builderType} {builderType}
{docsURL} {docsURL}
{evaluationContext}
on:change on:change
/> />

View File

@ -39,6 +39,7 @@
export let allowJS = false export let allowJS = false
export let actionButtonDisabled = false export let actionButtonDisabled = false
export let compare = (option, value) => option === value export let compare = (option, value) => option === value
export let context = null
let fields = Object.entries(object || {}).map(([name, value]) => ({ let fields = Object.entries(object || {}).map(([name, value]) => ({
name, name,
@ -132,6 +133,7 @@
{allowJS} {allowJS}
{allowHelpers} {allowHelpers}
drawerLeft={bindingDrawerLeft} drawerLeft={bindingDrawerLeft}
{context}
/> />
{:else} {:else}
<Input readonly={readOnly} bind:value={field.name} on:blur={changed} /> <Input readonly={readOnly} bind:value={field.name} on:blur={changed} />
@ -158,6 +160,7 @@
{allowJS} {allowJS}
{allowHelpers} {allowHelpers}
drawerLeft={bindingDrawerLeft} drawerLeft={bindingDrawerLeft}
{context}
/> />
{:else} {:else}
<Input <Input

View File

@ -29,7 +29,9 @@
let modal let modal
let webhookModal let webhookModal
onMount(() => { onMount(async () => {
await automationStore.actions.initAppSelf()
$automationStore.showTestPanel = false $automationStore.showTestPanel = false
}) })

View File

@ -1,9 +1,9 @@
import { derived, get } from "svelte/store" import { derived, get, Readable, Writable } from "svelte/store"
import { API } from "@/api" import { API } from "@/api"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import { generate } from "shortid" import { generate } from "shortid"
import { createHistoryStore } from "@/stores/builder/history" import { createHistoryStore } from "@/stores/builder/history"
import { licensing } from "@/stores/portal" import { licensing, organisation, environment, auth } from "@/stores/portal"
import { tables, appStore } from "@/stores/builder" import { tables, appStore } from "@/stores/builder"
import { notifications } from "@budibase/bbui" import { notifications } from "@budibase/bbui"
import { import {
@ -32,8 +32,10 @@ import {
BlockDefinitions, BlockDefinitions,
GetAutomationTriggerDefinitionsResponse, GetAutomationTriggerDefinitionsResponse,
GetAutomationActionDefinitionsResponse, GetAutomationActionDefinitionsResponse,
AppSelfResponse,
TestAutomationResponse,
} from "@budibase/types" } from "@budibase/types"
import { ActionStepID } from "@/constants/backend/automations" import { ActionStepID, TriggerStepID } from "@/constants/backend/automations"
import { FIELDS } from "@/constants/backend" import { FIELDS } from "@/constants/backend"
import { sdk } from "@budibase/shared-core" import { sdk } from "@budibase/shared-core"
import { rowActions } from "./rowActions" import { rowActions } from "./rowActions"
@ -43,10 +45,11 @@ import { BudiStore, DerivedBudiStore } from "@/stores/BudiStore"
interface AutomationState { interface AutomationState {
automations: Automation[] automations: Automation[]
testResults: any | null testResults?: TestAutomationResponse
showTestPanel: boolean showTestPanel: boolean
blockDefinitions: BlockDefinitions blockDefinitions: BlockDefinitions
selectedAutomationId: string | null selectedAutomationId: string | null
appSelf?: AppSelfResponse
} }
interface DerivedAutomationState extends AutomationState { interface DerivedAutomationState extends AutomationState {
@ -56,7 +59,6 @@ interface DerivedAutomationState extends AutomationState {
const initialAutomationState: AutomationState = { const initialAutomationState: AutomationState = {
automations: [], automations: [],
testResults: null,
showTestPanel: false, showTestPanel: false,
blockDefinitions: { blockDefinitions: {
TRIGGER: {}, TRIGGER: {},
@ -88,6 +90,20 @@ const getFinalDefinitions = (
} }
const automationActions = (store: AutomationStore) => ({ const automationActions = (store: AutomationStore) => ({
/**
* Fetches the app user context used for live evaluation
* This matches the context used on the server
* @returns {AppSelfResponse | null}
*/
initAppSelf: async (): Promise<AppSelfResponse | null> => {
// Fetch and update the app self if it hasn't been set
const appSelfResponse = await API.fetchSelf()
store.update(state => ({
...state,
appSelf: appSelfResponse,
}))
return appSelfResponse
},
/** /**
* Move a given block from one location on the tree to another. * Move a given block from one location on the tree to another.
* *
@ -284,9 +300,12 @@ const automationActions = (store: AutomationStore) => ({
* Build a sequential list of all steps on the step path provided * Build a sequential list of all steps on the step path provided
* *
* @param {Array<Object>} pathWay e.g. [{stepIdx:2},{branchIdx:0, stepIdx:2},...] * @param {Array<Object>} pathWay e.g. [{stepIdx:2},{branchIdx:0, stepIdx:2},...]
* @returns {Array<Object>} all steps encountered on the provided path * @returns {Array<AutomationStep | AutomationTrigger>} all steps encountered on the provided path
*/ */
getPathSteps: (pathWay: Array<BranchPath>, automation: Automation) => { getPathSteps: (
pathWay: Array<BranchPath>,
automation: Automation
): Array<AutomationStep | AutomationTrigger> => {
// Base Steps, including trigger // Base Steps, including trigger
const steps = [ const steps = [
automation.definition.trigger, automation.definition.trigger,
@ -533,18 +552,24 @@ const automationActions = (store: AutomationStore) => ({
icon: string, icon: string,
idx: number, idx: number,
isLoopBlock: boolean, isLoopBlock: boolean,
bindingName?: string pathBlock: AutomationStep | AutomationTrigger,
bindingName: string
) => { ) => {
if (!name) return if (!name) return
const runtimeBinding = store.actions.determineRuntimeBinding( const runtimeBinding = store.actions.determineRuntimeBinding(
name, name,
idx, idx,
isLoopBlock, isLoopBlock,
bindingName,
automation, automation,
currentBlock, currentBlock,
pathSteps pathSteps
) )
const readableBinding = store.actions.determineReadableBinding(
name,
pathBlock
)
const categoryName = store.actions.determineCategoryName( const categoryName = store.actions.determineCategoryName(
idx, idx,
isLoopBlock, isLoopBlock,
@ -561,7 +586,8 @@ const automationActions = (store: AutomationStore) => ({
isLoopBlock, isLoopBlock,
runtimeBinding, runtimeBinding,
categoryName, categoryName,
bindingName bindingName,
readableBinding
) )
) )
} }
@ -636,7 +662,15 @@ const automationActions = (store: AutomationStore) => ({
} }
} }
Object.entries(schema).forEach(([name, value]) => { Object.entries(schema).forEach(([name, value]) => {
addBinding(name, value, icon, blockIdx, isLoopBlock, bindingName) addBinding(
name,
value,
icon,
blockIdx,
isLoopBlock,
pathBlock,
bindingName
)
}) })
} }
@ -647,23 +681,60 @@ const automationActions = (store: AutomationStore) => ({
return bindings return bindings
}, },
determineReadableBinding: (
name: string,
block: AutomationStep | AutomationTrigger
) => {
const rowTriggers = [
TriggerStepID.ROW_UPDATED,
TriggerStepID.ROW_SAVED,
TriggerStepID.ROW_DELETED,
TriggerStepID.ROW_ACTION,
]
const isTrigger = block.type === AutomationStepType.TRIGGER
const isAppTrigger = block.stepId === AutomationTriggerStepId.APP
const isRowTrigger = rowTriggers.includes(block.stepId)
let readableBinding: string = ""
if (isTrigger) {
if (isAppTrigger) {
readableBinding = `trigger.fields.${name}`
} else if (isRowTrigger) {
let noRowKeywordBindings = ["id", "revision", "oldRow"]
readableBinding = noRowKeywordBindings.includes(name)
? `trigger.${name}`
: `trigger.row.${name}`
} else {
readableBinding = `trigger.${name}`
}
}
return readableBinding
},
determineRuntimeBinding: ( determineRuntimeBinding: (
name: string, name: string,
idx: number, idx: number,
isLoopBlock: boolean, isLoopBlock: boolean,
bindingName: string | undefined,
automation: Automation, automation: Automation,
currentBlock: AutomationStep | AutomationTrigger | undefined, currentBlock: AutomationStep | AutomationTrigger | undefined,
pathSteps: (AutomationStep | AutomationTrigger)[] pathSteps: (AutomationStep | AutomationTrigger)[]
) => { ) => {
let runtimeName: string | null let runtimeName: string | null
// Legacy support for EXECUTE_SCRIPT steps
const isJSScript =
currentBlock?.stepId === AutomationActionStepId.EXECUTE_SCRIPT
/* Begin special cases for generating custom schemas based on triggers */ /* Begin special cases for generating custom schemas based on triggers */
if ( if (
idx === 0 && idx === 0 &&
automation.definition.trigger?.event === AutomationEventType.APP_TRIGGER automation.definition.trigger?.event === AutomationEventType.APP_TRIGGER
) { ) {
return `trigger.fields.${name}` return isJSScript
? `trigger.fields["${name}"]`
: `trigger.fields.[${name}]`
} }
if ( if (
@ -673,18 +744,17 @@ const automationActions = (store: AutomationStore) => ({
automation.definition.trigger?.event === AutomationEventType.ROW_SAVE) automation.definition.trigger?.event === AutomationEventType.ROW_SAVE)
) { ) {
let noRowKeywordBindings = ["id", "revision", "oldRow"] let noRowKeywordBindings = ["id", "revision", "oldRow"]
if (!noRowKeywordBindings.includes(name)) return `trigger.row.${name}` if (!noRowKeywordBindings.includes(name)) {
return isJSScript ? `trigger.row["${name}"]` : `trigger.row.[${name}]`
}
} }
/* End special cases for generating custom schemas based on triggers */ /* End special cases for generating custom schemas based on triggers */
if (isLoopBlock) { if (isLoopBlock) {
runtimeName = `loop.${name}` runtimeName = `loop.${name}`
} else if (idx === 0) { } else if (idx === 0) {
runtimeName = `trigger.${name}` runtimeName = `trigger.[${name}]`
} else if ( } else if (isJSScript) {
currentBlock?.stepId === AutomationActionStepId.EXECUTE_SCRIPT ||
currentBlock?.stepId === AutomationActionStepId.EXECUTE_SCRIPT_V2
) {
const stepId = pathSteps[idx].id const stepId = pathSteps[idx].id
if (!stepId) { if (!stepId) {
notifications.error("Error generating binding: Step ID not found.") notifications.error("Error generating binding: Step ID not found.")
@ -725,18 +795,22 @@ const automationActions = (store: AutomationStore) => ({
isLoopBlock: boolean, isLoopBlock: boolean,
runtimeBinding: string | null, runtimeBinding: string | null,
categoryName: string, categoryName: string,
bindingName?: string bindingName?: string,
readableBinding?: string
) => { ) => {
const field = Object.values(FIELDS).find( const field = Object.values(FIELDS).find(
field => field =>
field.type === value.type && field.type === value.type &&
("subtype" in field ? field.subtype === value.subtype : true) ("subtype" in field ? field.subtype === value.subtype : true)
) )
const readableBindingDefault =
bindingName && !isLoopBlock && idx !== 0
? `steps.${bindingName}.${name}`
: runtimeBinding
return { return {
readableBinding: readableBinding: readableBinding || readableBindingDefault,
bindingName && !isLoopBlock && idx !== 0
? `steps.${bindingName}.${name}`
: runtimeBinding,
runtimeBinding, runtimeBinding,
type: value.type, type: value.type,
description: value.description, description: value.description,
@ -801,7 +875,7 @@ const automationActions = (store: AutomationStore) => ({
}, },
test: async (automation: Automation, testData: any) => { test: async (automation: Automation, testData: any) => {
let result: any let result: TestAutomationResponse
try { try {
result = await API.testAutomation(automation._id!, testData) result = await API.testAutomation(automation._id!, testData)
} catch (err: any) { } catch (err: any) {
@ -1395,7 +1469,7 @@ const automationActions = (store: AutomationStore) => ({
} }
store.update(state => { store.update(state => {
state.selectedAutomationId = id state.selectedAutomationId = id
state.testResults = null delete state.testResults
state.showTestPanel = false state.showTestPanel = false
return state return state
}) })
@ -1521,3 +1595,86 @@ export class SelectedAutomationStore extends DerivedBudiStore<
} }
} }
export const selectedAutomation = new SelectedAutomationStore(automationStore) export const selectedAutomation = new SelectedAutomationStore(automationStore)
type TriggerContext = AutomationTrigger & {
meta?: Record<any, any>
table?: Table
body?: Record<any, any>
row?: Record<any, any>
oldRow?: Record<any, any>
outputs?: Record<any, any>
}
export interface AutomationContext {
user: AppSelfResponse
trigger: TriggerContext
steps: Record<string, AutomationStep>
env: Record<string, any>
settings: Record<string, any>
}
export const evaluationContext: Readable<AutomationContext> = derived(
[organisation, selectedAutomation, environment, tables],
([$organisation, $selectedAutomation, $env, $tables]) => {
const { platformUrl: url, company, logoUrl: logo } = $organisation
const results: TestAutomationResponse | undefined =
$selectedAutomation?.testResults
const testData = $selectedAutomation.data?.testData || {}
const triggerDef = $selectedAutomation.data?.definition?.trigger
const isWebhook = triggerDef?.stepId! === TriggerStepID.WEBHOOK
const isRowAction = triggerDef?.stepId! === TriggerStepID.ROW_ACTION
const rowActionTableId = triggerDef?.inputs?.tableId
const rowActionTable = rowActionTableId
? $tables.list.find(table => table._id === rowActionTableId)
: undefined
// Needs a clone to avoid state mutation.
const triggerData: TriggerContext = cloneDeep(
results?.trigger?.outputs || testData
)
if (isRowAction && rowActionTable) {
// Row action table must always be retrieved as it is never
// returned in the test results
triggerData.table = rowActionTable
} else if (isWebhook) {
// Ensure it displays in the event that the configuration have been skipped
triggerData.body = triggerData.body ?? {}
}
// Clean up unnecessary data from the context
// Meta contains UI/UX config data. Non-bindable
delete triggerData?.meta
// AppSelf context required to mirror server user context
const userContext = $selectedAutomation.appSelf || {}
return {
user: userContext,
trigger: {
...triggerData,
},
// This will initially be empty for each step but will populate
// upon running the test.
steps: (results?.steps || []).reduce(
(acc: Record<string, any>, res: Record<string, any>) => {
acc[res.id] = res.outputs
return acc
},
{}
),
env: ($env?.variables || []).reduce(
(acc: Record<string, any>, variable: Record<string, any>) => {
acc[variable.name] = ""
return acc
},
{}
),
settings: { url, company, logo },
}
}
)

View File

@ -11,6 +11,7 @@ import {
automationStore, automationStore,
selectedAutomation, selectedAutomation,
automationHistoryStore, automationHistoryStore,
evaluationContext,
} from "./automations.js" } from "./automations.js"
import { userStore, userSelectedResourceMap, isOnlyUser } from "./users.js" import { userStore, userSelectedResourceMap, isOnlyUser } from "./users.js"
import { deploymentStore } from "./deployments.js" import { deploymentStore } from "./deployments.js"
@ -67,6 +68,7 @@ export {
snippets, snippets,
rowActions, rowActions,
appPublished, appPublished,
evaluationContext,
} }
export const reset = () => { export const reset = () => {

View File

@ -10,6 +10,7 @@
export let drawerTitle export let drawerTitle
export let toReadable export let toReadable
export let toRuntime export let toRuntime
export let evaluationContext = {}
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -66,7 +67,6 @@
> >
Confirm Confirm
</Button> </Button>
<svelte:component <svelte:component
this={panel} this={panel}
slot="body" slot="body"
@ -76,6 +76,7 @@
allowHBS allowHBS
on:change={drawerOnChange} on:change={drawerOnChange}
{bindings} {bindings}
context={evaluationContext}
/> />
</Drawer> </Drawer>

View File

@ -42,6 +42,7 @@
export let panel export let panel
export let toReadable export let toReadable
export let toRuntime export let toRuntime
export let evaluationContext = {}
$: editableFilters = migrateFilters(filters) $: editableFilters = migrateFilters(filters)
$: { $: {
@ -385,6 +386,7 @@
{panel} {panel}
{toReadable} {toReadable}
{toRuntime} {toRuntime}
{evaluationContext}
on:change={e => { on:change={e => {
const updated = { const updated = {
...filter, ...filter,
@ -423,6 +425,7 @@
{panel} {panel}
{toReadable} {toReadable}
{toRuntime} {toRuntime}
{evaluationContext}
on:change={e => { on:change={e => {
onFilterFieldUpdate( onFilterFieldUpdate(
{ ...filter, ...e.detail }, { ...filter, ...e.detail },

View File

@ -24,6 +24,7 @@
export let drawerTitle export let drawerTitle
export let toReadable export let toReadable
export let toRuntime export let toRuntime
export let evaluationContext = {}
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const { OperatorOptions, FilterValueType } = Constants const { OperatorOptions, FilterValueType } = Constants
@ -156,6 +157,7 @@
allowHBS allowHBS
on:change={drawerOnChange} on:change={drawerOnChange}
{bindings} {bindings}
context={evaluationContext}
/> />
</Drawer> </Drawer>

View File

@ -21,6 +21,7 @@ import {
AutomationRowEvent, AutomationRowEvent,
UserBindings, UserBindings,
AutomationResults, AutomationResults,
DidNotTriggerResponse,
} from "@budibase/types" } from "@budibase/types"
import { executeInThread } from "../threads/automation" import { executeInThread } from "../threads/automation"
import { dataFilters, sdk } from "@budibase/shared-core" import { dataFilters, sdk } from "@budibase/shared-core"
@ -33,14 +34,6 @@ const JOB_OPTS = {
import * as automationUtils from "../automations/automationUtils" import * as automationUtils from "../automations/automationUtils"
import { doesTableExist } from "../sdk/app/tables/getters" import { doesTableExist } from "../sdk/app/tables/getters"
type DidNotTriggerResponse = {
outputs: {
success: false
status: AutomationStatus.STOPPED
}
message: AutomationStoppedReason.TRIGGER_FILTER_NOT_MET
}
async function getAllAutomations() { async function getAllAutomations() {
const db = context.getAppDB() const db = context.getAppDB()
let automations = await db.allDocs<Automation>( let automations = await db.allDocs<Automation>(

View File

@ -1,7 +1,9 @@
import { AutomationJob, DidNotTriggerResponse } from "../../../sdk/automations"
import { import {
Automation, Automation,
AutomationActionStepId, AutomationActionStepId,
AutomationLogPage, AutomationLogPage,
AutomationResults,
AutomationStatus, AutomationStatus,
AutomationStepDefinition, AutomationStepDefinition,
AutomationTriggerDefinition, AutomationTriggerDefinition,
@ -74,4 +76,8 @@ export interface TestAutomationRequest {
fields: Record<string, any> fields: Record<string, any>
row?: Row row?: Row
} }
export interface TestAutomationResponse {}
export type TestAutomationResponse =
| AutomationResults
| DidNotTriggerResponse
| AutomationJob

View File

@ -5,6 +5,7 @@ import { ReadStream } from "fs"
import { Row } from "../row" import { Row } from "../row"
import { Table } from "../table" import { Table } from "../table"
import { AutomationStep, AutomationTrigger } from "./schema" import { AutomationStep, AutomationTrigger } from "./schema"
import { StepOutputs, TriggerOutputs } from "./StepInputsOutputs"
export enum AutomationIOType { export enum AutomationIOType {
OBJECT = "object", OBJECT = "object",
@ -194,7 +195,7 @@ export enum AutomationStoppedReason {
export interface AutomationResults { export interface AutomationResults {
automationId?: string automationId?: string
status?: AutomationStatus status?: AutomationStatus
trigger?: AutomationTrigger trigger?: AutomationTrigger & { outputs: TriggerOutputs }
steps: { steps: {
stepId: AutomationTriggerStepId | AutomationActionStepId stepId: AutomationTriggerStepId | AutomationActionStepId
inputs: { inputs: {

View File

@ -1,6 +1,8 @@
import { import {
Automation, Automation,
AutomationMetadata, AutomationMetadata,
AutomationStatus,
AutomationStoppedReason,
Row, Row,
UserBindings, UserBindings,
} from "../../documents" } from "../../documents"
@ -29,3 +31,11 @@ export interface AutomationRowEvent {
} }
export type AutomationJob = Job<AutomationData> export type AutomationJob = Job<AutomationData>
export type DidNotTriggerResponse = {
outputs: {
success: false
status: AutomationStatus.STOPPED
}
message: AutomationStoppedReason.TRIGGER_FILTER_NOT_MET
}