From 3f35a41046c62d077cba6d66e049990aea7df8fd Mon Sep 17 00:00:00 2001 From: Dean Date: Tue, 11 Jun 2024 09:07:55 +0100 Subject: [PATCH 01/28] Fix binding modal z-index and removing double border in automation header --- .../automation/AutomationBuilder/FlowChart/FlowChart.svelte | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte index d68e57ca36..f79b36b1ca 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte @@ -112,7 +112,7 @@ This action cannot be undone. - + @@ -148,7 +148,6 @@ .header.scrolling { background: var(--background); border-bottom: var(--border-light); - border-left: var(--border-light); z-index: 1; } From 3a10c57651382be6b3b71b1bda6079985e8c36c3 Mon Sep 17 00:00:00 2001 From: Dean Date: Thu, 13 Jun 2024 16:56:25 +0100 Subject: [PATCH 02/28] Merge commit --- .../FlowChart/TestDataModal.svelte | 43 +- .../SetupPanel/AutomationBlockSetup.svelte | 720 ++++++++++-------- .../SetupPanel/FieldSelector.svelte | 32 +- .../automation/SetupPanel/PropField.svelte | 31 + .../automation/SetupPanel/RowSelector.svelte | 344 ++++++--- .../SetupPanel/RowSelectorTypes.svelte | 23 +- .../SetupPanel/TableSelector.svelte | 2 + .../bindings/DrawerBindableInput.svelte | 2 + .../src/components/common/bindings/index.js | 12 + .../RestAuthenticationModal.svelte | 2 +- .../server/src/automations/automationUtils.ts | 4 +- .../server/src/automations/steps/updateRow.ts | 55 +- .../server/src/automations/triggerInfo/app.ts | 3 +- .../src/automations/triggerInfo/cron.ts | 3 +- .../src/automations/triggerInfo/rowDeleted.ts | 3 +- .../src/automations/triggerInfo/rowSaved.ts | 3 +- .../src/automations/triggerInfo/rowUpdated.ts | 3 +- .../src/automations/triggerInfo/webhook.ts | 3 +- packages/server/src/automations/triggers.ts | 21 +- .../server/src/tests/utilities/structures.ts | 5 +- .../types/src/documents/app/automation.ts | 9 + 21 files changed, 852 insertions(+), 471 deletions(-) create mode 100644 packages/builder/src/components/automation/SetupPanel/PropField.svelte create mode 100644 packages/builder/src/components/common/bindings/index.js diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte index d212300cdf..a6d8797df7 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte @@ -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}
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 @@ }) -
- {#each schemaProperties as [key, value]} - {#if canShowField(key, value)} - {@const label = getFieldLabel(key, value)} -
- {#if key !== "fields" && value.type !== "boolean" && shouldRenderField(value)} - - {/if} -
- {#if value.type === "string" && value.enum && canShowField(key, value)} - 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} /> - - {:else if value.customType === "column"} - onChange({ [key]: e.detail })} + value={inputData[key]} + options={Object.keys(table?.schema || {})} + /> + {:else if value.type === "attachment" || value.type === "signature_single"} +
+
+ +
+
+ + 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"} - /> -
-
- {:else if value.customType === "filters"} - Define filters - - - - (tempFilters = e.detail)} - /> - - - {:else if value.customType === "password"} - onChange(e, key)} - value={inputData[key]} - /> - {:else if value.customType === "email"} - {#if isTestModal} - onChange(e, key)} - {bindings} - updateOnChange={false} - /> - {:else} - onChange(e, key)} - {bindings} - allowJS={false} - updateOnChange={false} - drawerLeft="260px" - /> - {/if} - {:else if value.customType === "query"} - onChange(e, key)} - value={inputData[key]} - /> - {:else if value.customType === "cron"} - onChange(e, key)} - value={inputData[key]} - /> - {:else if value.customType === "automationFields"} - onChange(e, key)} - value={inputData[key]} - {bindings} - /> - {:else if value.customType === "queryParams"} - onChange(e, key)} - value={inputData[key]} - {bindings} - /> - {:else if value.customType === "table"} - onChange(e, key)} - /> - {:else if value.customType === "row"} - { - if (e.detail?.key) { - onChange(e, e.detail.key) - } else { - onChange(e, key) - } - }} - {bindings} - {isTestModal} - {isUpdateRow} - /> - {:else if value.customType === "webhookUrl"} - onChange(e, key)} - value={inputData[key]} - /> - {:else if value.customType === "fields"} - onChange(e, key)} - {bindings} - {isTestModal} - /> - {:else if value.customType === "triggerSchema"} - onChange(e, key)} - value={inputData[key]} - /> - {:else if value.customType === "code"} - -
-
- { - // 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"} />
- {#if editingJs} -
- - bindingsHelpers.onSelectBinding( - inputData[key], - binding, - { - js: true, - dontDecode: true, - type: BindingType.RUNTIME, - } - )} - mode="javascript" +
+ {:else if value.customType === "filters"} + Define filters + + + + (tempFilters = e.detail)} + /> + + + {:else if value.customType === "cron"} + onChange({ [key]: e.detail })} + value={inputData[key]} + /> + {:else if value.customType === "automationFields"} + onChange({ [key]: e.detail })} + value={inputData[key]} + {bindings} + /> + {:else if value.customType === "queryParams"} + onChange({ [key]: e.detail })} + value={inputData[key]} + {bindings} + /> + {:else if value.customType === "table"} + onChange({ [key]: e.detail })} + /> + {:else if value.customType === "webhookUrl"} + + {:else if value.customType === "fields"} + onChange({ [key]: e.detail })} + {bindings} + {isTestModal} + /> + {:else if value.customType === "triggerSchema"} + onChange({ [key]: e.detail })} + value={inputData[key]} + /> + {:else if value.customType === "code"} + +
+
+ { + // 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} />
- {/if} -
-
- {:else if value.customType === "loopOption"} - 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} -
- onChange(e, key)} + on:change={e => onChange({ [key]: e.detail })} {bindings} updateOnChange={false} - placeholder={value.customType === "queryLimit" - ? queryLimit - : ""} - drawerLeft="260px" /> -
+ {:else} +
+ onChange({ [key]: e.detail })} + {bindings} + updateOnChange={false} + placeholder={value.customType === "queryLimit" + ? queryLimit + : ""} + drawerLeft="260px" + /> +
+ {/if} {/if} - {/if} +
-
- {/if} - {/each} + {/if} + {/each} + {/if}
+ -{#if stepId === TriggerStepID.WEBHOOK} +{#if stepId === TriggerStepID.WEBHOOK && !isTestModal} {/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; diff --git a/packages/builder/src/components/automation/SetupPanel/FieldSelector.svelte b/packages/builder/src/components/automation/SetupPanel/FieldSelector.svelte index 3920885a2e..9c8c99598c 100644 --- a/packages/builder/src/components/automation/SetupPanel/FieldSelector.svelte +++ b/packages/builder/src/components/automation/SetupPanel/FieldSelector.svelte @@ -1,6 +1,7 @@ {#if schemaFields.length && isTestModal} -
+
{#each schemaFields as [field, schema]} - + + + {/each}
{/if} diff --git a/packages/builder/src/components/automation/SetupPanel/PropField.svelte b/packages/builder/src/components/automation/SetupPanel/PropField.svelte new file mode 100644 index 0000000000..9af3380155 --- /dev/null +++ b/packages/builder/src/components/automation/SetupPanel/PropField.svelte @@ -0,0 +1,31 @@ + + +
+
+ +
+
+ +
+
+ + diff --git a/packages/builder/src/components/automation/SetupPanel/RowSelector.svelte b/packages/builder/src/components/automation/SetupPanel/RowSelector.svelte index b5a54138ca..5b75bbeb5f 100644 --- a/packages/builder/src/components/automation/SetupPanel/RowSelector.svelte +++ b/packages/builder/src/components/automation/SetupPanel/RowSelector.svelte @@ -1,25 +1,34 @@ -
- -
-