save automation on change / delete / create

This commit is contained in:
Peter Clement 2021-09-09 12:00:37 +01:00
parent 3e851c5eae
commit 4514776e94
12 changed files with 262 additions and 117 deletions

View File

@ -1,16 +1,18 @@
<script>
import { automationStore } from "builderStore"
import FlowItem from "./FlowItem.svelte"
import Arrow from "./Arrow.svelte"
import { flip } from "svelte/animate"
import { fade, fly } from "svelte/transition"
import { Body, Detail, Icon, Label, StatusLight } from "@budibase/bbui"
import { Detail, Icon, ActionButton, notifications } from "@budibase/bbui"
import { database } from "stores/backend"
export let automation
export let onSelect
let blocks
// TODO: ADD LOGIC FOR SWITCHING THIS
let published = true
$: instanceId = $database._id
$: {
blocks = []
@ -21,6 +23,29 @@
blocks = blocks.concat(automation.definition.steps || [])
}
}
async function deleteAutomation() {
await automationStore.actions.delete({
instanceId,
automation: $automationStore.selectedAutomation?.automation,
})
}
async function testAutomation() {
const result = await automationStore.actions.trigger({
automation: $automationStore.selectedAutomation.automation,
})
if (result.status === 200) {
notifications.success(
`Automation ${$automationStore.selectedAutomation.automation.name} triggered successfully.`
)
} else {
notifications.error(
`Failed to trigger automation ${$automationStore.selectedAutomation.automation.name}.`
)
}
return result
}
</script>
<section class="canvas">
@ -32,26 +57,23 @@
style="display:flex;
color: var(--spectrum-global-color-gray-400);"
>
<span class="iconPadding">
<span on:click={() => deleteAutomation()} class="iconPadding">
<Icon name="DeleteOutline" />
</span>
<Label>Delete</Label>
<ActionButton
on:change={() => testAutomation()}
icon="MultipleCheck"
size="S">Run test</ActionButton
>
</div>
</div>
<div style="margin-left: calc(-1 * var(--spacing-s))">
<StatusLight positive={published} notice={!published}
>{#if published}
<Body size="XS">Automation is published</Body>{:else}
<Body size="XS">Automation is not published</Body>{/if}</StatusLight
>
</div>
</div>
{#each blocks as block, idx (block.id)}
<div
class="block"
animate:flip={{ duration: 600 }}
animate:flip={{ duration: 800 }}
in:fade|local
out:fly|local={{ x: 100 }}
out:fly|local={{ x: 500 }}
>
<FlowItem {onSelect} {block} />
{#if idx !== blocks.length - 1}
@ -97,6 +119,7 @@
}
.iconPadding {
cursor: pointer;
display: flex;
padding-right: var(--spacing-m);
}

View File

@ -8,77 +8,52 @@
Detail,
Modal,
Button,
ActionButton,
notifications,
StatusLight,
} from "@budibase/bbui"
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
import CreateWebhookModal from "components/automation/shared/CreateWebhookModal.svelte"
import TestDataModal from "./TestDataModal.svelte"
import ResultsModal from "./ResultsModal.svelte"
import ActionModal from "./ActionModal.svelte"
import { database } from "stores/backend"
export let onSelect
export let block
let selected
let webhookModal
let testDataModal
let actionModal
let setupComplete
let resultsModal
let setupToggled
let blockComplete
let testToggled
$: setupToggled = !setupComplete || false
$: instanceId = $database._id
$: schemaKey = Object.keys(block.schema?.inputs?.properties || {})
$: isTrigger = block.type === "TRIGGER"
$: selected = $automationStore.selectedBlock?.id === block.id
$: steps =
$automationStore.selectedAutomation?.automation?.definition?.steps ?? []
$: blockIdx = steps.findIndex(step => step.id === block.id)
$: allowDeleteTrigger = !steps.length
$: lastStep = !isTrigger && blockIdx + 1 === steps.length
function deleteStep() {
// Logic for hiding / showing the add button.first we check if it has a child
// then we check to see whether its inputs have been commpleted
$: disableAddButton = isTrigger
? $automationStore.selectedAutomation?.automation?.definition?.steps
.length > 0
: !isTrigger && steps.length - blockIdx > 1
$: hasCompletedInputs = Object.keys(
block.schema?.inputs?.properties || {}
).every(x => block?.inputs[x])
async function deleteStep() {
automationStore.actions.deleteAutomationBlock(block)
}
async function testAutomation() {
const result = await automationStore.actions.trigger({
automation: $automationStore.selectedAutomation.automation,
})
if (result.status === 200) {
notifications.success(
`Automation ${$automationStore.selectedAutomation.automation.name} triggered successfully.`
)
} else {
notifications.error(
`Failed to trigger automation ${$automationStore.selectedAutomation.automation.name}.`
)
}
return result
}
async function saveAutomation() {
await automationStore.actions.save({
instanceId,
automation: $automationStore.selectedAutomation.automation,
automation: $automationStore.selectedAutomation?.automation,
})
notifications.success(
`Automation ${$automationStore.selectedAutomation.automation.name} saved.`
)
}
function onContinue() {
const testResult = testAutomation()
const saveResult = saveAutomation()
if (testResult && saveResult) {
setupComplete = true
testToggled = true
}
}
</script>
@ -108,8 +83,12 @@
<Detail size="S">{block?.name?.toUpperCase() || ""}</Detail>
</div>
</div>
{#if blockComplete}
<StatusLight positive={true} notice={false} />
{#if !blockComplete}
<span on:click={() => resultsModal.show()}>
<StatusLight positive={true} negative={false}
><Body size="XS">View response</Body></StatusLight
>
</span>
{/if}
</div>
</div>
@ -120,9 +99,7 @@
<div class="splitHeader">
<div
on:click={() => {
if (!setupComplete) {
setupToggled = !setupToggled
}
setupToggled = !setupToggled
}}
class="toggle"
>
@ -133,55 +110,38 @@
{/if}
<Detail size="S">Setup</Detail>
</div>
<div on:click={() => deleteStep()}>
<Icon name="DeleteOutline" />
</div>
{#if !isTrigger}
<div on:click={() => deleteStep()}>
<Icon name="DeleteOutline" />
</div>
{/if}
</div>
{#if setupToggled}
<AutomationBlockSetup {block} {webhookModal} />
{#if block.inputs[schemaKey]}
<Button on:click={() => onContinue()} cta
>Continue and test trigger</Button
{#if lastStep}
<Button on:click={() => testDataModal.show()} cta
>Test Automation</Button
>
{/if}
{/if}
</Layout>
</div>
<Divider noMargin />
<div class="blockSection">
<Layout noPadding gap="S">
<div
on:click={() => {
if (setupComplete) {
testToggled = !testToggled
}
}}
class="toggle"
>
{#if testToggled}
<Icon size="M" name="ChevronDown" />
{:else}
<Icon size="M" name="ChevronRight" />
{/if}
<Detail size="S">Test</Detail>
</div>
{#if testToggled}
<ActionButton on:click={testDataModal.show()} fullWidth icon="Add"
>Add test data</ActionButton
>
<Button
disabled={disableAddButton ? true : !hasCompletedInputs}
on:click={() => {
setupToggled = false
actionModal.show()
}}
cta>Save trigger and continue to action</Button
primary={!isTrigger}
cta={isTrigger}>Add Action</Button
>
{/if}
</Layout>
</div>
{/if}
<Modal bind:this={resultsModal} width="30%">
<ResultsModal />
</Modal>
<Modal bind:this={actionModal} width="30%">
<ActionModal bind:blockComplete />
</Modal>

View File

@ -0,0 +1,58 @@
<script>
import { ModalContent, Icon, Detail } from "@budibase/bbui"
let inputToggled
let outputToggled
</script>
<ModalContent
showCloseIcon={false}
showConfirmButton={false}
title="Test Automation"
cancelText="Close"
>
<div class="splitHeader">
<div
on:click={() => {
inputToggled = !inputToggled
}}
class="toggle"
>
{#if inputToggled}
<Icon size="M" name="ChevronDown" />
{:else}
<Icon size="M" name="ChevronRight" />
{/if}
<Detail size="S">Input</Detail>
</div>
</div>
<div class="splitHeader">
<div
on:click={() => {
outputToggled = !outputToggled
}}
class="toggle"
>
{#if outputToggled}
<Icon size="M" name="ChevronDown" />
{:else}
<Icon size="M" name="ChevronRight" />
{/if}
<Detail size="S">Output</Detail>
</div>
</div>
</ModalContent>
<style>
.splitHeader {
cursor: pointer;
display: flex;
justify-content: space-between;
}
.toggle {
display: flex;
align-items: center;
}
</style>

View File

@ -1,5 +1,10 @@
<script>
import { ModalContent } from "@budibase/bbui"
import { ModalContent, Tabs, Tab, TextArea, Label } from "@budibase/bbui"
import { automationStore } from "builderStore"
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
import { cloneDeep } from "lodash/fp"
let trigger = cloneDeep($automationStore.automation?.defintion.trigger)
</script>
<ModalContent
@ -8,5 +13,14 @@
showConfirmButton={true}
cancelText="Cancel"
>
test
<div class="tabs-positioning">
<Tabs selected="Form" quiet
><Tab icon="Form" title="Form"
><AutomationBlockSetup block={trigger} /></Tab
>>
<Tab icon="FileJson" title="JSON">
<Label>JSON</Label><TextArea />
</Tab>
</Tabs>
</div>
</ModalContent>

View File

@ -26,8 +26,14 @@
automationStore.actions.addBlockToAutomation(newBlock)
if (triggerVal.stepId === "WEBHOOK") {
webhookModal.show()
webhookModal.show
}
await automationStore.actions.save({
instanceId,
automation: $automationStore.selectedAutomation?.automation,
})
notifications.success(`Automation ${name} created.`)
$goto(`./${$automationStore.selectedAutomation.automation._id}`)

View File

@ -12,16 +12,25 @@
import QueryParamSelector from "./QueryParamSelector.svelte"
import CronBuilder from "./CronBuilder.svelte"
import Editor from "components/integration/QueryEditor.svelte"
import { database } from "stores/backend"
export let block
export let webhookModal
$: inputs = Object.entries(block.schema?.inputs?.properties || {})
$: stepId = block.stepId
$: bindings = getAvailableBindings(
block,
$automationStore.selectedAutomation?.automation?.definition
)
$: instanceId = $database._id
async function saveOnChange(e, key) {
block.inputs[key] = e.detail
await automationStore.actions.save({
instanceId,
automation: $automationStore.selectedAutomation?.automation,
})
}
function getAvailableBindings(block, automation) {
if (!block || !automation) {
@ -61,41 +70,67 @@
<Label>{value.title}</Label>
{#if value.type === "string" && value.enum}
<Select
bind:value={block.inputs[key]}
on:change={e => saveOnChange(e, key)}
value={block.inputs[key]}
options={value.enum}
getOptionLabel={(x, idx) => (value.pretty ? value.pretty[idx] : x)}
/>
{:else if value.customType === "password"}
<Input type="password" bind:value={block.inputs[key]} />
<Input
type="password"
on:change={e => saveOnChange(e, key)}
value={block.inputs[key]}
/>
{:else if value.customType === "email"}
<DrawerBindableInput
title={value.title}
panel={AutomationBindingPanel}
type="email"
value={block.inputs[key]}
on:change={e => (block.inputs[key] = e.detail)}
on:change={e => saveOnChange(e, key)}
{bindings}
/>
{:else if value.customType === "query"}
<QuerySelector bind:value={block.inputs[key]} />
<QuerySelector
on:change={e => saveOnChange(e, key)}
value={block.inputs[key]}
/>
{:else if value.customType === "cron"}
<CronBuilder bind:value={block.inputs[key]} />
<CronBuilder
on:change={e => saveOnChange(e, key)}
value={block.inputs[key]}
/>
{:else if value.customType === "queryParams"}
<QueryParamSelector bind:value={block.inputs[key]} {bindings} />
<QueryParamSelector
on:change={e => saveOnChange(e, key)}
value={block.inputs[key]}
{bindings}
/>
{:else if value.customType === "table"}
<TableSelector bind:value={block.inputs[key]} />
<TableSelector
value={block.inputs[key]}
on:change={e => saveOnChange(e, key)}
/>
{:else if value.customType === "row"}
<RowSelector bind:value={block.inputs[key]} {bindings} />
<RowSelector
value={block.inputs[key]}
on:change={e => saveOnChange(e, key)}
{bindings}
/>
{:else if value.customType === "webhookUrl"}
<WebhookDisplay value={block.inputs[key]} />
{:else if value.customType === "triggerSchema"}
<SchemaSetup bind:value={block.inputs[key]} />
<SchemaSetup
on:change={e => saveOnChange(e, key)}
value={block.inputs[key]}
/>
{:else if value.customType === "code"}
<CodeEditorModal>
<pre>{JSON.stringify(bindings, null, 2)}</pre>
<Editor
mode="javascript"
on:change={e => {
saveOnChange(e, key)
block.inputs[key] = e.detail.value
}}
value={block.inputs[key]}
@ -107,7 +142,7 @@
panel={AutomationBindingPanel}
type={value.customType}
value={block.inputs[key]}
on:change={e => (block.inputs[key] = e.detail)}
on:change={e => saveOnChange(e, key)}
{bindings}
/>
{/if}

View File

@ -1,7 +1,13 @@
<script>
import { Button, Select, Input } from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
const dispatch = createEventDispatcher()
export let value
const onChange = e => {
value = e.detail
dispatch("change", e.detail)
}
let presets = false
@ -30,7 +36,7 @@
</script>
<div class="block-field">
<Input bind:value />
<Input on:change={onChange} {value} />
<div class="presets">
<Button on:click={() => (presets = !presets)}
@ -38,7 +44,8 @@
>
{#if presets}
<Select
bind:value
on:change={onChange}
{value}
secondary
extraThin
label="Presets"

View File

@ -3,10 +3,17 @@
import { Select } from "@budibase/bbui"
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
import { createEventDispatcher } from "svelte"
const dispatch = createEventDispatcher()
const onChange = e => {
value = e.detail
dispatch("change", e.detail)
}
export let value
export let bindings
$: table = $tables.list.find(table => table._id === value?.tableId)
$: schemaFields = Object.entries(table?.schema ?? {})
@ -20,7 +27,8 @@
</script>
<Select
bind:value={value.tableId}
on:change={onChange}
value={value.tableId}
options={$tables.list}
getOptionLabel={table => table.name}
getOptionValue={table => table._id}
@ -32,15 +40,19 @@
{#if !schema.autocolumn}
{#if schemaHasOptions(schema)}
<Select
on:change={onChange}
label={field}
bind:value={value[field]}
value={value[field]}
options={schema.constraints.inclusion}
/>
{:else if schema.type === "string" || schema.type === "number"}
<DrawerBindableInput
panel={AutomationBindingPanel}
value={value[field]}
on:change={e => (value[field] = e.detail)}
on:change={e => {
value[field] = e.detail
dispatch("change", e.detail)
}}
label={field}
type="string"
{bindings}

View File

@ -1,5 +1,8 @@
<script>
import { Input, Select } from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
const dispatch = createEventDispatcher()
export let value = {}
$: fieldsArray = Object.entries(value).map(([name, type]) => ({
@ -29,12 +32,14 @@
const newValue = { ...value }
newValue[""] = "string"
value = newValue
dispatch("change", value)
}
function removeField(name) {
const newValues = { ...value }
delete newValues[name]
value = newValues
dispatch("change", value)
}
const fieldNameChanged = originalName => e => {
@ -50,6 +55,7 @@
newVals[current.name] = current.type
return newVals
}, {})
dispatch("change", value)
}
</script>

View File

@ -1,11 +1,20 @@
<script>
import { tables } from "stores/backend"
import { Select } from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
const dispatch = createEventDispatcher()
export let value
const onChange = e => {
value = e.detail
dispatch("change", e.detail)
}
</script>
<Select
on:change={onChange}
bind:value
options={$tables.list}
getOptionLabel={table => table.name}

View File

@ -0,0 +1,15 @@
<script>
import { automationStore } from "builderStore"
import { params } from "@roxi/routify"
if ($params.automation) {
const automation = $automationStore.automations.find(
m => m._id === $params.automation
)
if (automation) {
automationStore.actions.select(automation)
}
}
</script>
<slot />

View File

@ -40,7 +40,7 @@
{/if}
</div>
<Modal bind:this={modal}>
<CreateAutomationModal webhookModal />
<CreateAutomationModal {webhookModal} />
</Modal>
<Modal bind:this={webhookModal} width="30%">
<CreateWebhookModal />