diff --git a/lerna.json b/lerna.json index d762e6c26c..d991a1e813 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "2.29.11", + "version": "2.29.12", "npmClient": "yarn", "packages": [ "packages/*", diff --git a/packages/bbui/src/Tooltip/TooltipWrapper.svelte b/packages/bbui/src/Tooltip/TooltipWrapper.svelte index 4c20cb54a0..8d12b88086 100644 --- a/packages/bbui/src/Tooltip/TooltipWrapper.svelte +++ b/packages/bbui/src/Tooltip/TooltipWrapper.svelte @@ -1,33 +1,25 @@ -
{#if tooltip}
-
(showTooltip = true)} - on:mouseleave={() => (showTooltip = false)} - on:focus - > - -
- {#if showTooltip} -
- + +
+
- {/if} +
{/if}
@@ -44,14 +36,6 @@ margin-left: 5px; margin-right: 5px; } - .tooltip { - position: absolute; - display: flex; - justify-content: center; - top: 15px; - z-index: 200; - width: 160px; - } .icon { transform: scale(0.75); } 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; } diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte index d212300cdf..7d223299c7 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte @@ -8,11 +8,63 @@ 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 = {} + const rowTriggers = [ + AutomationEventType.ROW_DELETE, + AutomationEventType.ROW_UPDATE, + AutomationEventType.ROW_SAVE, + ] + + /** + * Parses the automation test data and ensures it is valid + * @param {object} testData contains all config for the test + * @returns {object} valid testData + * @todo Parse *all* data for each trigger type and relay adequate feedback + */ + const parseTestData = testData => { + const autoTrigger = $selectedAutomation?.definition?.trigger + const { tableId } = autoTrigger?.inputs || {} + + // Ensure the tableId matches the trigger table for row trigger automations + if ( + rowTriggers.includes(autoTrigger?.event) && + testData?.row?.tableId !== tableId + ) { + return { + // Reset Core fields + row: { tableId }, + meta: {}, + id: "", + revision: "", + } + } else { + // Leave the core data as it is + return testData + } + } + + /** + * Before executing a test run, relay if an automation is in a valid state + * @param {object} trigger The automation trigger config + * @returns {boolean} validation status + * @todo Parse *all* trigger types relay adequate feedback + */ + const isTriggerValid = trigger => { + if (rowTriggers.includes(trigger?.event) && !trigger?.inputs?.tableId) { + return false + } + return true + } + + const memoTestData = memo(parseTestData($selectedAutomation.testData)) + $: memoTestData.set(parseTestData($selectedAutomation.testData)) + $: { // clone the trigger so we're not mutating the reference trigger = cloneDeep($selectedAutomation.definition.trigger) @@ -20,34 +72,45 @@ // 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" - ) + $: isError = + !isTriggerValid(trigger) || + !trigger.schema.outputs.required.every( + required => $memoTestData?.[required] || required !== "row" + ) function parseTestJSON(e) { + let jsonUpdate + try { - const obj = JSON.parse(e.detail) + jsonUpdate = JSON.parse(e.detail) failedParse = null - automationStore.actions.addTestDataToAutomation(obj) } catch (e) { failedParse = "Invalid JSON" + return false } + + if (rowTriggers.includes(trigger?.event)) { + const tableId = trigger?.inputs?.tableId + + // Reset the tableId as it must match the trigger + if (jsonUpdate?.row?.tableId !== tableId) { + jsonUpdate.row.tableId = tableId + } + } + + automationStore.actions.addTestDataToAutomation(jsonUpdate) } const testAutomation = async () => { try { - await automationStore.actions.test($selectedAutomation, testData) + await automationStore.actions.test($selectedAutomation, $memoTestData) $automationStore.showTestPanel = true } catch (error) { notifications.error(error) @@ -85,7 +148,7 @@ {#if selectedValues}
import TableSelector from "./TableSelector.svelte" - import RowSelector from "./RowSelector.svelte" import FieldSelector from "./FieldSelector.svelte" import SchemaSetup from "./SchemaSetup.svelte" + import RowSelector from "./RowSelector.svelte" import { Button, - Input, Select, Label, ActionButton, @@ -15,26 +14,27 @@ Checkbox, DatePicker, DrawerContent, + Helpers, Toggle, - Icon, Divider, } 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 { @@ -43,31 +43,57 @@ EditorModes, } from "components/common/CodeEditor" import FilterBuilder from "components/design/settings/controls/FilterEditor/FilterBuilder.svelte" - import { QueryUtils, Utils, search } from "@budibase/frontend-core" + import { QueryUtils, Utils, search, memo } from "@budibase/frontend-core" import { getSchemaForDatasourcePlus, getEnvironmentBindings, } from "dataBinding" import { TriggerStepID, ActionStepID } from "constants/backend/automations" import { onMount } from "svelte" + import { writable } from "svelte/store" import { cloneDeep } from "lodash/fp" + import { + AutomationEventType, + AutomationStepType, + AutomationActionStepId, + } 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 rowEvents = [ + AutomationEventType.ROW_DELETE, + AutomationEventType.ROW_SAVE, + AutomationEventType.ROW_UPDATE, + ] + + const rowSteps = [ActionStepID.UPDATE_ROW, ActionStepID.CREATE_ROW] + let webhookModal let drawer let inputData let insertAtPos, getCaretPosition + let stepLayouts = {} + $: 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) @@ -81,31 +107,33 @@ { allowLinks: true } ) $: queryLimit = tableId?.includes("datasource") ? "∞" : "1000" - $: isTrigger = block?.type === "TRIGGER" - $: isUpdateRow = stepId === ActionStepID.UPDATE_ROW + $: isTrigger = block?.type === AutomationStepType.TRIGGER $: codeMode = - stepId === "EXECUTE_BASH" ? EditorModes.Handlebars : EditorModes.JS + stepId === AutomationActionStepId.EXECUTE_BASH + ? EditorModes.Handlebars + : EditorModes.JS $: bindingsHelpers = new BindingHelpers(getCaretPosition, insertAtPos, { disableWrapping: true, }) $: editingJs = codeMode === EditorModes.JS - $: requiredProperties = block.schema.inputs.required || [] + $: requiredProperties = isTestModal ? [] : block.schema["inputs"].required + $: stepCompletions = codeMode === EditorModes.Handlebars ? [hbAutocomplete([...bindingsToCompletions(bindings, codeMode)])] : [] - let testDataRowVisibility = {} - const getInputData = (testData, blockInputs) => { // Test data is not cloned for reactivity let newInputData = testData || cloneDeep(blockInputs) // Ensures the app action fields are populated - if (block.event === "app:trigger" && !newInputData?.fields) { + if ( + block.event === AutomationEventType.APP_TRIGGER && + !newInputData?.fields + ) { newInputData = cloneDeep(blockInputs) } - inputData = newInputData setDefaultEnumValues() } @@ -117,15 +145,338 @@ } } } - const onChange = Utils.sequential(async (e, key) => { + + // Store for any UX related data + const stepStore = writable({}) + $: currentStep = $stepStore?.[block.id] + + $: customStepLayouts($memoBlock, schemaProperties, currentStep) + + const customStepLayouts = block => { + if ( + rowSteps.includes(block.stepId) || + (rowTriggers.includes(block.stepId) && isTestModal) + ) { + const schema = schemaProperties.reduce((acc, entry) => { + const [key, val] = entry + acc[key] = val + return acc + }, {}) + + // Optionally build the rev field config when its needed. + const getRevConfig = () => { + const rowRevEntry = schema["revision"] + if (!rowRevEntry) { + return [] + } + const rowRevlabel = getFieldLabel("revision", rowRevEntry) + + return isTestModal + ? [ + { + type: DrawerBindableInput, + title: rowRevlabel, + props: { + panel: AutomationBindingPanel, + value: inputData["revision"], + onChange: e => { + onChange({ ["revision"]: e.detail }) + }, + 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, + }, + }, + ] + } + + // A select to switch from `row` to `oldRow` + const getRowTypeConfig = () => { + if (!isTestModal || block.event !== AutomationEventType.ROW_UPDATE) { + return [] + } + + if (!$stepStore?.[block.id]) { + stepStore.update(state => ({ + ...state, + [block.id]: { + rowType: "row", + }, + })) + } + + return [ + { + type: Select, + tooltip: `You can configure test data for both the updated row and + the old row, if you need it. Just select the one you wish to alter`, + title: "Row data", + props: { + value: $stepStore?.[block.id].rowType, + onChange: e => { + stepStore.update(state => ({ + ...state, + [block.id]: { + rowType: e.detail, + }, + })) + }, + getOptionLabel: type => type.name, + getOptionValue: type => type.id, + options: [ + { + id: "row", + name: "Updated row", + }, + { id: "oldRow", name: "Old row" }, + ], + }, + }, + ] + } + + const getRowSelector = () => { + const baseProps = { + bindings, + isTestModal, + isUpdateRow: block.stepId === ActionStepID.UPDATE_ROW, + } + + if (isTestModal && currentStep?.rowType === "oldRow") { + return [ + { + type: RowSelector, + props: { + row: inputData["oldRow"] || { + tableId: inputData["row"].tableId, + }, + meta: { + fields: inputData["meta"].oldFields || {}, + }, + onChange: e => { + onChange({ + oldRow: e.detail.row, + meta: { + fields: inputData["meta"].fields, + oldFields: e.detail.meta.fields, + }, + }) + }, + ...baseProps, + }, + }, + ] + } + + return [ + { + type: RowSelector, + props: { + row: inputData["row"], + meta: inputData["meta"] || {}, + onChange: e => { + onChange(e.detail) + }, + ...baseProps, + }, + }, + ] + } + + stepLayouts[block.stepId] = { + row: { + schema: schema["row"], + //?layout: RowLayoutStepComponent. + content: [ + { + type: TableSelector, + title: "Table", + props: { + isTrigger, + value: inputData["row"]?.tableId ?? "", + onChange: e => { + const rowKey = $stepStore?.[block.id]?.rowType || "row" + onChange({ + _tableId: e.detail, + meta: {}, + [rowKey]: e.detail + ? { + tableId: e.detail, + } + : {}, + }) + }, + disabled: isTestModal, + }, + }, + ...getIdConfig(), + ...getRevConfig(), + ...getRowTypeConfig(), + { + type: Divider, + props: { + noMargin: true, + }, + }, + ...getRowSelector(), + ], + }, + } + } + } + + /** + * Handler for row trigger automation updates. + @param {object} update - An automation block.inputs update object + @example + onRowTriggerUpdate({ + "tableId" : "ta_bb_employee" + }) + */ + const onRowTriggerUpdate = async update => { + if ( + Object.hasOwn(update, "tableId") && + $selectedAutomation.testData?.row?.tableId !== update.tableId + ) { + try { + const reqSchema = getSchemaForDatasourcePlus(update.tableId, { + searchableSchema: true, + }).schema + + // Parse the block inputs as usual + const updatedAutomation = + await automationStore.actions.processBlockInputs(block, { + schema: reqSchema, + ...update, + }) + + // Save the entire automation and reset the testData + await automationStore.actions.save({ + ...updatedAutomation, + testData: { + // Reset Core fields + row: { tableId: update.tableId }, + oldRow: { tableId: update.tableId }, + meta: {}, + id: "", + revision: "", + }, + }) + + return + } catch (e) { + console.error("Error saving automation", e) + notifications.error("Error saving automation") + } + } + } + + /** + * Handler for App trigger automation updates. + * Ensure updates to the field list are reflected in testData + @param {object} update - An app trigger update object + @example + onAppTriggerUpdate({ + "fields" : {"myField": "123", "myArray": "cat,dog,badger"} + }) + */ + const onAppTriggerUpdate = async update => { + try { + // Parse the block inputs as usual + const updatedAutomation = + await automationStore.actions.processBlockInputs(block, { + schema: {}, + ...update, + }) + + // Exclude default or invalid data from the test data + let updatedFields = {} + for (const key of Object.keys(block?.inputs?.fields || {})) { + if (Object.hasOwn(update.fields, key)) { + if (key !== "") { + updatedFields[key] = updatedAutomation.testData?.fields?.[key] + } + } + } + + // Save the entire automation and reset the testData + await automationStore.actions.save({ + ...updatedAutomation, + testData: { + fields: updatedFields, + }, + }) + } catch (e) { + console.error("Error saving automation", e) + notifications.error("Error saving automation") + } + } + + /** + * Handler for automation block input updates. + @param {object} update - An automation inputs update object + @example + onChange({ + meta: { fields : { "Photo": { useAttachmentBinding: false }} } + row: { "Active": true, "Order Id" : 14, ... } + }) + */ + const onChange = Utils.sequential(async update => { + const request = cloneDeep(update) + + // Process app trigger updates + if (isTrigger && !isTestModal) { + // Row trigger + if (rowEvents.includes(block.event)) { + await onRowTriggerUpdate(request) + return + } + // App trigger + if (block.event === AutomationEventType.APP_TRIGGER) { + await onAppTriggerUpdate(request) + return + } + } + // We need to cache the schema as part of the definition because it is // used in the server to detect relationships. It would be far better to // instead fetch the schema in the backend at runtime. + // If _tableId is explicitly included in the update request, the schema will be requested let schema - if (e.detail?.tableId) { - schema = getSchemaForDatasourcePlus(e.detail.tableId, { + if (request?._tableId) { + schema = getSchemaForDatasourcePlus(request._tableId, { searchableSchema: true, }).schema + delete request._tableId } try { if (isTestModal) { @@ -136,21 +487,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") } }) @@ -195,14 +547,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) ) { let noRowKeywordBindings = ["id", "revision", "oldRow"] if (!noRowKeywordBindings.includes(name)) return `trigger.row.${name}` @@ -277,7 +632,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, @@ -286,8 +644,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 @@ -353,10 +712,12 @@ function saveFilters(key) { const filters = QueryUtils.buildQuery(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() } @@ -373,6 +734,7 @@ value.customType !== "cron" && value.customType !== "triggerSchema" && value.customType !== "automationFields" && + value.customType !== "fields" && value.type !== "signature_single" && value.type !== "attachment" && value.type !== "attachment_single" @@ -381,11 +743,10 @@ function getFieldLabel(key, value) { const requiredSuffix = requiredProperties.includes(key) ? "*" : "" - return `${value.title || (key === "row" ? "Row" : key)} ${requiredSuffix}` - } - - function toggleTestDataRowVisibility(key) { - testDataRowVisibility[key] = !testDataRowVisibility[key] + const label = `${ + value.title || (key === "row" ? "Row" : key) + } ${requiredSuffix}` + return Helpers.capitalise(label) } function handleAttachmentParams(keyValueObj) { @@ -398,16 +759,6 @@ return params } - function toggleAttachmentBinding(e, key) { - onChange( - { - detail: "", - }, - key - ) - onChange({ detail: { useAttachmentBinding: e.detail } }, "meta") - } - onMount(async () => { try { await environment.loadVariables() @@ -417,122 +768,152 @@ }) -
- {#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]: null, + meta: { + useAttachmentBinding: e.detail, + }, + }) + }} + /> +
-
- {#if !inputData?.meta?.useAttachmentBinding} - - onChange( - { - detail: e.detail.map(({ name, value }) => ({ +
+ {#if !inputData?.meta?.useAttachmentBinding} + + 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 isTestModal} - onChange(e, key)} - {bindings} - updateOnChange={false} - /> - {:else} -
+ })} + object={handleAttachmentParams(inputData[key])} + allowJS + {bindings} + keyBindings + customButtonText={value.type === "attachment" + ? "Add attachment" + : "Add signature"} + keyPlaceholder={"URL"} + valuePlaceholder={"Filename"} + /> + {:else if isTestModal} + onChange({ [key]: e.detail })} + {bindings} + updateOnChange={false} + /> + {:else} onChange(e, key)} + on:change={e => onChange({ [key]: e.detail })} {bindings} updateOnChange={false} placeholder={value.customType === "queryLimit" @@ -540,235 +921,158 @@ : ""} drawerLeft="260px" /> -
- {/if} + {/if} +
-
- {: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} - Define filters + + + + (tempFilters = e.detail)} + /> + + + {:else if value.customType === "cron"} + onChange({ [key]: e.detail })} value={inputData[key]} - panel={AutomationBindingPanel} - type="email" - on:change={e => onChange(e, key)} - {bindings} - updateOnChange={false} /> - {:else} - onChange({ [key]: e.detail })} value={inputData[key]} - on:change={e => 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 isTestModal} -
- toggleTestDataRowVisibility(key)} - /> - -
- {#if testDataRowVisibility[key]} - { - if (e.detail?.key) { - onChange(e, e.detail.key) - } else { - onChange(e, key) - } - }} - {bindings} - {isTestModal} - {isUpdateRow} - /> - {/if} - - {:else} - onChange({ [key]: e.detail })} value={inputData[key]} - meta={inputData["meta"] || {}} - on:change={e => { - if (e.detail?.key) { - onChange(e, e.detail.key) - } else { - onChange(e, 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} - {isUpdateRow} /> - {/if} - {: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} - /> -
- {#if editingJs} -
- - bindingsHelpers.onSelectBinding( - inputData[key], - binding, - { - js: true, - dontDecode: true, - type: BindingType.RUNTIME, - } - )} - mode="javascript" + {: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} @@ -777,18 +1081,12 @@ width: 320px; } - .align-horizontally { - display: flex; - gap: var(--spacing-s); - align-items: center; - } - - .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 { @@ -808,10 +1106,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..7dd38ee44e 100644 --- a/packages/builder/src/components/automation/SetupPanel/FieldSelector.svelte +++ b/packages/builder/src/components/automation/SetupPanel/FieldSelector.svelte @@ -1,19 +1,28 @@ -{#if schemaFields.length && isTestModal} -
+{#if schemaFields?.length && isTestModal} +
{#each schemaFields as [field, schema]} - + + {#if [STRING, NUMBER, ARRAY].includes(schema.type)} + onChange(e, field)} + type="string" + {bindings} + allowJS={true} + updateOnChange={false} + title={schema.name} + autocomplete="off" + /> + {:else if schema.type === "boolean"} + table._id !== TableNames.USERS)} - getOptionLabel={table => table.name} - getOptionValue={table => table._id} - /> -
-
-{#if schemaFields.length} - {#each schemaFields as [field, schema]} - {#if !schema.autocolumn} -
- -
- {#if isTestModal} +{#each schemaFields || [] as [field, schema]} + {#if !schema.autocolumn && Object.hasOwn(editableFields, field)} + +
+ {#if isTestModal} + + {:else} + + onChange({ + row: { + [field]: e.detail, + }, + })} + {bindings} + allowJS={true} + updateOnChange={false} + drawerLeft="260px" + > onChange(change)} /> - {:else} - onChange(e, field)} - {bindings} - allowJS={true} - updateOnChange={false} - drawerLeft="260px" - > - - - {/if} - - {#if isUpdateRow && schema.type === "link"} -
- - onChangeSetting(field, "clearRelationships", e.detail)} - /> -
- {/if} -
+ + {/if}
- {/if} - {/each} + + {/if} +{/each} + +{#if table && schemaFields} + {#key editableFields} +
+ { + customPopover.show() + }} + disabled={!schemaFields} + >Add fields + +
+ {/key} {/if} + + + +
    + {#each schemaFields || [] as [field, schema]} + {#if !schema.autocolumn} +
  • { + if (Object.hasOwn(editableFields, field)) { + delete editableFields[field] + onChange({ + meta: { fields: editableFields }, + row: { [field]: null }, + }) + } else { + editableFields[field] = {} + onChange({ meta: { fields: editableFields } }) + } + }} + > + +
    {field}
    + +
  • + {/if} + {/each} +
+
+ diff --git a/packages/builder/src/components/automation/SetupPanel/RowSelectorTypes.svelte b/packages/builder/src/components/automation/SetupPanel/RowSelectorTypes.svelte index a43ff35c80..85d57e665a 100644 --- a/packages/builder/src/components/automation/SetupPanel/RowSelectorTypes.svelte +++ b/packages/builder/src/components/automation/SetupPanel/RowSelectorTypes.svelte @@ -11,17 +11,18 @@ import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte" import ModalBindableInput from "../../common/bindings/ModalBindableInput.svelte" import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte" - import Editor from "components/integration/QueryEditor.svelte" + import CodeEditor from "components/common/CodeEditor/CodeEditor.svelte" import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte" export let onChange export let field export let schema export let value + export let meta export let bindings export let isTestModal - export let useAttachmentBinding - export let onChangeSetting + + $: fieldData = value[field] $: parsedBindings = bindings.map(binding => { let clone = Object.assign({}, binding) @@ -35,14 +36,15 @@ FieldType.SIGNATURE_SINGLE, ] - let previousBindingState = useAttachmentBinding - function schemaHasOptions(schema) { return !!schema.constraints?.inclusion?.length } function handleAttachmentParams(keyValueObj) { let params = {} + if (!keyValueObj) { + return null + } if (!Array.isArray(keyValueObj) && keyValueObj) { keyValueObj = [keyValueObj] @@ -50,45 +52,68 @@ if (keyValueObj.length) { for (let param of keyValueObj) { - params[param.url] = param.filename + params[param.url || ""] = param.filename || "" } } return params } - async function handleToggleChange(toggleField, event) { - if (event.detail === true) { - value[toggleField] = [] - } else { - value[toggleField] = "" - } - previousBindingState = event.detail - onChangeSetting(toggleField, "useAttachmentBinding", event.detail) - onChange({ detail: value[toggleField] }, toggleField) - } + const handleMediaUpdate = e => { + const media = e.detail || [] + const isSingle = + schema.type === FieldType.ATTACHMENT_SINGLE || + schema.type === FieldType.SIGNATURE_SINGLE + const parsedMedia = media.map(({ name, value }) => ({ + url: name, + filename: value, + })) - $: if (useAttachmentBinding !== previousBindingState) { - if (useAttachmentBinding) { - value[field] = [] - } else { - value[field] = "" + if (isSingle) { + const [singleMedia] = parsedMedia + // Return only the first entry + return singleMedia + ? { + url: singleMedia.url, + filename: singleMedia.filename, + } + : null } - previousBindingState = useAttachmentBinding + + // Return the entire array + return parsedMedia } {#if schemaHasOptions(schema) && schema.type !== "array"} onChange(e, field)} - value={value[field]} + on:change={e => + onChange({ + row: { + [field]: e.detail, + }, + })} + value={fieldData} options={[ { label: "True", value: "true" }, { label: "False", value: "false" }, @@ -96,83 +121,111 @@ /> {:else if schemaHasOptions(schema) && schema.type === "array"} onChange(e, field)} + on:change={e => + onChange({ + row: { + [field]: e.detail, + }, + })} /> {:else if schema.type === "longform"} -