diff --git a/packages/builder/.gitignore b/packages/builder/.gitignore index acd1a70579..abc5671984 100644 --- a/packages/builder/.gitignore +++ b/packages/builder/.gitignore @@ -6,3 +6,4 @@ release/ dist/ routify .routify/ +svelte.config.js \ No newline at end of file diff --git a/packages/builder/src/builderStore/store/automation/index.js b/packages/builder/src/builderStore/store/automation/index.js index c22240370b..ba2458f414 100644 --- a/packages/builder/src/builderStore/store/automation/index.js +++ b/packages/builder/src/builderStore/store/automation/index.js @@ -3,6 +3,7 @@ import { API } from "api" import { cloneDeep } from "lodash/fp" import { generate } from "shortid" import { selectedAutomation } from "builderStore" +import { notifications } from "@budibase/bbui" const initialAutomationState = { automations: [], @@ -21,6 +22,37 @@ export const getAutomationStore = () => { return store } +const updateReferencesInObject = (obj, modifiedIndex, action) => { + const regex = /{{\s*steps\.(\d+)\./g + for (const key in obj) { + if (typeof obj[key] === "string") { + let matches + while ((matches = regex.exec(obj[key])) !== null) { + const referencedStep = parseInt(matches[1]) + if (action === "add" && referencedStep >= modifiedIndex) { + obj[key] = obj[key].replace( + `{{ steps.${referencedStep}.`, + `{{ steps.${referencedStep + 1}.` + ) + } else if (action === "delete" && referencedStep > modifiedIndex) { + obj[key] = obj[key].replace( + `{{ steps.${referencedStep}.`, + `{{ steps.${referencedStep - 1}.` + ) + } + } + } else if (typeof obj[key] === "object" && obj[key] !== null) { + updateReferencesInObject(obj[key], modifiedIndex, action) + } + } +} + +const updateStepReferences = (steps, modifiedIndex, action) => { + steps.forEach(step => { + updateReferencesInObject(step.inputs, modifiedIndex, action) + }) +} + const automationActions = store => ({ definitions: async () => { const response = await API.getAutomationDefinitions() @@ -218,10 +250,40 @@ const automationActions = store => ({ if (!automation) { return } + + try { + updateStepReferences(newAutomation.definition.steps, blockIdx, "add") + } catch (e) { + notifications.error("Error adding automation block") + } newAutomation.definition.steps.splice(blockIdx, 0, block) await store.actions.save(newAutomation) }, - deleteAutomationBlock: async block => { + saveAutomationName: async (blockId, name) => { + const automation = get(selectedAutomation) + let newAutomation = cloneDeep(automation) + if (!automation) { + return + } + newAutomation.definition.stepNames = { + ...newAutomation.definition.stepNames, + [blockId]: name.trim(), + } + + await store.actions.save(newAutomation) + }, + deleteAutomationName: async blockId => { + const automation = get(selectedAutomation) + let newAutomation = cloneDeep(automation) + if (!automation) { + return + } + delete newAutomation.definition.stepNames[blockId] + + await store.actions.save(newAutomation) + }, + + deleteAutomationBlock: async (block, blockIdx) => { const automation = get(selectedAutomation) let newAutomation = cloneDeep(automation) @@ -233,7 +295,14 @@ const automationActions = store => ({ newAutomation.definition.steps = newAutomation.definition.steps.filter( step => step.id !== block.id ) + delete newAutomation.definition.stepNames?.[block.id] } + try { + updateStepReferences(newAutomation.definition.steps, blockIdx, "delete") + } catch (e) { + notifications.error("Error deleting automation block") + } + await store.actions.save(newAutomation) }, replace: async (automationId, automation) => { diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte index 63a3478ef3..a3c6a06690 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte @@ -5,13 +5,7 @@ import TestDataModal from "./TestDataModal.svelte" import { flip } from "svelte/animate" import { fly } from "svelte/transition" - import { - Heading, - Icon, - ActionButton, - notifications, - Modal, - } from "@budibase/bbui" + import { Icon, notifications, Modal } from "@budibase/bbui" import { ActionStepID } from "constants/backend/automations" import UndoRedoControl from "components/common/UndoRedoControl.svelte" import { automationHistoryStore } from "builderStore" @@ -20,9 +14,8 @@ let testDataModal let confirmDeleteDialog - - $: blocks = getBlocks(automation) - + let scrolling = false + $: blocks = getBlocks(automation).filter(x => x.stepId !== ActionStepID.LOOP) const getBlocks = automation => { let blocks = [] if (automation.definition.trigger) { @@ -32,58 +25,72 @@ return blocks } - async function deleteAutomation() { + const deleteAutomation = async () => { try { await automationStore.actions.delete($selectedAutomation) } catch (error) { notifications.error("Error deleting automation") } } + + const handleScroll = e => { + if (e.target.scrollTop >= 30) { + scrolling = true + } else if (e.target.scrollTop) { + // Set scrolling back to false if scrolled back to less than 100px + scrolling = false + } + } -
-
- {automation.name} -
- +
+
+ +
+
+
+ +
{ + testDataModal.show() + }} + > + Run test +
+
+
-
- { - testDataModal.show() - }} - icon="MultipleCheck" - size="M">Run test - { - $automationStore.showTestPanel = true - }} - size="M">Test Details +
{ + $automationStore.showTestPanel = true + }} + > + Test details
-
- {#each blocks as block, idx (block.id)} -
- {#if block.stepId !== ActionStepID.LOOP} - - {/if} -
- {/each} +
+
+ {#each blocks as block, idx (block.id)} +
+ {#if block.stepId !== ActionStepID.LOOP} + + {/if} +
+ {/each} +
.canvas { padding: var(--spacing-l) var(--spacing-xl); + overflow-y: auto; + max-height: 100%; + } + + .header-left :global(div) { + border-right: none; } /* Fix for firefox not respecting bottom padding in scrolling containers */ .canvas > *:last-child { @@ -117,23 +130,45 @@ } .content { - display: inline-block; - text-align: left; + flex-grow: 1; + padding: 23px 23px 80px; + box-sizing: border-box; + } + + .header.scrolling { + background: var(--background); + border-bottom: var(--border-light); + border-left: var(--border-light); + z-index: 1; } .header { + z-index: 1; display: flex; justify-content: space-between; align-items: center; + padding-left: var(--spacing-l); + transition: background 130ms ease-out; + flex: 0 0 48px; + padding-right: var(--spacing-xl); + } + .controls { + display: flex; + gap: var(--spacing-xl); } - .controls, .buttons { display: flex; justify-content: flex-end; align-items: center; - gap: var(--spacing-xl); - } - .buttons { gap: var(--spacing-s); } + + .buttons:hover { + cursor: pointer; + } + + .disabled { + pointer-events: none; + color: var(--spectrum-global-color-gray-500) !important; + } diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte index 6c964c84a9..c6d38a4d2e 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte @@ -7,20 +7,16 @@ Detail, Modal, Button, - ActionButton, notifications, Label, + AbsTooltip, } from "@budibase/bbui" import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte" import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte" import ActionModal from "./ActionModal.svelte" import FlowItemHeader from "./FlowItemHeader.svelte" import RoleSelect from "components/design/settings/controls/RoleSelect.svelte" - import { - ActionStepID, - TriggerStepID, - Features, - } from "constants/backend/automations" + import { ActionStepID, TriggerStepID } from "constants/backend/automations" import { permissions } from "stores/backend" export let block @@ -86,7 +82,7 @@ if (loopBlock) { await automationStore.actions.deleteAutomationBlock(loopBlock) } - await automationStore.actions.deleteAutomationBlock(block) + await automationStore.actions.deleteAutomationBlock(block, blockIdx) } catch (error) { notifications.error("Error saving automation") } @@ -129,6 +125,10 @@
+ + + +
{}}>
@@ -139,9 +139,6 @@ {#if !showLooping}
-
- removeLooping()} icon="DeleteOutline" /> -
(open = !open)} /> {#if open}
- {#if !isTrigger} -
-
- {#if !loopBlock && (block?.features?.[Features.LOOPING] || !block.features)} - addLooping()} icon="Reuse"> - Add Looping - - {/if} - deleteStep()} - icon="DeleteOutline" - /> -
-
- {/if} - {#if isAppAction} - - +
+ + +
{/if} diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItemHeader.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItemHeader.svelte index c09474a370..4c72c57208 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItemHeader.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItemHeader.svelte @@ -1,8 +1,9 @@ -
-
dispatch("toggle")} class="splitHeader"> +
+
{#if externalActions[block.stepId]} {/if}
- {#if isTrigger} + {#if isHeaderTrigger} Trigger - When this happens: {:else} - Step {idx} - Do this: +
+ Step {idx} +
{/if} - {block?.name?.toUpperCase() || ""} + { + automationName = e.target.value.trim() + }} + on:click={startTyping} + on:blur={async () => { + typing = false + if (automationNameError) { + automationName = stepNames[block.id] || block?.name + } else { + await saveName() + } + }} + />
{#if showTestStatus && testResult} -
- {status?.message} +
+
+ + {status?.message} + +
+ dispatch("toggle")} + hoverable + name={open ? "ChevronUp" : "ChevronDown"} + />
{/if}
{ onSelect(block) }} > - + {#if !showTestStatus} + {#if !isHeaderTrigger && !loopBlock && (block?.features?.[Features.LOOPING] || !block.features)} + + + + {/if} + + + + {/if} + {#if !showTestStatus} + dispatch("toggle")} + hoverable + name={open ? "ChevronUp" : "ChevronDown"} + /> + {/if}
+ {#if automationNameError} +
+ +
+ +
+
+
+ {/if}
diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte index 17d5b35575..5c97d77ae8 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/TestDataModal.svelte @@ -60,6 +60,7 @@ step.id === block.id) - // Extract all outputs from all previous steps as available bindins + // Extract all outputs from all previous steps as available bindingsx§x let bindings = [] let loopBlockCount = 0 for (let idx = 0; idx < blockIdx; idx++) { @@ -183,20 +182,19 @@ } } const outputs = Object.entries(schema) - let bindingIcon = "" - let bindindingRank = 0 - + let bindingRank = 0 if (idx === 0) { bindingIcon = automation.trigger.icon } else if (isLoopBlock) { bindingIcon = "Reuse" - bindindingRank = idx + 1 + bindingRank = idx + 1 } else { bindingIcon = allSteps[idx].icon - bindindingRank = idx - loopBlockCount + bindingRank = idx - loopBlockCount } - + let bindingName = + automation.stepNames?.[allSteps[idx - loopBlockCount].id] bindings = bindings.concat( outputs.map(([name, value]) => { let runtimeName = isLoopBlock @@ -205,14 +203,20 @@ ? `steps[${idx - loopBlockCount}].${name}` : `steps.${idx - loopBlockCount}.${name}` const runtime = idx === 0 ? `trigger.${name}` : runtimeName - const categoryName = - idx === 0 - ? "Trigger outputs" - : isLoopBlock - ? "Loop Outputs" - : `Step ${idx - loopBlockCount} outputs` + + let categoryName + if (idx === 0) { + categoryName = "Trigger outputs" + } else if (isLoopBlock) { + categoryName = "Loop Outputs" + } else if (bindingName) { + categoryName = `${bindingName} outputs` + } else { + categoryName = `Step ${idx - loopBlockCount} outputs` + } + return { - readableBinding: runtime, + readableBinding: bindingName ? `${bindingName}.${name}` : runtime, runtimeBinding: runtime, type: value.type, description: value.description, @@ -221,7 +225,7 @@ display: { type: value.type, name: name, - rank: bindindingRank, + rank: bindingRank, }, } }) @@ -277,6 +281,16 @@ return !dependsOn || !!inputData[dependsOn] } + function shouldRenderField(value) { + return ( + value.customType !== "row" && + value.customType !== "code" && + value.customType !== "queryParams" && + value.customType !== "cron" && + value.customType !== "triggerSchema" + ) + } + onMount(async () => { try { await environment.loadVariables() @@ -289,245 +303,248 @@
{#each schemaProperties as [key, value]} {#if canShowField(key, value)} -
- {#if key !== "fields" && value.type !== "boolean"} +
+ {#if key !== "fields" && value.type !== "boolean" && shouldRenderField(value)} {/if} - {#if value.type === "string" && value.enum && canShowField(key, value)} - onChange(e, key)} - /> -
- {:else if value.type === "date"} - onChange(e, key)} - {bindings} - allowJS={true} - updateOnChange={false} - drawerLeft="260px" - > - onChange(e, key)} + placeholder={false} + options={value.enum} + getOptionLabel={(x, idx) => + value.pretty ? value.pretty[idx] : x} /> - - {:else if value.customType === "column"} - onChange(e, key)} - value={inputData[key]} - /> - {:else if value.customType === "email"} - {#if isTestModal} - onChange(e, key)} - {bindings} - fillWidth - updateOnChange={false} - /> - {:else} - + onChange(e, key)} + /> +
+ {:else if value.type === "date"} + onChange(e, key)} {bindings} - allowJS={false} + allowJS={true} 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 === "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"} - - {#if codeMode == EditorModes.JS} - (codeBindingOpen = !codeBindingOpen)} - quiet - icon={codeBindingOpen ? "ChevronDown" : "ChevronRight"} - > - Bindings - - {#if codeBindingOpen} -
{JSON.stringify(bindings, null, 2)}
- {/if} - {/if} - { - // need to pass without the value inside - onChange({ detail: e.detail }, key) - inputData[key] = e.detail - }} - completions={stepCompletions} - mode={codeMode} - autocompleteEnabled={codeMode != EditorModes.JS} - height={500} - /> -
- {#if codeMode == EditorModes.Handlebars} - -
-
- Add available bindings by typing - }} - -
-
- {/if} -
-
- {:else if value.customType === "loopOption"} - onChange(e, key)} - {bindings} - updateOnChange={false} + value={inputData[key]} + options={Object.keys(table?.schema || {})} /> - {:else} -
+ {: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} + fillWidth + updateOnChange={false} + /> + {:else} onChange(e, key)} {bindings} + allowJS={false} updateOnChange={false} - placeholder={value.customType === "queryLimit" - ? queryLimit - : ""} 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 === "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"} + + {#if codeMode == EditorModes.JS} + (codeBindingOpen = !codeBindingOpen)} + quiet + icon={codeBindingOpen ? "ChevronDown" : "ChevronRight"} + > + Bindings + + {#if codeBindingOpen} +
{JSON.stringify(bindings, null, 2)}
+ {/if} + {/if} + { + // need to pass without the value inside + onChange({ detail: e.detail }, key) + inputData[key] = e.detail + }} + completions={stepCompletions} + mode={codeMode} + autocompleteEnabled={codeMode != EditorModes.JS} + height={500} + /> +
+ {#if codeMode == EditorModes.Handlebars} + +
+
+ Add available bindings by typing + }} + +
+
+ {/if} +
+
+ {:else if value.customType === "loopOption"} + query._id} - getOptionLabel={query => query.name} - /> +
+ +
+ table.name} - getOptionValue={table => table._id} -/> +
+ +
+