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> <script>
import { automationStore } from "builderStore"
import FlowItem from "./FlowItem.svelte" import FlowItem from "./FlowItem.svelte"
import Arrow from "./Arrow.svelte" import Arrow from "./Arrow.svelte"
import { flip } from "svelte/animate" import { flip } from "svelte/animate"
import { fade, fly } from "svelte/transition" 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 automation
export let onSelect export let onSelect
let blocks let blocks
// TODO: ADD LOGIC FOR SWITCHING THIS $: instanceId = $database._id
let published = true
$: { $: {
blocks = [] blocks = []
@ -21,6 +23,29 @@
blocks = blocks.concat(automation.definition.steps || []) 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> </script>
<section class="canvas"> <section class="canvas">
@ -32,26 +57,23 @@
style="display:flex; style="display:flex;
color: var(--spectrum-global-color-gray-400);" color: var(--spectrum-global-color-gray-400);"
> >
<span class="iconPadding"> <span on:click={() => deleteAutomation()} class="iconPadding">
<Icon name="DeleteOutline" /> <Icon name="DeleteOutline" />
</span> </span>
<Label>Delete</Label> <ActionButton
</div> on:change={() => testAutomation()}
</div> icon="MultipleCheck"
<div style="margin-left: calc(-1 * var(--spacing-s))"> size="S">Run test</ActionButton
<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>
</div> </div>
</div>
{#each blocks as block, idx (block.id)} {#each blocks as block, idx (block.id)}
<div <div
class="block" class="block"
animate:flip={{ duration: 600 }} animate:flip={{ duration: 800 }}
in:fade|local in:fade|local
out:fly|local={{ x: 100 }} out:fly|local={{ x: 500 }}
> >
<FlowItem {onSelect} {block} /> <FlowItem {onSelect} {block} />
{#if idx !== blocks.length - 1} {#if idx !== blocks.length - 1}
@ -97,6 +119,7 @@
} }
.iconPadding { .iconPadding {
cursor: pointer;
display: flex; display: flex;
padding-right: var(--spacing-m); padding-right: var(--spacing-m);
} }

View File

@ -8,77 +8,52 @@
Detail, Detail,
Modal, Modal,
Button, Button,
ActionButton,
notifications,
StatusLight, StatusLight,
} from "@budibase/bbui" } from "@budibase/bbui"
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte" import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
import CreateWebhookModal from "components/automation/shared/CreateWebhookModal.svelte" import CreateWebhookModal from "components/automation/shared/CreateWebhookModal.svelte"
import TestDataModal from "./TestDataModal.svelte" import TestDataModal from "./TestDataModal.svelte"
import ResultsModal from "./ResultsModal.svelte"
import ActionModal from "./ActionModal.svelte" import ActionModal from "./ActionModal.svelte"
import { database } from "stores/backend" import { database } from "stores/backend"
export let onSelect export let onSelect
export let block export let block
let selected let selected
let webhookModal let webhookModal
let testDataModal let testDataModal
let actionModal let actionModal
let setupComplete let resultsModal
let setupToggled
let blockComplete let blockComplete
let testToggled
$: setupToggled = !setupComplete || false
$: instanceId = $database._id $: instanceId = $database._id
$: schemaKey = Object.keys(block.schema?.inputs?.properties || {})
$: isTrigger = block.type === "TRIGGER"
$: selected = $automationStore.selectedBlock?.id === block.id $: selected = $automationStore.selectedBlock?.id === block.id
$: steps = $: steps =
$automationStore.selectedAutomation?.automation?.definition?.steps ?? [] $automationStore.selectedAutomation?.automation?.definition?.steps ?? []
$: blockIdx = steps.findIndex(step => step.id === block.id) $: 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) 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({ await automationStore.actions.save({
instanceId, 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> </script>
@ -108,8 +83,12 @@
<Detail size="S">{block?.name?.toUpperCase() || ""}</Detail> <Detail size="S">{block?.name?.toUpperCase() || ""}</Detail>
</div> </div>
</div> </div>
{#if blockComplete} {#if !blockComplete}
<StatusLight positive={true} notice={false} /> <span on:click={() => resultsModal.show()}>
<StatusLight positive={true} negative={false}
><Body size="XS">View response</Body></StatusLight
>
</span>
{/if} {/if}
</div> </div>
</div> </div>
@ -120,9 +99,7 @@
<div class="splitHeader"> <div class="splitHeader">
<div <div
on:click={() => { on:click={() => {
if (!setupComplete) {
setupToggled = !setupToggled setupToggled = !setupToggled
}
}} }}
class="toggle" class="toggle"
> >
@ -133,55 +110,38 @@
{/if} {/if}
<Detail size="S">Setup</Detail> <Detail size="S">Setup</Detail>
</div> </div>
{#if !isTrigger}
<div on:click={() => deleteStep()}> <div on:click={() => deleteStep()}>
<Icon name="DeleteOutline" /> <Icon name="DeleteOutline" />
</div> </div>
{/if}
</div> </div>
{#if setupToggled} {#if setupToggled}
<AutomationBlockSetup {block} {webhookModal} /> <AutomationBlockSetup {block} {webhookModal} />
{#if block.inputs[schemaKey]} {#if lastStep}
<Button on:click={() => onContinue()} cta <Button on:click={() => testDataModal.show()} cta
>Continue and test trigger</Button >Test Automation</Button
> >
{/if} {/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 <Button
disabled={disableAddButton ? true : !hasCompletedInputs}
on:click={() => { on:click={() => {
setupToggled = false
actionModal.show() actionModal.show()
}} }}
cta>Save trigger and continue to action</Button primary={!isTrigger}
cta={isTrigger}>Add Action</Button
> >
{/if} {/if}
</Layout> </Layout>
</div> </div>
{/if} {/if}
<Modal bind:this={resultsModal} width="30%">
<ResultsModal />
</Modal>
<Modal bind:this={actionModal} width="30%"> <Modal bind:this={actionModal} width="30%">
<ActionModal bind:blockComplete /> <ActionModal bind:blockComplete />
</Modal> </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> <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> </script>
<ModalContent <ModalContent
@ -8,5 +13,14 @@
showConfirmButton={true} showConfirmButton={true}
cancelText="Cancel" 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> </ModalContent>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,11 +1,20 @@
<script> <script>
import { tables } from "stores/backend" import { tables } from "stores/backend"
import { Select } from "@budibase/bbui" import { Select } from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
const dispatch = createEventDispatcher()
export let value export let value
const onChange = e => {
value = e.detail
dispatch("change", e.detail)
}
</script> </script>
<Select <Select
on:change={onChange}
bind:value bind:value
options={$tables.list} options={$tables.list}
getOptionLabel={table => table.name} 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} {/if}
</div> </div>
<Modal bind:this={modal}> <Modal bind:this={modal}>
<CreateAutomationModal webhookModal /> <CreateAutomationModal {webhookModal} />
</Modal> </Modal>
<Modal bind:this={webhookModal} width="30%"> <Modal bind:this={webhookModal} width="30%">
<CreateWebhookModal /> <CreateWebhookModal />