Merge commit
This commit is contained in:
parent
3f35a41046
commit
3a10c57651
|
@ -8,10 +8,41 @@
|
|||
import { automationStore, selectedAutomation } from "stores/builder"
|
||||
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import { memo } from "@budibase/frontend-core"
|
||||
import { AutomationEventType } from "@budibase/types"
|
||||
|
||||
let failedParse = null
|
||||
let trigger = {}
|
||||
let schemaProperties = {}
|
||||
let baseData = {}
|
||||
|
||||
let rowEvents = [
|
||||
AutomationEventType.ROW_DELETE,
|
||||
AutomationEventType.ROW_SAVE,
|
||||
AutomationEventType.ROW_UPDATE,
|
||||
]
|
||||
|
||||
const memoTestData = memo($selectedAutomation.testData)
|
||||
$: memoTestData.set($selectedAutomation.testData)
|
||||
|
||||
$: if (memoTestData) {
|
||||
baseData = cloneDeep($selectedAutomation.testData)
|
||||
// Reset the test data for row trigger data when the table is changed.
|
||||
if (rowEvents.includes(trigger?.event)) {
|
||||
if (
|
||||
!baseData?.row?.tableId ||
|
||||
baseData.row.tableId !== trigger.inputs?.tableId
|
||||
) {
|
||||
baseData = {
|
||||
...baseData,
|
||||
_tableId: trigger.inputs?.tableId,
|
||||
row: { tableId: trigger.inputs?.tableId },
|
||||
meta: {},
|
||||
id: "",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
// clone the trigger so we're not mutating the reference
|
||||
|
@ -20,19 +51,15 @@
|
|||
// get the outputs so we can define the fields
|
||||
let schema = Object.entries(trigger.schema?.outputs?.properties || {})
|
||||
|
||||
if (trigger?.event === "app:trigger") {
|
||||
if (trigger?.event === AutomationEventType.APP_TRIGGER) {
|
||||
schema = [["fields", { customType: "fields" }]]
|
||||
}
|
||||
|
||||
schemaProperties = schema
|
||||
}
|
||||
|
||||
// check to see if there is existing test data in the store
|
||||
$: testData = $selectedAutomation.testData || {}
|
||||
|
||||
// Check the schema to see if required fields have been entered
|
||||
$: isError = !trigger.schema.outputs.required.every(
|
||||
required => testData[required] || required !== "row"
|
||||
required => baseData?.[required] || required !== "row"
|
||||
)
|
||||
|
||||
function parseTestJSON(e) {
|
||||
|
@ -47,7 +74,7 @@
|
|||
|
||||
const testAutomation = async () => {
|
||||
try {
|
||||
await automationStore.actions.test($selectedAutomation, testData)
|
||||
await automationStore.actions.test($selectedAutomation, baseData)
|
||||
$automationStore.showTestPanel = true
|
||||
} catch (error) {
|
||||
notifications.error(error)
|
||||
|
@ -85,7 +112,7 @@
|
|||
{#if selectedValues}
|
||||
<div class="tab-content-padding">
|
||||
<AutomationBlockSetup
|
||||
{testData}
|
||||
testData={baseData}
|
||||
{schemaProperties}
|
||||
isTestModal
|
||||
block={trigger}
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
<script>
|
||||
import TableSelector from "./TableSelector.svelte"
|
||||
import RowSelector from "./RowSelector.svelte"
|
||||
import FieldSelector from "./FieldSelector.svelte"
|
||||
import SchemaSetup from "./SchemaSetup.svelte"
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Select,
|
||||
Label,
|
||||
ActionButton,
|
||||
|
@ -15,23 +13,25 @@
|
|||
Checkbox,
|
||||
DatePicker,
|
||||
DrawerContent,
|
||||
Helpers,
|
||||
} 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 DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
|
||||
import DrawerBindableSlot from "../../common/bindings/DrawerBindableSlot.svelte"
|
||||
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
|
||||
import {
|
||||
BindingSidePanel,
|
||||
DrawerBindableSlot,
|
||||
DrawerBindableInput,
|
||||
ServerBindingPanel as AutomationBindingPanel,
|
||||
ModalBindableInput,
|
||||
} from "components/common/bindings"
|
||||
import CodeEditorModal from "./CodeEditorModal.svelte"
|
||||
import QuerySelector from "./QuerySelector.svelte"
|
||||
import QueryParamSelector from "./QueryParamSelector.svelte"
|
||||
import AutomationSelector from "./AutomationSelector.svelte"
|
||||
import CronBuilder from "./CronBuilder.svelte"
|
||||
import Editor from "components/integration/QueryEditor.svelte"
|
||||
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
|
||||
import CodeEditor from "components/common/CodeEditor/CodeEditor.svelte"
|
||||
import BindingSidePanel from "components/common/bindings/BindingSidePanel.svelte"
|
||||
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
|
||||
import { BindingHelpers, BindingType } from "components/common/bindings/utils"
|
||||
import {
|
||||
|
@ -40,7 +40,7 @@
|
|||
EditorModes,
|
||||
} from "components/common/CodeEditor"
|
||||
import FilterBuilder from "components/design/settings/controls/FilterEditor/FilterBuilder.svelte"
|
||||
import { LuceneUtils, Utils } from "@budibase/frontend-core"
|
||||
import { LuceneUtils, Utils, memo } from "@budibase/frontend-core"
|
||||
import {
|
||||
getSchemaForDatasourcePlus,
|
||||
getEnvironmentBindings,
|
||||
|
@ -48,23 +48,37 @@
|
|||
import { TriggerStepID, ActionStepID } from "constants/backend/automations"
|
||||
import { onMount } from "svelte"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import { AutomationEventType } from "@budibase/types"
|
||||
import { FIELDS } from "constants/backend"
|
||||
import PropField from "./PropField.svelte"
|
||||
|
||||
export let block
|
||||
export let testData
|
||||
export let schemaProperties
|
||||
export let isTestModal = false
|
||||
|
||||
// Stop unnecessary rendering
|
||||
const memoBlock = memo(block)
|
||||
|
||||
const rowTriggers = [
|
||||
TriggerStepID.ROW_UPDATED,
|
||||
TriggerStepID.ROW_SAVED,
|
||||
TriggerStepID.ROW_DELETED,
|
||||
]
|
||||
const rowSteps = [ActionStepID.UPDATE_ROW, ActionStepID.CREATE_ROW]
|
||||
|
||||
let webhookModal
|
||||
let drawer
|
||||
let inputData
|
||||
let insertAtPos, getCaretPosition
|
||||
let stepLayouts = {}
|
||||
|
||||
$: memoBlock.set(block)
|
||||
$: filters = lookForFilters(schemaProperties) || []
|
||||
$: tempFilters = filters
|
||||
$: stepId = block.stepId
|
||||
$: bindings = getAvailableBindings(block, $selectedAutomation?.definition)
|
||||
$: getInputData(testData, block.inputs)
|
||||
$: getInputData(testData, $memoBlock.inputs)
|
||||
$: tableId = inputData ? inputData.tableId : null
|
||||
$: table = tableId
|
||||
? $tables.list.find(table => table._id === inputData.tableId)
|
||||
|
@ -75,14 +89,15 @@
|
|||
$: schemaFields = Object.values(schema || {})
|
||||
$: queryLimit = tableId?.includes("datasource") ? "∞" : "1000"
|
||||
$: isTrigger = block?.type === "TRIGGER"
|
||||
$: isUpdateRow = stepId === ActionStepID.UPDATE_ROW
|
||||
$: codeMode =
|
||||
stepId === "EXECUTE_BASH" ? EditorModes.Handlebars : EditorModes.JS
|
||||
$: bindingsHelpers = new BindingHelpers(getCaretPosition, insertAtPos, {
|
||||
disableWrapping: true,
|
||||
})
|
||||
$: editingJs = codeMode === EditorModes.JS
|
||||
$: requiredProperties = block.schema.inputs.required || []
|
||||
$: requiredProperties =
|
||||
block.schema[isTestModal ? "outputs" : "inputs"].required || []
|
||||
|
||||
$: stepCompletions =
|
||||
codeMode === EditorModes.Handlebars
|
||||
? [hbAutocomplete([...bindingsToCompletions(bindings, codeMode)])]
|
||||
|
@ -93,10 +108,12 @@
|
|||
let newInputData = testData || cloneDeep(blockInputs)
|
||||
|
||||
// Ensures the app action fields are populated
|
||||
if (block.event === "app:trigger" && !newInputData?.fields) {
|
||||
if (
|
||||
block.event === AutomationEventType.APP_TRIGGER &&
|
||||
!newInputData?.fields
|
||||
) {
|
||||
newInputData = cloneDeep(blockInputs)
|
||||
}
|
||||
|
||||
inputData = newInputData
|
||||
setDefaultEnumValues()
|
||||
}
|
||||
|
@ -108,15 +125,136 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
const onChange = Utils.sequential(async (e, key) => {
|
||||
|
||||
$: customStepLayouts($memoBlock, schemaProperties)
|
||||
|
||||
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,
|
||||
panel: AutomationBindingPanel,
|
||||
value: inputData["revision"],
|
||||
onChange: e => {
|
||||
onChange({ ["revision"]: e.detail })
|
||||
},
|
||||
bindings,
|
||||
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 })
|
||||
},
|
||||
bindings,
|
||||
updateOnChange: false,
|
||||
forceModal: true,
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
stepLayouts[block.stepId] = {
|
||||
row: {
|
||||
schema: schema["row"],
|
||||
//?layout: RowLayoutStepComponent.
|
||||
content: [
|
||||
{
|
||||
type: TableSelector,
|
||||
title: "Table",
|
||||
props: {
|
||||
isTrigger,
|
||||
value: inputData["row"]?.tableId ?? "",
|
||||
onChange: e => {
|
||||
onChange({
|
||||
_tableId: e.detail,
|
||||
meta: {},
|
||||
["row"]: {
|
||||
tableId: e.detail,
|
||||
},
|
||||
})
|
||||
},
|
||||
disabled: isTestModal,
|
||||
},
|
||||
},
|
||||
...getIdConfig(),
|
||||
...getRevConfig(),
|
||||
{
|
||||
type: Divider,
|
||||
props: {
|
||||
noMargin: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: RowSelector,
|
||||
props: {
|
||||
row: inputData["row"],
|
||||
meta: inputData["meta"] || {},
|
||||
onChange: e => {
|
||||
onChange(e.detail)
|
||||
},
|
||||
bindings,
|
||||
isTestModal,
|
||||
isUpdateRow: block.stepId === ActionStepID.UPDATE_ROW,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onChange = Utils.sequential(async update => {
|
||||
// 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.
|
||||
const request = cloneDeep(update)
|
||||
|
||||
let schema
|
||||
if (e.detail?.tableId) {
|
||||
schema = getSchemaForDatasourcePlus(e.detail.tableId, {
|
||||
if (request?._tableId) {
|
||||
schema = getSchemaForDatasourcePlus(request._tableId, {
|
||||
searchableSchema: true,
|
||||
}).schema
|
||||
delete request._tableId
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -128,21 +266,22 @@
|
|||
newTestData = {
|
||||
...newTestData,
|
||||
body: {
|
||||
[key]: e.detail,
|
||||
...update,
|
||||
...$selectedAutomation.testData?.body,
|
||||
},
|
||||
}
|
||||
}
|
||||
newTestData = {
|
||||
...newTestData,
|
||||
[key]: e.detail,
|
||||
...request,
|
||||
}
|
||||
await automationStore.actions.addTestDataToAutomation(newTestData)
|
||||
} else {
|
||||
const data = { schema, [key]: e.detail }
|
||||
const data = { schema, ...request }
|
||||
await automationStore.actions.updateBlockInputs(block, data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error saving automation", error)
|
||||
notifications.error("Error saving automation")
|
||||
}
|
||||
})
|
||||
|
@ -187,14 +326,17 @@
|
|||
let runtimeName
|
||||
|
||||
/* Begin special cases for generating custom schemas based on triggers */
|
||||
if (idx === 0 && automation.trigger?.event === "app:trigger") {
|
||||
if (
|
||||
idx === 0 &&
|
||||
automation.trigger?.event === AutomationEventType.APP_TRIGGER
|
||||
) {
|
||||
return `trigger.fields.${name}`
|
||||
}
|
||||
|
||||
if (
|
||||
idx === 0 &&
|
||||
(automation.trigger?.event === "row:update" ||
|
||||
automation.trigger?.event === "row:save")
|
||||
(automation.trigger?.event === AutomationEventType.ROW_UPDATE ||
|
||||
automation.trigger?.event === AutomationEventType.ROW_SAVE)
|
||||
) {
|
||||
if (name !== "id" && name !== "revision") return `trigger.row.${name}`
|
||||
}
|
||||
|
@ -268,7 +410,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
if (idx === 0 && automation.trigger?.event === "app:trigger") {
|
||||
if (
|
||||
idx === 0 &&
|
||||
automation.trigger?.event === AutomationEventType.APP_TRIGGER
|
||||
) {
|
||||
schema = Object.fromEntries(
|
||||
Object.keys(automation.trigger.inputs.fields || []).map(key => [
|
||||
key,
|
||||
|
@ -277,8 +422,9 @@
|
|||
)
|
||||
}
|
||||
if (
|
||||
(idx === 0 && automation.trigger.event === "row:update") ||
|
||||
(idx === 0 && automation.trigger.event === "row:save")
|
||||
(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
|
||||
|
@ -344,10 +490,12 @@
|
|||
|
||||
function saveFilters(key) {
|
||||
const filters = LuceneUtils.buildLuceneQuery(tempFilters)
|
||||
const defKey = `${key}-def`
|
||||
onChange({ detail: filters }, key)
|
||||
// need to store the builder definition in the automation
|
||||
onChange({ detail: tempFilters }, defKey)
|
||||
|
||||
onChange({
|
||||
[key]: filters,
|
||||
[`${key}-def`]: tempFilters, // need to store the builder definition in the automation
|
||||
})
|
||||
|
||||
drawer.hide()
|
||||
}
|
||||
|
||||
|
@ -364,6 +512,7 @@
|
|||
value.customType !== "cron" &&
|
||||
value.customType !== "triggerSchema" &&
|
||||
value.customType !== "automationFields" &&
|
||||
value.customType !== "fields" &&
|
||||
value.type !== "signature_single" &&
|
||||
value.type !== "attachment" &&
|
||||
value.type !== "attachment_single"
|
||||
|
@ -372,7 +521,10 @@
|
|||
|
||||
function getFieldLabel(key, value) {
|
||||
const requiredSuffix = requiredProperties.includes(key) ? "*" : ""
|
||||
return `${value.title || (key === "row" ? "Table" : key)} ${requiredSuffix}`
|
||||
const label = `${
|
||||
value.title || (key === "row" ? "Table" : key)
|
||||
} ${requiredSuffix}`
|
||||
return Helpers.capitalise(label)
|
||||
}
|
||||
|
||||
function handleAttachmentParams(keyValueObj) {
|
||||
|
@ -394,293 +546,269 @@
|
|||
})
|
||||
</script>
|
||||
|
||||
<div class="fields">
|
||||
{#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)}
|
||||
<Label
|
||||
tooltip={value.title === "Binding / Value"
|
||||
? "If using the String input type, please use a comma or newline separated string"
|
||||
: null}>{label}</Label
|
||||
>
|
||||
{/if}
|
||||
<div class:field-width={shouldRenderField(value)}>
|
||||
{#if value.type === "string" && value.enum && canShowField(key, value)}
|
||||
<Select
|
||||
on:change={e => onChange(e, key)}
|
||||
value={inputData[key]}
|
||||
placeholder={false}
|
||||
options={value.enum}
|
||||
getOptionLabel={(x, idx) =>
|
||||
value.pretty ? value.pretty[idx] : x}
|
||||
/>
|
||||
{:else if value.type === "json"}
|
||||
<Editor
|
||||
editorHeight="250"
|
||||
editorWidth="448"
|
||||
mode="json"
|
||||
value={inputData[key]?.value}
|
||||
on:change={e => {
|
||||
onChange(e, key)
|
||||
}}
|
||||
/>
|
||||
{:else if value.type === "boolean"}
|
||||
<div style="margin-top: 10px">
|
||||
<Checkbox
|
||||
text={value.title}
|
||||
value={inputData[key]}
|
||||
on:change={e => onChange(e, key)}
|
||||
<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}>
|
||||
<svelte:component
|
||||
this={config.type}
|
||||
{...config.props}
|
||||
on:change={config.props.onChange}
|
||||
/>
|
||||
</div>
|
||||
{:else if value.type === "date"}
|
||||
<DrawerBindableSlot
|
||||
title={value.title ?? label}
|
||||
panel={AutomationBindingPanel}
|
||||
type={"date"}
|
||||
value={inputData[key]}
|
||||
on:change={e => onChange(e, key)}
|
||||
{bindings}
|
||||
allowJS={true}
|
||||
updateOnChange={false}
|
||||
drawerLeft="260px"
|
||||
</PropField>
|
||||
{:else}
|
||||
<svelte:component
|
||||
this={config.type}
|
||||
{...config.props}
|
||||
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)}
|
||||
<Label
|
||||
tooltip={value.title === "Binding / Value"
|
||||
? "If using the String input type, please use a comma or newline separated string"
|
||||
: null}
|
||||
>
|
||||
<DatePicker
|
||||
{label}
|
||||
</Label>
|
||||
{/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]}
|
||||
on:change={e => onChange(e, key)}
|
||||
placeholder={false}
|
||||
options={value.enum}
|
||||
getOptionLabel={(x, idx) =>
|
||||
value.pretty ? value.pretty[idx] : x}
|
||||
/>
|
||||
</DrawerBindableSlot>
|
||||
{:else if value.customType === "column"}
|
||||
<Select
|
||||
on:change={e => onChange(e, key)}
|
||||
value={inputData[key]}
|
||||
options={Object.keys(table?.schema || {})}
|
||||
/>
|
||||
{:else if value.type === "attachment" || value.type === "signature_single"}
|
||||
<div class="attachment-field-wrapper">
|
||||
<div class="label-wrapper">
|
||||
<Label>{label}</Label>
|
||||
{:else if value.type === "json"}
|
||||
<Editor
|
||||
editorHeight="250"
|
||||
editorWidth="448"
|
||||
mode="json"
|
||||
value={inputData[key]?.value}
|
||||
on:change={e => onChange({ [key]: e.detail })}
|
||||
/>
|
||||
{:else if value.type === "boolean"}
|
||||
<div style="margin-top: 10px">
|
||||
<Checkbox
|
||||
text={value.title}
|
||||
value={inputData[key]}
|
||||
on:change={e => onChange({ [key]: e.detail })}
|
||||
/>
|
||||
</div>
|
||||
<div class="attachment-field-width">
|
||||
<KeyValueBuilder
|
||||
on:change={e =>
|
||||
onChange(
|
||||
{
|
||||
detail: e.detail.map(({ name, value }) => ({
|
||||
{: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"
|
||||
>
|
||||
<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 || {})}
|
||||
/>
|
||||
{:else if value.type === "attachment" || value.type === "signature_single"}
|
||||
<div class="attachment-field-wrapper">
|
||||
<div class="label-wrapper">
|
||||
<Label>{label}</Label>
|
||||
</div>
|
||||
<div class="attachment-field-width">
|
||||
<KeyValueBuilder
|
||||
on:change={e =>
|
||||
onChange({
|
||||
[key]: e.detail.map(({ name, value }) => ({
|
||||
url: name,
|
||||
filename: value,
|
||||
})),
|
||||
},
|
||||
key
|
||||
)}
|
||||
object={handleAttachmentParams(inputData[key])}
|
||||
allowJS
|
||||
{bindings}
|
||||
keyBindings
|
||||
customButtonText={"Add attachment"}
|
||||
keyPlaceholder={"URL"}
|
||||
valuePlaceholder={"Filename"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{:else if value.customType === "filters"}
|
||||
<ActionButton on:click={drawer.show}>Define filters</ActionButton>
|
||||
<Drawer bind:this={drawer} title="Filtering">
|
||||
<Button cta slot="buttons" on:click={() => saveFilters(key)}>
|
||||
Save
|
||||
</Button>
|
||||
<DrawerContent slot="body">
|
||||
<FilterBuilder
|
||||
{filters}
|
||||
{bindings}
|
||||
{schemaFields}
|
||||
datasource={{ type: "table", tableId }}
|
||||
panel={AutomationBindingPanel}
|
||||
on:change={e => (tempFilters = e.detail)}
|
||||
/>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
{:else if value.customType === "password"}
|
||||
<Input
|
||||
type="password"
|
||||
on:change={e => onChange(e, key)}
|
||||
value={inputData[key]}
|
||||
/>
|
||||
{:else if value.customType === "email"}
|
||||
{#if isTestModal}
|
||||
<ModalBindableInput
|
||||
title={value.title ?? label}
|
||||
value={inputData[key]}
|
||||
panel={AutomationBindingPanel}
|
||||
type="email"
|
||||
on:change={e => onChange(e, key)}
|
||||
{bindings}
|
||||
updateOnChange={false}
|
||||
/>
|
||||
{:else}
|
||||
<DrawerBindableInput
|
||||
title={value.title ?? label}
|
||||
panel={AutomationBindingPanel}
|
||||
type="email"
|
||||
value={inputData[key]}
|
||||
on:change={e => onChange(e, key)}
|
||||
{bindings}
|
||||
allowJS={false}
|
||||
updateOnChange={false}
|
||||
drawerLeft="260px"
|
||||
/>
|
||||
{/if}
|
||||
{:else if value.customType === "query"}
|
||||
<QuerySelector
|
||||
on:change={e => onChange(e, key)}
|
||||
value={inputData[key]}
|
||||
/>
|
||||
{:else if value.customType === "cron"}
|
||||
<CronBuilder
|
||||
on:change={e => onChange(e, key)}
|
||||
value={inputData[key]}
|
||||
/>
|
||||
{:else if value.customType === "automationFields"}
|
||||
<AutomationSelector
|
||||
on:change={e => onChange(e, key)}
|
||||
value={inputData[key]}
|
||||
{bindings}
|
||||
/>
|
||||
{:else if value.customType === "queryParams"}
|
||||
<QueryParamSelector
|
||||
on:change={e => onChange(e, key)}
|
||||
value={inputData[key]}
|
||||
{bindings}
|
||||
/>
|
||||
{:else if value.customType === "table"}
|
||||
<TableSelector
|
||||
{isTrigger}
|
||||
value={inputData[key]}
|
||||
on:change={e => onChange(e, key)}
|
||||
/>
|
||||
{:else if value.customType === "row"}
|
||||
<RowSelector
|
||||
value={inputData[key]}
|
||||
meta={inputData["meta"] || {}}
|
||||
on:change={e => {
|
||||
if (e.detail?.key) {
|
||||
onChange(e, e.detail.key)
|
||||
} else {
|
||||
onChange(e, key)
|
||||
}
|
||||
}}
|
||||
{bindings}
|
||||
{isTestModal}
|
||||
{isUpdateRow}
|
||||
/>
|
||||
{:else if value.customType === "webhookUrl"}
|
||||
<WebhookDisplay
|
||||
on:change={e => onChange(e, key)}
|
||||
value={inputData[key]}
|
||||
/>
|
||||
{:else if value.customType === "fields"}
|
||||
<FieldSelector
|
||||
{block}
|
||||
value={inputData[key]}
|
||||
on:change={e => onChange(e, key)}
|
||||
{bindings}
|
||||
{isTestModal}
|
||||
/>
|
||||
{:else if value.customType === "triggerSchema"}
|
||||
<SchemaSetup
|
||||
on:change={e => onChange(e, key)}
|
||||
value={inputData[key]}
|
||||
/>
|
||||
{:else if value.customType === "code"}
|
||||
<CodeEditorModal>
|
||||
<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
|
||||
onChange({ detail: e.detail }, key)
|
||||
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}
|
||||
})}
|
||||
object={handleAttachmentParams(inputData[key])}
|
||||
allowJS
|
||||
{bindings}
|
||||
keyBindings
|
||||
customButtonText={value.type === "attachment"
|
||||
? "Add attachment"
|
||||
: "Add signature"}
|
||||
keyPlaceholder={"URL"}
|
||||
valuePlaceholder={"Filename"}
|
||||
/>
|
||||
</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>
|
||||
{:else if value.customType === "filters"}
|
||||
<ActionButton on:click={drawer.show}>Define filters</ActionButton>
|
||||
<Drawer bind:this={drawer} title="Filtering">
|
||||
<Button cta slot="buttons" on:click={() => saveFilters(key)}>
|
||||
Save
|
||||
</Button>
|
||||
<DrawerContent slot="body">
|
||||
<FilterBuilder
|
||||
{filters}
|
||||
{bindings}
|
||||
{schemaFields}
|
||||
datasource={{ type: "table", tableId }}
|
||||
panel={AutomationBindingPanel}
|
||||
on:change={e => (tempFilters = e.detail)}
|
||||
/>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
{:else if value.customType === "cron"}
|
||||
<CronBuilder
|
||||
on:change={e => onChange({ [key]: e.detail })}
|
||||
value={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 })}
|
||||
/>
|
||||
{: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>
|
||||
<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
|
||||
onChange({ [key]: e.detail })
|
||||
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}
|
||||
</div>
|
||||
</CodeEditorModal>
|
||||
{:else if value.customType === "loopOption"}
|
||||
<Select
|
||||
on:change={e => onChange(e, key)}
|
||||
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}
|
||||
{#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]}
|
||||
panel={AutomationBindingPanel}
|
||||
type={value.customType}
|
||||
on:change={e => onChange(e, key)}
|
||||
{bindings}
|
||||
updateOnChange={false}
|
||||
options={["Array", "String"]}
|
||||
defaultValue={"Array"}
|
||||
/>
|
||||
{:else}
|
||||
<div class="test">
|
||||
<DrawerBindableInput
|
||||
title={value.title ?? label}
|
||||
{: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}
|
||||
value={inputData[key]}
|
||||
on:change={e => onChange(e, key)}
|
||||
on:change={e => onChange({ [key]: e.detail })}
|
||||
{bindings}
|
||||
updateOnChange={false}
|
||||
placeholder={value.customType === "queryLimit"
|
||||
? queryLimit
|
||||
: ""}
|
||||
drawerLeft="260px"
|
||||
/>
|
||||
</div>
|
||||
{: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}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<Modal bind:this={webhookModal} width="30%">
|
||||
<CreateWebhookModal />
|
||||
</Modal>
|
||||
|
||||
{#if stepId === TriggerStepID.WEBHOOK}
|
||||
{#if stepId === TriggerStepID.WEBHOOK && !isTestModal}
|
||||
<Button secondary on:click={() => webhookModal.show()}>Set Up Webhook</Button>
|
||||
{/if}
|
||||
|
||||
|
@ -689,12 +817,12 @@
|
|||
width: 320px;
|
||||
}
|
||||
|
||||
.fields {
|
||||
.step-fields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
gap: var(--spacing-s);
|
||||
gap: var(--spacing-l);
|
||||
}
|
||||
|
||||
.block-field {
|
||||
|
@ -714,10 +842,6 @@
|
|||
margin-top: var(--spacing-s);
|
||||
}
|
||||
|
||||
.test :global(.drawer) {
|
||||
width: 10000px !important;
|
||||
}
|
||||
|
||||
.js-editor {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import RowSelectorTypes from "./RowSelectorTypes.svelte"
|
||||
import PropField from "./PropField.svelte"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
|
@ -88,27 +89,26 @@
|
|||
</script>
|
||||
|
||||
{#if schemaFields.length && isTestModal}
|
||||
<div class="schema-fields">
|
||||
<div class="fields">
|
||||
{#each schemaFields as [field, schema]}
|
||||
<RowSelectorTypes
|
||||
{isTestModal}
|
||||
{field}
|
||||
{schema}
|
||||
{bindings}
|
||||
{value}
|
||||
{onChange}
|
||||
/>
|
||||
<PropField label={field}>
|
||||
<RowSelectorTypes
|
||||
{isTestModal}
|
||||
{field}
|
||||
{schema}
|
||||
{bindings}
|
||||
{value}
|
||||
{onChange}
|
||||
/>
|
||||
</PropField>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.schema-fields {
|
||||
display: grid;
|
||||
grid-gap: var(--spacing-s);
|
||||
margin-top: var(--spacing-s);
|
||||
}
|
||||
.schema-fields :global(label) {
|
||||
text-transform: capitalize;
|
||||
.fields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
<script>
|
||||
import { Label } from "@budibase/bbui"
|
||||
|
||||
export let label
|
||||
export let fullWidth = false
|
||||
</script>
|
||||
|
||||
<div class="prop-field" class:fullWidth>
|
||||
<div class="prop-label">
|
||||
<Label>{label}</Label>
|
||||
</div>
|
||||
<div class="prop-control">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.prop-field {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 320px;
|
||||
}
|
||||
|
||||
.prop-field.fullWidth {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.prop-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
|
@ -1,25 +1,34 @@
|
|||
<script>
|
||||
import { tables } from "stores/builder"
|
||||
import { Select, Checkbox, Label } from "@budibase/bbui"
|
||||
import {
|
||||
Label,
|
||||
ActionButton,
|
||||
Popover,
|
||||
Icon,
|
||||
TooltipPosition,
|
||||
TooltipType,
|
||||
} from "@budibase/bbui"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { FieldType } from "@budibase/types"
|
||||
|
||||
import RowSelectorTypes from "./RowSelectorTypes.svelte"
|
||||
import DrawerBindableSlot from "../../common/bindings/DrawerBindableSlot.svelte"
|
||||
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
|
||||
import { TableNames } from "constants"
|
||||
import { FIELDS } from "constants/backend"
|
||||
import { capitalise } from "helpers"
|
||||
import { memo } from "@budibase/frontend-core"
|
||||
import PropField from "./PropField.svelte"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let value
|
||||
export let row
|
||||
export let meta
|
||||
export let bindings
|
||||
export let isTestModal
|
||||
export let isUpdateRow
|
||||
$: parsedBindings = bindings.map(binding => {
|
||||
let clone = Object.assign({}, binding)
|
||||
clone.icon = "ShareAndroid"
|
||||
return clone
|
||||
|
||||
const memoStore = memo({
|
||||
row,
|
||||
meta,
|
||||
})
|
||||
|
||||
let table
|
||||
|
@ -30,32 +39,113 @@
|
|||
FieldType.SIGNATURE_SINGLE,
|
||||
]
|
||||
|
||||
$: {
|
||||
table = $tables.list.find(table => table._id === value?.tableId)
|
||||
let customPopover
|
||||
let popoverAnchor
|
||||
let editableRow = {}
|
||||
let columns = new Set()
|
||||
|
||||
// Just sorting attachment types to the bottom here for a cleaner UX
|
||||
schemaFields = Object.entries(table?.schema ?? {}).sort(
|
||||
([, schemaA], [, schemaB]) =>
|
||||
(schemaA.type === "attachment") - (schemaB.type === "attachment")
|
||||
)
|
||||
// Avoid unnecessary updates
|
||||
$: memoStore.set({
|
||||
row,
|
||||
meta,
|
||||
})
|
||||
|
||||
schemaFields.forEach(([, schema]) => {
|
||||
if (!schema.autocolumn && !value[schema.name]) {
|
||||
value[schema.name] = ""
|
||||
// Legacy support
|
||||
$: fields = $memoStore?.meta?.fields
|
||||
|
||||
$: if ($memoStore?.meta?.columns) {
|
||||
columns = new Set(meta?.columns)
|
||||
}
|
||||
|
||||
$: parsedBindings = bindings.map(binding => {
|
||||
let clone = Object.assign({}, binding)
|
||||
clone.icon = "ShareAndroid"
|
||||
return clone
|
||||
})
|
||||
|
||||
$: tableId = $memoStore?.row?.tableId
|
||||
$: if (tableId) {
|
||||
table = $tables.list.find(table => table._id === tableId)
|
||||
|
||||
if (table) {
|
||||
editableRow["tableId"] = tableId
|
||||
|
||||
schemaFields = Object.entries(table?.schema ?? {})
|
||||
.filter(entry => {
|
||||
const [key, field] = entry
|
||||
return field.type !== "formula" && !field.autocolumn
|
||||
})
|
||||
.sort(
|
||||
([, schemaA], [, schemaB]) =>
|
||||
(schemaA.type === "attachment") - (schemaB.type === "attachment")
|
||||
)
|
||||
|
||||
// Parse out any unused data.
|
||||
if ($memoStore?.meta?.columns) {
|
||||
for (const column of meta?.columns) {
|
||||
if (!(column in table?.schema)) {
|
||||
columns.delete(column)
|
||||
}
|
||||
}
|
||||
columns = new Set(columns)
|
||||
}
|
||||
})
|
||||
}
|
||||
const onChangeTable = e => {
|
||||
value["tableId"] = e.detail
|
||||
dispatch("change", value)
|
||||
}
|
||||
|
||||
const coerce = (value, type) => {
|
||||
const re = new RegExp(/{{([^{].*?)}}/g)
|
||||
if (re.test(value)) {
|
||||
return value
|
||||
}
|
||||
|
||||
if (columns.size) {
|
||||
for (const key of columns) {
|
||||
const entry = schemaFields.find(entry => {
|
||||
const [fieldKey] = entry
|
||||
return fieldKey == key
|
||||
})
|
||||
|
||||
if (entry) {
|
||||
const [_, fieldSchema] = entry
|
||||
editableRow = {
|
||||
...editableRow,
|
||||
[key]: coerce(
|
||||
!(key in $memoStore?.row) ? "" : $memoStore?.row[key],
|
||||
fieldSchema.type
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
schemaFields.forEach(entry => {
|
||||
const [key] = entry
|
||||
if ($memoStore?.row?.[key] && !editableRow?.[key]) {
|
||||
editableRow = {
|
||||
...editableRow,
|
||||
[key]: $memoStore?.row[key],
|
||||
}
|
||||
columns.add(key)
|
||||
}
|
||||
})
|
||||
columns = new Set(columns)
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy - add explicitly cleared relationships to the request.
|
||||
$: if (schemaFields?.length && fields) {
|
||||
// Meta fields processing.
|
||||
Object.keys(fields).forEach(key => {
|
||||
if (fields[key]?.clearRelationships) {
|
||||
columns.add(key)
|
||||
}
|
||||
})
|
||||
columns = new Set(columns)
|
||||
}
|
||||
|
||||
$: typeToField = Object.values(FIELDS).reduce((acc, field) => {
|
||||
acc[field.type] = field
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
// Row coerce
|
||||
const coerce = (value, type) => {
|
||||
const re = new RegExp(/{{([^{].*?)}}/g)
|
||||
if (typeof value === "string" && re.test(value)) {
|
||||
return value
|
||||
}
|
||||
if (type === "number") {
|
||||
if (typeof value === "number") {
|
||||
return value
|
||||
|
@ -66,6 +156,9 @@
|
|||
return value
|
||||
}
|
||||
if (type === "array") {
|
||||
if (!value) {
|
||||
return []
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value
|
||||
}
|
||||
|
@ -73,7 +166,9 @@
|
|||
}
|
||||
|
||||
if (type === "link") {
|
||||
if (Array.isArray(value)) {
|
||||
if (!value) {
|
||||
return []
|
||||
} else if (Array.isArray(value)) {
|
||||
return value
|
||||
}
|
||||
return value.split(",").map(x => x.trim())
|
||||
|
@ -86,65 +181,52 @@
|
|||
return value
|
||||
}
|
||||
|
||||
const onChange = (e, field, type) => {
|
||||
let newValue = {
|
||||
...value,
|
||||
[field]: coerce(e.detail, type),
|
||||
const onChange = u => {
|
||||
const update = {
|
||||
_tableId: tableId,
|
||||
row: { ...$memoStore.row },
|
||||
meta: { ...$memoStore.meta },
|
||||
...u,
|
||||
}
|
||||
dispatch("change", newValue)
|
||||
dispatch("change", update)
|
||||
}
|
||||
|
||||
const onChangeSetting = (e, field) => {
|
||||
let fields = {}
|
||||
fields[field] = {
|
||||
clearRelationships: e.detail,
|
||||
const fieldUpdate = (e, field) => {
|
||||
const update = {
|
||||
row: {
|
||||
...$memoStore?.row,
|
||||
[field]: e.detail,
|
||||
},
|
||||
}
|
||||
dispatch("change", {
|
||||
key: "meta",
|
||||
fields,
|
||||
})
|
||||
onChange(update)
|
||||
}
|
||||
|
||||
// Ensure any nullish tableId values get set to empty string so
|
||||
// that the select works
|
||||
$: if (value?.tableId == null) value = { tableId: "" }
|
||||
</script>
|
||||
|
||||
<div class="schema-fields">
|
||||
<Label>Table</Label>
|
||||
<div class="field-width">
|
||||
<Select
|
||||
on:change={onChangeTable}
|
||||
value={value.tableId}
|
||||
options={$tables.list.filter(table => table._id !== TableNames.USERS)}
|
||||
getOptionLabel={table => table.name}
|
||||
getOptionValue={table => table._id}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{#if schemaFields.length}
|
||||
{#if columns.size}
|
||||
{#each schemaFields as [field, schema]}
|
||||
{#if !schema.autocolumn}
|
||||
<div class:schema-fields={!attachmentTypes.includes(schema.type)}>
|
||||
<Label>{field}</Label>
|
||||
<div class:field-width={!attachmentTypes.includes(schema.type)}>
|
||||
{#if !schema.autocolumn && columns.has(field)}
|
||||
<PropField
|
||||
label={field}
|
||||
fullWidth={attachmentTypes.includes(schema.type)}
|
||||
>
|
||||
<div class="prop-control-wrap">
|
||||
{#if isTestModal}
|
||||
<RowSelectorTypes
|
||||
{isTestModal}
|
||||
{field}
|
||||
{schema}
|
||||
bindings={parsedBindings}
|
||||
{value}
|
||||
{onChange}
|
||||
value={$memoStore?.row}
|
||||
onChange={fieldUpdate}
|
||||
/>
|
||||
{:else}
|
||||
<DrawerBindableSlot
|
||||
title={value.title || field}
|
||||
title={$memoStore?.row?.title || field}
|
||||
panel={AutomationBindingPanel}
|
||||
type={schema.type}
|
||||
{schema}
|
||||
value={value[field]}
|
||||
on:change={e => onChange(e, field)}
|
||||
value={editableRow[field]}
|
||||
on:change={e => fieldUpdate(e, field)}
|
||||
{bindings}
|
||||
allowJS={true}
|
||||
updateOnChange={false}
|
||||
|
@ -155,52 +237,106 @@
|
|||
{field}
|
||||
{schema}
|
||||
bindings={parsedBindings}
|
||||
{value}
|
||||
{onChange}
|
||||
value={editableRow}
|
||||
onChange={fieldUpdate}
|
||||
/>
|
||||
</DrawerBindableSlot>
|
||||
{/if}
|
||||
|
||||
{#if isUpdateRow && schema.type === "link"}
|
||||
<div class="checkbox-field">
|
||||
<Checkbox
|
||||
value={meta.fields?.[field]?.clearRelationships}
|
||||
text={"Clear relationships if empty?"}
|
||||
size={"S"}
|
||||
on:change={e => onChangeSetting(e, field)}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<Icon
|
||||
hoverable
|
||||
name="Close"
|
||||
on:click={() => {
|
||||
columns.delete(field)
|
||||
const update = { ...editableRow }
|
||||
delete update[field]
|
||||
onChange({ row: update, meta: { columns: Array.from(columns) } })
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</PropField>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
{#if table && schemaFields}
|
||||
<div
|
||||
class="add-fields-btn"
|
||||
class:empty={!columns?.size}
|
||||
bind:this={popoverAnchor}
|
||||
>
|
||||
<ActionButton
|
||||
icon="Add"
|
||||
fullWidth
|
||||
on:click={() => {
|
||||
customPopover.show()
|
||||
}}
|
||||
disabled={!schemaFields}
|
||||
>Add fields
|
||||
</ActionButton>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Popover
|
||||
align="center"
|
||||
bind:this={customPopover}
|
||||
anchor={popoverAnchor}
|
||||
minWidth={popoverAnchor?.getBoundingClientRect()?.width}
|
||||
maxHeight={300}
|
||||
resizable={false}
|
||||
offset={10}
|
||||
>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
||||
<ul class="spectrum-Menu" role="listbox">
|
||||
{#each schemaFields || [] as [field, schema]}
|
||||
{#if !schema.autocolumn}
|
||||
<li
|
||||
class="table_field spectrum-Menu-item"
|
||||
class:is-selected={columns.has(field)}
|
||||
on:click={e => {
|
||||
if (columns.has(field)) {
|
||||
columns.delete(field)
|
||||
} else {
|
||||
columns.add(field)
|
||||
}
|
||||
onChange({ meta: { columns: Array.from(columns) } })
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name={typeToField?.[schema.type]?.icon}
|
||||
color={"var(--spectrum-global-color-gray-600)"}
|
||||
tooltip={capitalise(schema.type)}
|
||||
tooltipType={TooltipType.Info}
|
||||
tooltipPosition={TooltipPosition.Left}
|
||||
/>
|
||||
<div class="field_name spectrum-Menu-itemLabel">{field}</div>
|
||||
<svg
|
||||
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
|
||||
focusable="false"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<use xlink:href="#spectrum-css-icon-Checkmark100" />
|
||||
</svg>
|
||||
</li>
|
||||
{/if}
|
||||
{/each}
|
||||
</ul>
|
||||
</Popover>
|
||||
|
||||
<style>
|
||||
.field-width {
|
||||
width: 320px;
|
||||
.table_field {
|
||||
display: flex;
|
||||
padding: var(--spacing-s) var(--spacing-l);
|
||||
gap: var(--spacing-s);
|
||||
}
|
||||
|
||||
.schema-fields {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex: 1;
|
||||
margin-bottom: 10px;
|
||||
li.is-selected .spectrum-Menu-itemLabel {
|
||||
color: var(--spectrum-global-color-gray-500);
|
||||
}
|
||||
.schema-fields :global(label) {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
.checkbox-field {
|
||||
padding-bottom: var(--spacing-s);
|
||||
padding-left: 1px;
|
||||
padding-top: var(--spacing-s);
|
||||
}
|
||||
.checkbox-field :global(label) {
|
||||
text-transform: none;
|
||||
|
||||
.prop-control-wrap {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr min-content;
|
||||
gap: var(--spacing-s);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -73,12 +73,12 @@
|
|||
/>
|
||||
{:else if schemaHasOptions(schema) && schema.type === "array"}
|
||||
<Multiselect
|
||||
bind:value={value[field]}
|
||||
value={value[field]}
|
||||
options={schema.constraints.inclusion}
|
||||
on:change={e => onChange(e, field)}
|
||||
/>
|
||||
{:else if schema.type === "longform"}
|
||||
<TextArea bind:value={value[field]} on:change={e => onChange(e, field)} />
|
||||
<TextArea value={value[field]} on:change={e => onChange(e, field)} />
|
||||
{:else if schema.type === "json"}
|
||||
<span>
|
||||
<Editor
|
||||
|
@ -108,7 +108,7 @@
|
|||
useLabel={false}
|
||||
/>
|
||||
{:else if attachmentTypes.includes(schema.type)}
|
||||
<div class="attachment-field-spacinng">
|
||||
<div class="attachment-field-spacing">
|
||||
<KeyValueBuilder
|
||||
on:change={e =>
|
||||
onChange(
|
||||
|
@ -129,16 +129,18 @@
|
|||
},
|
||||
field
|
||||
)}
|
||||
object={handleAttachmentParams(value[field])}
|
||||
object={handleAttachmentParams(value[field] || {})}
|
||||
allowJS
|
||||
{bindings}
|
||||
keyBindings
|
||||
customButtonText={"Add attachment"}
|
||||
customButtonText={schema.type === FieldType.SIGNATURE_SINGLE
|
||||
? "Add signature"
|
||||
: "Add attachment"}
|
||||
keyPlaceholder={"URL"}
|
||||
valuePlaceholder={"Filename"}
|
||||
actionButtonDisabled={(schema.type === FieldType.ATTACHMENT_SINGLE ||
|
||||
schema.type === FieldType.SIGNATURE) &&
|
||||
Object.keys(value[field]).length >= 1}
|
||||
schema.type === FieldType.SIGNATURE_SINGLE) &&
|
||||
Object.keys(value[field] || {}).length >= 1}
|
||||
/>
|
||||
</div>
|
||||
{:else if ["string", "number", "bigint", "barcodeqr", "array"].includes(schema.type)}
|
||||
|
@ -152,12 +154,15 @@
|
|||
allowJS={true}
|
||||
updateOnChange={false}
|
||||
title={schema.name}
|
||||
autocomplete="off"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.attachment-field-spacinng {
|
||||
.attachment-field-spacing {
|
||||
margin-top: var(--spacing-s);
|
||||
margin-bottom: var(--spacing-l);
|
||||
border: 1px solid var(--spectrum-global-color-gray-400);
|
||||
border-radius: 4px;
|
||||
padding: var(--spacing-s);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
export let value
|
||||
export let isTrigger
|
||||
export let disabled = false
|
||||
|
||||
$: filteredTables = $tables.list.filter(table => {
|
||||
return !isTrigger || table._id !== TableNames.USERS
|
||||
|
@ -25,4 +26,5 @@
|
|||
options={filteredTables}
|
||||
getOptionLabel={table => table.name}
|
||||
getOptionValue={table => table._id}
|
||||
{disabled}
|
||||
/>
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
export let disableBindings = false
|
||||
export let forceModal = false
|
||||
export let context = null
|
||||
export let autocomplete
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
|
@ -71,6 +72,7 @@
|
|||
on:blur={onBlur}
|
||||
{placeholder}
|
||||
{updateOnChange}
|
||||
{autocomplete}
|
||||
/>
|
||||
{#if !disabled && !disableBindings}
|
||||
<div
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
export { default as BindableCombobox } from "./BindableCombobox.svelte"
|
||||
export { default as BindingPanel } from "./BindingPanel.svelte"
|
||||
export { default as BindingSidePanel } from "./BindingSidePanel.svelte"
|
||||
export { default as DrawerBindableCombobox } from "./DrawerBindableCombobox.svelte"
|
||||
export { default as ClientBindingPanel } from "./ClientBindingPanel.svelte"
|
||||
export { default as DrawerBindableInput } from "./DrawerBindableInput.svelte"
|
||||
export { default as DrawerBindableSlot } from "./DrawerBindableSlot.svelte"
|
||||
export { default as EvaluationSidePanel } from "./EvaluationSidePanel.svelte"
|
||||
export { default as ModalBindableInput } from "./ModalBindableInput.svelte"
|
||||
export { default as ServerBindingPanel } from "./ServerBindingPanel.svelte"
|
||||
export { default as SnippetDrawer } from "./SnippetDrawer.svelte"
|
||||
export { default as SnippetSidePanel } from "./SnippetSidePanel.svelte"
|
|
@ -11,7 +11,7 @@
|
|||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
import { AUTH_TYPE_LABELS, AUTH_TYPES } from "./authTypes"
|
||||
import BindableCombobox from "components/common/bindings/BindableCombobox.svelte"
|
||||
import { BindableCombobox } from "components/common/bindings"
|
||||
import { getAuthBindings, getEnvironmentBindings } from "dataBinding"
|
||||
import { environment, licensing, auth } from "stores/portal"
|
||||
import CreateEditVariableModal from "components/portal/environment/CreateEditVariableModal.svelte"
|
||||
|
|
|
@ -120,7 +120,9 @@ export async function sendAutomationAttachmentsToStorage(
|
|||
}
|
||||
}
|
||||
for (const [prop, attachments] of Object.entries(attachmentRows)) {
|
||||
if (Array.isArray(attachments)) {
|
||||
if (!attachments) {
|
||||
continue
|
||||
} else if (Array.isArray(attachments)) {
|
||||
if (attachments.length) {
|
||||
row[prop] = await Promise.all(
|
||||
attachments.map(attachment => generateAttachmentRow(attachment))
|
||||
|
|
|
@ -82,39 +82,58 @@ export async function run({ inputs, appId, emitter }: AutomationStepInput) {
|
|||
}
|
||||
const tableId = inputs.row.tableId
|
||||
|
||||
// clear any undefined, null or empty string properties so that they aren't updated
|
||||
for (let propKey of Object.keys(inputs.row)) {
|
||||
const clearRelationships =
|
||||
inputs.meta?.fields?.[propKey]?.clearRelationships
|
||||
if (
|
||||
(inputs.row[propKey] == null || inputs.row[propKey]?.length === 0) &&
|
||||
!clearRelationships
|
||||
) {
|
||||
delete inputs.row[propKey]
|
||||
// Base update
|
||||
let rowUpdate: Record<string, any> = {
|
||||
tableId,
|
||||
}
|
||||
|
||||
// Column checking - explicit clearing of empty fields
|
||||
if (inputs?.meta?.columns) {
|
||||
rowUpdate = inputs?.meta?.columns.reduce(
|
||||
(acc: Record<string, any>, key: string) => {
|
||||
acc[key] =
|
||||
!inputs.row[key] || inputs.row[key]?.length === 0
|
||||
? null
|
||||
: inputs.row[key]
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
} else {
|
||||
// Legacy - clear any empty string column values so that they aren't updated
|
||||
rowUpdate = {
|
||||
...inputs.row,
|
||||
}
|
||||
for (let propKey of Object.keys(rowUpdate)) {
|
||||
const clearRelationships =
|
||||
inputs.meta?.fields?.[propKey]?.clearRelationships
|
||||
if (
|
||||
(rowUpdate[propKey] == null || rowUpdate[propKey]?.length === 0) &&
|
||||
!clearRelationships
|
||||
) {
|
||||
delete rowUpdate[propKey]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (tableId) {
|
||||
inputs.row = await automationUtils.cleanUpRow(
|
||||
inputs.row.tableId,
|
||||
inputs.row
|
||||
)
|
||||
rowUpdate = await automationUtils.cleanUpRow(tableId, rowUpdate)
|
||||
|
||||
inputs.row = await automationUtils.sendAutomationAttachmentsToStorage(
|
||||
inputs.row.tableId,
|
||||
inputs.row
|
||||
rowUpdate = await automationUtils.sendAutomationAttachmentsToStorage(
|
||||
tableId,
|
||||
rowUpdate
|
||||
)
|
||||
}
|
||||
// have to clean up the row, remove the table from it
|
||||
const ctx: any = buildCtx(appId, emitter, {
|
||||
body: {
|
||||
...inputs.row,
|
||||
...rowUpdate,
|
||||
_id: inputs.rowId,
|
||||
},
|
||||
params: {
|
||||
rowId: inputs.rowId,
|
||||
tableId: tableId,
|
||||
tableId,
|
||||
},
|
||||
})
|
||||
await rowController.patch(ctx)
|
||||
|
|
|
@ -4,11 +4,12 @@ import {
|
|||
AutomationStepType,
|
||||
AutomationTriggerSchema,
|
||||
AutomationTriggerStepId,
|
||||
AutomationEventType,
|
||||
} from "@budibase/types"
|
||||
|
||||
export const definition: AutomationTriggerSchema = {
|
||||
name: "App Action",
|
||||
event: "app:trigger",
|
||||
event: AutomationEventType.APP_TRIGGER,
|
||||
icon: "Apps",
|
||||
tagline: "Automation fired from the frontend",
|
||||
description: "Trigger an automation from an action inside your app",
|
||||
|
|
|
@ -4,11 +4,12 @@ import {
|
|||
AutomationStepType,
|
||||
AutomationTriggerSchema,
|
||||
AutomationTriggerStepId,
|
||||
AutomationEventType,
|
||||
} from "@budibase/types"
|
||||
|
||||
export const definition: AutomationTriggerSchema = {
|
||||
name: "Cron Trigger",
|
||||
event: "cron:trigger",
|
||||
event: AutomationEventType.CRON_TRIGGER,
|
||||
icon: "Clock",
|
||||
tagline: "Cron Trigger (<b>{{inputs.cron}}</b>)",
|
||||
description: "Triggers automation on a cron schedule.",
|
||||
|
|
|
@ -4,11 +4,12 @@ import {
|
|||
AutomationStepType,
|
||||
AutomationTriggerSchema,
|
||||
AutomationTriggerStepId,
|
||||
AutomationEventType,
|
||||
} from "@budibase/types"
|
||||
|
||||
export const definition: AutomationTriggerSchema = {
|
||||
name: "Row Deleted",
|
||||
event: "row:delete",
|
||||
event: AutomationEventType.ROW_DELETE,
|
||||
icon: "TableRowRemoveCenter",
|
||||
tagline: "Row is deleted from {{inputs.enriched.table.name}}",
|
||||
description: "Fired when a row is deleted from your database",
|
||||
|
|
|
@ -4,11 +4,12 @@ import {
|
|||
AutomationStepType,
|
||||
AutomationTriggerSchema,
|
||||
AutomationTriggerStepId,
|
||||
AutomationEventType,
|
||||
} from "@budibase/types"
|
||||
|
||||
export const definition: AutomationTriggerSchema = {
|
||||
name: "Row Created",
|
||||
event: "row:save",
|
||||
event: AutomationEventType.ROW_SAVE,
|
||||
icon: "TableRowAddBottom",
|
||||
tagline: "Row is added to {{inputs.enriched.table.name}}",
|
||||
description: "Fired when a row is added to your database",
|
||||
|
|
|
@ -4,11 +4,12 @@ import {
|
|||
AutomationStepType,
|
||||
AutomationTriggerSchema,
|
||||
AutomationTriggerStepId,
|
||||
AutomationEventType,
|
||||
} from "@budibase/types"
|
||||
|
||||
export const definition: AutomationTriggerSchema = {
|
||||
name: "Row Updated",
|
||||
event: "row:update",
|
||||
event: AutomationEventType.ROW_UPDATE,
|
||||
icon: "Refresh",
|
||||
tagline: "Row is updated in {{inputs.enriched.table.name}}",
|
||||
description: "Fired when a row is updated in your database",
|
||||
|
|
|
@ -4,11 +4,12 @@ import {
|
|||
AutomationStepType,
|
||||
AutomationTriggerSchema,
|
||||
AutomationTriggerStepId,
|
||||
AutomationEventType,
|
||||
} from "@budibase/types"
|
||||
|
||||
export const definition: AutomationTriggerSchema = {
|
||||
name: "Webhook",
|
||||
event: "web:trigger",
|
||||
event: AutomationEventType.WEBHOOK_TRIGGER,
|
||||
icon: "Send",
|
||||
tagline: "Webhook endpoint is hit",
|
||||
description: "Trigger an automation when a HTTP POST webhook is hit",
|
||||
|
|
|
@ -8,7 +8,13 @@ import { checkTestFlag } from "../utilities/redis"
|
|||
import * as utils from "./utils"
|
||||
import env from "../environment"
|
||||
import { context, db as dbCore } from "@budibase/backend-core"
|
||||
import { Automation, Row, AutomationData, AutomationJob } from "@budibase/types"
|
||||
import {
|
||||
Automation,
|
||||
Row,
|
||||
AutomationData,
|
||||
AutomationJob,
|
||||
AutomationEventType,
|
||||
} from "@budibase/types"
|
||||
import { executeInThread } from "../threads/automation"
|
||||
|
||||
export const TRIGGER_DEFINITIONS = definitions
|
||||
|
@ -65,28 +71,28 @@ async function queueRelevantRowAutomations(
|
|||
})
|
||||
}
|
||||
|
||||
emitter.on("row:save", async function (event) {
|
||||
emitter.on(AutomationEventType.ROW_SAVE, async function (event) {
|
||||
/* istanbul ignore next */
|
||||
if (!event || !event.row || !event.row.tableId) {
|
||||
return
|
||||
}
|
||||
await queueRelevantRowAutomations(event, "row:save")
|
||||
await queueRelevantRowAutomations(event, AutomationEventType.ROW_SAVE)
|
||||
})
|
||||
|
||||
emitter.on("row:update", async function (event) {
|
||||
emitter.on(AutomationEventType.ROW_UPDATE, async function (event) {
|
||||
/* istanbul ignore next */
|
||||
if (!event || !event.row || !event.row.tableId) {
|
||||
return
|
||||
}
|
||||
await queueRelevantRowAutomations(event, "row:update")
|
||||
await queueRelevantRowAutomations(event, AutomationEventType.ROW_UPDATE)
|
||||
})
|
||||
|
||||
emitter.on("row:delete", async function (event) {
|
||||
emitter.on(AutomationEventType.ROW_DELETE, async function (event) {
|
||||
/* istanbul ignore next */
|
||||
if (!event || !event.row || !event.row.tableId) {
|
||||
return
|
||||
}
|
||||
await queueRelevantRowAutomations(event, "row:delete")
|
||||
await queueRelevantRowAutomations(event, AutomationEventType.ROW_DELETE)
|
||||
})
|
||||
|
||||
export async function externalTrigger(
|
||||
|
@ -112,7 +118,6 @@ export async function externalTrigger(
|
|||
}
|
||||
params.fields = coercedFields
|
||||
}
|
||||
|
||||
const data: AutomationData = { automation, event: params as any }
|
||||
if (getResponses) {
|
||||
data.event = {
|
||||
|
|
|
@ -24,6 +24,7 @@ import {
|
|||
Query,
|
||||
Webhook,
|
||||
WebhookActionType,
|
||||
AutomationEventType,
|
||||
} from "@budibase/types"
|
||||
import { LoopInput, LoopStepType } from "../../definitions/automations"
|
||||
import { merge } from "lodash"
|
||||
|
@ -305,7 +306,7 @@ export function loopAutomation(
|
|||
trigger: {
|
||||
id: "a",
|
||||
type: "TRIGGER",
|
||||
event: "row:save",
|
||||
event: AutomationEventType.ROW_SAVE,
|
||||
stepId: AutomationTriggerStepId.ROW_SAVED,
|
||||
inputs: {
|
||||
tableId,
|
||||
|
@ -347,7 +348,7 @@ export function collectAutomation(tableId?: string): Automation {
|
|||
trigger: {
|
||||
id: "a",
|
||||
type: "TRIGGER",
|
||||
event: "row:save",
|
||||
event: AutomationEventType.ROW_SAVE,
|
||||
stepId: AutomationTriggerStepId.ROW_SAVED,
|
||||
inputs: {
|
||||
tableId,
|
||||
|
|
|
@ -252,3 +252,12 @@ export type BucketedContent = AutomationAttachmentContent & {
|
|||
bucket: string
|
||||
path: string
|
||||
}
|
||||
|
||||
export enum AutomationEventType {
|
||||
ROW_SAVE = "row:save",
|
||||
ROW_UPDATE = "row:update",
|
||||
ROW_DELETE = "row:delete",
|
||||
APP_TRIGGER = "app:trigger",
|
||||
CRON_TRIGGER = "cron:trigger",
|
||||
WEBHOOK_TRIGGER = "web:trigger",
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue