Improve test automation modal and handling of data

This commit is contained in:
Peter Clement 2021-09-13 13:14:03 +01:00
parent 0fadf104e2
commit d86c238d53
14 changed files with 241 additions and 113 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -13,6 +13,10 @@ export default class Automation {
return this.automation.definition.trigger
}
addTestData(data) {
this.automation.testData = data
}
addBlock(block) {
// Make sure to add trigger if doesn't exist
if (!this.hasTrigger() && block.type === "TRIGGER") {

View File

@ -80,9 +80,9 @@ const automationActions = store => ({
const { _id } = automation
return await api.post(`/api/automations/${_id}/trigger`)
},
test: async ({ automation }) => {
test: async ({ automation }, testData) => {
const { _id } = automation
return await api.post(`/api/automations/${_id}/test`)
return await api.post(`/api/automations/${_id}/test`, testData)
},
select: automation => {
store.update(state => {
@ -91,6 +91,13 @@ const automationActions = store => ({
return state
})
},
addTestDataToAutomation: data => {
store.update(state => {
state.selectedAutomation.addTestData(data)
console.log(state)
return state
})
},
addBlockToAutomation: block => {
store.update(state => {
const newBlock = state.selectedAutomation.addBlock(cloneDeep(block))

View File

@ -1,7 +1,6 @@
<script>
import { automationStore } from "builderStore"
import Flowchart from "./FlowChart/FlowChart.svelte"
import BlockList from "./BlockList.svelte"
$: automation = $automationStore.selectedAutomation?.automation
function onSelect(block) {
@ -13,6 +12,5 @@
</script>
{#if automation}
<BlockList />
<Flowchart {automation} {onSelect} />
{/if}

View File

@ -4,7 +4,11 @@
import DiscordLogo from "assets/discord.svg"
import ZapierLogo from "assets/zapier.png"
import IntegromatLogo from "assets/integromat.png"
import SlackLogo from "assets/integromat.png"
import SlackLogo from "assets/slack.svg"
import n8nlogo from "assets/n8nlogo.png"
import { database } from "stores/backend"
$: instanceId = $database._id
let selectedAction
let actionVal
@ -15,6 +19,7 @@
{ name: "discord", logo: DiscordLogo },
{ name: "slack", logo: SlackLogo },
{ name: "integromat", logo: IntegromatLogo },
{ name: "n8n", logo: n8nlogo },
]
let actions = Object.entries($automationStore.blockDefinitions.ACTION)
@ -39,13 +44,17 @@
selectedAction = action.name
}
function addBlockToAutomation() {
async function addBlockToAutomation() {
const newBlock = $automationStore.selectedAutomation.constructBlock(
"ACTION",
actionVal.stepId,
actionVal
)
automationStore.actions.addBlockToAutomation(newBlock)
await automationStore.actions.save({
instanceId,
automation: $automationStore.selectedAutomation?.automation,
})
}
</script>

View File

@ -2,14 +2,23 @@
import { automationStore } from "builderStore"
import FlowItem from "./FlowItem.svelte"
import TestDataModal from "./TestDataModal.svelte"
import Arrow from "./Arrow.svelte"
import { flip } from "svelte/animate"
import { fade, fly } from "svelte/transition"
import { Detail, Icon, ActionButton, notifications } from "@budibase/bbui"
import {
Detail,
Icon,
ActionButton,
notifications,
Modal,
} from "@budibase/bbui"
import { database } from "stores/backend"
export let automation
export let onSelect
let testDataModal
let blocks
$: instanceId = $database._id
@ -61,7 +70,7 @@
<Icon name="DeleteOutline" />
</span>
<ActionButton
on:change={() => testAutomation()}
on:click={() => testDataModal.show()}
icon="MultipleCheck"
size="S">Run test</ActionButton
>
@ -75,21 +84,24 @@
in:fade|local
out:fly|local={{ x: 500 }}
>
<FlowItem {onSelect} {block} />
<FlowItem {testDataModal} {testAutomation} {onSelect} {block} />
{#if idx !== blocks.length - 1}
<Arrow />
{/if}
</div>
{/each}
</div>
<Modal bind:this={testDataModal} width="30%">
<TestDataModal {testAutomation} />
</Modal>
</section>
<style>
.canvas {
margin: 0 -40px calc(-1 * var(--spacing-l)) -40px;
padding: var(--spacing-l) 40px 0 40px;
overflow-y: auto;
text-align: center;
height: 100%;
}
/* Fix for firefox not respecting bottom padding in scrolling containers */
.canvas > *:last-child {

View File

@ -12,17 +12,15 @@
} 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
export let testDataModal
let selected
let webhookModal
let testDataModal
let actionModal
let resultsModal
let setupToggled
@ -61,13 +59,17 @@
class={`block ${block.type} hoverable`}
class:selected
on:click={() => {
blockComplete = false
onSelect(block)
}}
>
<div class="blockSection">
<div class="splitHeader">
<div>
<div
on:click={() => {
blockComplete = !blockComplete
}}
class="splitHeader"
>
<div style="display: flex;">
<svg
width="35px"
height="35px"
@ -83,13 +85,11 @@
<Detail size="S">{block?.name?.toUpperCase() || ""}</Detail>
</div>
</div>
{#if !blockComplete}
<span on:click={() => resultsModal.show()}>
<StatusLight positive={true} negative={false}
><Body size="XS">View response</Body></StatusLight
>
</span>
{/if}
<span on:click={() => resultsModal.show()}>
<StatusLight positive={true} negative={false}
><Body size="XS">View response</Body></StatusLight
>
</span>
</div>
</div>
{#if !blockComplete}
@ -98,7 +98,7 @@
<Layout noPadding gap="S">
<div class="splitHeader">
<div
on:click={() => {
on:click|stopPropagation={() => {
setupToggled = !setupToggled
}}
class="toggle"
@ -118,10 +118,14 @@
</div>
{#if setupToggled}
<AutomationBlockSetup {block} {webhookModal} />
<AutomationBlockSetup
schemaProperties={Object.entries(block.schema.inputs.properties)}
{block}
{webhookModal}
/>
{#if lastStep}
<Button on:click={() => testDataModal.show()} cta
>Test Automation</Button
>Continue and test automation</Button
>
{/if}
<Button
@ -149,10 +153,6 @@
<Modal bind:this={webhookModal} width="30%">
<CreateWebhookModal />
</Modal>
<Modal bind:this={testDataModal} width="30%">
<TestDataModal />
</Modal>
</div>
<style>

View File

@ -4,23 +4,70 @@
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
import { cloneDeep } from "lodash/fp"
let trigger = cloneDeep($automationStore.automation?.defintion.trigger)
let failedParse = null
// clone the trigger so we're not mutating the reference
let trigger = cloneDeep(
$automationStore.selectedAutomation.automation.definition.trigger
)
if (!$automationStore.selectedAutomation.automation.testData) {
$automationStore.selectedAutomation.automation.testData = {}
}
// get the outputs so we can define the fields
let schemaProperties = Object.entries(
trigger.schema?.outputs?.properties || {}
)
// check to see if there is existing test data in the store
$: testData = Object.keys(
$automationStore.selectedAutomation?.automation?.testData
).length
? $automationStore.selectedAutomation.automation.testData
: {}
function parseTestJSON(e) {
try {
const obj = JSON.parse(e.detail)
failedParse = null
automationStore.actions.addTestDataToAutomation(obj)
} catch (e) {
failedParse = "Invalid JSON"
}
}
</script>
<ModalContent
title="Add test data"
confirmText="Save"
showConfirmButton={true}
onConfirm={() => {
automationStore.actions.trigger(
$automationStore.selectedAutomation,
testData
)
}}
cancelText="Cancel"
>
<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>
<Tabs selected="Form" quiet
><Tab icon="Form" title="Form"
><AutomationBlockSetup
{testData}
{schemaProperties}
block={trigger}
/></Tab
>
<Tab icon="FileJson" title="JSON">
<Label>JSON</Label><TextArea
value={JSON.stringify(
$automationStore.selectedAutomation.automation.testData,
null,
2
)}
error={failedParse}
on:change={e => parseTestJSON(e)}
/>
</Tab>
</Tabs>
</ModalContent>

View File

@ -13,24 +13,34 @@
import CronBuilder from "./CronBuilder.svelte"
import Editor from "components/integration/QueryEditor.svelte"
import { database } from "stores/backend"
import { debounce } from "lodash"
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
export let block
export let webhookModal
$: inputs = Object.entries(block.schema?.inputs?.properties || {})
export let testData
export let schemaProperties
$: stepId = block.stepId
$: bindings = getAvailableBindings(
block,
block || $automationStore.selectedBlock,
$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,
})
}
$: inputData = testData ? testData : block.inputs
const debouncedOnChange = debounce(async function (e, key) {
if (testData) {
testData[key] = e.detail
} else {
block.inputs[key] = e.detail
await automationStore.actions.save({
instanceId,
automation: $automationStore.selectedAutomation?.automation,
})
}
}, 800)
function getAvailableBindings(block, automation) {
if (!block || !automation) {
@ -65,64 +75,75 @@
</script>
<div class="fields">
{#each inputs as [key, value]}
{#each schemaProperties as [key, value]}
<div class="block-field">
<Label>{value.title}</Label>
<Label>{value.title || key}</Label>
{#if value.type === "string" && value.enum}
<Select
on:change={e => saveOnChange(e, key)}
value={block.inputs[key]}
on:change={e => debouncedOnChange(e, key)}
value={inputData[key]}
options={value.enum}
getOptionLabel={(x, idx) => (value.pretty ? value.pretty[idx] : x)}
/>
{:else if value.customType === "password"}
<Input
type="password"
on:change={e => saveOnChange(e, key)}
value={block.inputs[key]}
on:change={e => debouncedOnChange(e, key)}
value={inputData[key]}
/>
{:else if value.customType === "email"}
<DrawerBindableInput
title={value.title}
panel={AutomationBindingPanel}
type="email"
value={block.inputs[key]}
on:change={e => saveOnChange(e, key)}
{bindings}
/>
{#if testData}
<ModalBindableInput
title={value.title}
value={inputData[key]}
panel={AutomationBindingPanel}
type="email"
on:change={e => debouncedOnChange(e, key)}
{bindings}
/>
{:else}
<DrawerBindableInput
title={value.title}
panel={AutomationBindingPanel}
type="email"
value={inputData[key]}
on:change={e => debouncedOnChange(e, key)}
{bindings}
/>
{/if}
{:else if value.customType === "query"}
<QuerySelector
on:change={e => saveOnChange(e, key)}
value={block.inputs[key]}
on:change={e => debouncedOnChange(e, key)}
value={inputData[key]}
/>
{:else if value.customType === "cron"}
<CronBuilder
on:change={e => saveOnChange(e, key)}
value={block.inputs[key]}
on:change={e => debouncedOnChange(e, key)}
value={inputData[key]}
/>
{:else if value.customType === "queryParams"}
<QueryParamSelector
on:change={e => saveOnChange(e, key)}
value={block.inputs[key]}
on:change={e => debouncedOnChange(e, key)}
value={inputData[key]}
{bindings}
/>
{:else if value.customType === "table"}
<TableSelector
value={block.inputs[key]}
on:change={e => saveOnChange(e, key)}
value={inputData[key]}
on:change={e => debouncedOnChange(e, key)}
/>
{:else if value.customType === "row"}
<RowSelector
value={block.inputs[key]}
on:change={e => saveOnChange(e, key)}
value={inputData[key]}
on:change={e => debouncedOnChange(e, key)}
{bindings}
/>
{:else if value.customType === "webhookUrl"}
<WebhookDisplay value={block.inputs[key]} />
<WebhookDisplay value={inputData[key]} />
{:else if value.customType === "triggerSchema"}
<SchemaSetup
on:change={e => saveOnChange(e, key)}
value={block.inputs[key]}
on:change={e => debouncedOnChange(e, key)}
value={value[key]}
/>
{:else if value.customType === "code"}
<CodeEditorModal>
@ -130,21 +151,32 @@
<Editor
mode="javascript"
on:change={e => {
saveOnChange(e, key)
block.inputs[key] = e.detail.value
debouncedOnChange(e, key)
inputData[key] = e.detail.value
}}
value={block.inputs[key]}
value={inputData[key]}
/>
</CodeEditorModal>
{:else if value.type === "string" || value.type === "number"}
<DrawerBindableInput
title={value.title}
panel={AutomationBindingPanel}
type={value.customType}
value={block.inputs[key]}
on:change={e => saveOnChange(e, key)}
{bindings}
/>
{#if testData}
<ModalBindableInput
title={value.title}
value={inputData[key]}
panel={AutomationBindingPanel}
type={value.customType}
on:change={e => debouncedOnChange(e, key)}
{bindings}
/>
{:else}
<DrawerBindableInput
title={value.title}
panel={AutomationBindingPanel}
type={value.customType}
value={inputData[key]}
on:change={e => debouncedOnChange(e, key)}
{bindings}
/>
{/if}
{/if}
</div>
{/each}

View File

@ -4,18 +4,24 @@
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
import { createEventDispatcher } from "svelte"
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
import { automationStore } from "builderStore"
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 ?? {})
const onChangeTable = e => {
value = { tableId: e.detail }
dispatch("change", value)
}
const onChange = (e, field) => {
value[field] = e.detail
dispatch("change", value)
}
// Ensure any nullish tableId values get set to empty string so
// that the select works
@ -27,7 +33,7 @@
</script>
<Select
on:change={onChange}
on:change={onChangeTable}
value={value.tableId}
options={$tables.list}
getOptionLabel={table => table.name}
@ -40,23 +46,31 @@
{#if !schema.autocolumn}
{#if schemaHasOptions(schema)}
<Select
on:change={onChange}
on:change={e => onChange(e, field)}
label={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
dispatch("change", e.detail)
}}
label={field}
type="string"
{bindings}
/>
{#if $automationStore.selectedAutomation.automation.testData}
<ModalBindableInput
value={value[field]}
panel={AutomationBindingPanel}
label={field}
type={value.customType}
on:change={e => onChange(e, field)}
{bindings}
/>
{:else}
<DrawerBindableInput
panel={AutomationBindingPanel}
value={value[field]}
on:change={e => onChange(e, field)}
label={field}
type="string"
{bindings}
/>
{/if}
{/if}
{/if}
{/each}

View File

@ -31,15 +31,13 @@
function addField() {
const newValue = { ...value }
newValue[""] = "string"
value = newValue
dispatch("change", value)
dispatch("change", newValue)
}
function removeField(name) {
const newValues = { ...value }
delete newValues[name]
value = newValues
dispatch("change", value)
dispatch("change", newValues)
}
const fieldNameChanged = originalName => e => {
@ -74,7 +72,10 @@
/>
<Select
value={field.type}
on:change={e => (value[field.name] = e.target.value)}
on:change={e => {
value[field.name] = e.target.value
dispatch("change", value)
}}
options={typeOptions}
/>
<i

View File

@ -10,7 +10,9 @@
$: instanceId = $database._id
$: automation = $automationStore.selectedAutomation?.automation
$: automationLive = automation?.live
$: console.log(
$automationStore.selectedBlock.definition.trigger.schema.outputs.properties
)
function setAutomationLive(live) {
if (automationLive === live) {
return

View File

@ -7,6 +7,7 @@
m => m._id === $params.automation
)
if (automation) {
console.log(automation)
automationStore.actions.select(automation)
}
}

View File

@ -4,9 +4,10 @@
import AutomationPanel from "components/automation/AutomationPanel/AutomationPanel.svelte"
import CreateAutomationModal from "components/automation/AutomationPanel/CreateAutomationModal.svelte"
import CreateWebhookModal from "components/automation/shared/CreateWebhookModal.svelte"
$: automation = $automationStore.selectedAutomation?.automation
$: automation = $automationStore.automations[0]
let modal
let webhookModal
$: console.log(automation)
</script>
<!-- routify:options index=3 -->