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 return this.automation.definition.trigger
} }
addTestData(data) {
this.automation.testData = data
}
addBlock(block) { addBlock(block) {
// Make sure to add trigger if doesn't exist // Make sure to add trigger if doesn't exist
if (!this.hasTrigger() && block.type === "TRIGGER") { if (!this.hasTrigger() && block.type === "TRIGGER") {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,23 +4,70 @@
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte" import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
import { cloneDeep } from "lodash/fp" 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> </script>
<ModalContent <ModalContent
title="Add test data" title="Add test data"
confirmText="Save" confirmText="Save"
showConfirmButton={true} showConfirmButton={true}
onConfirm={() => {
automationStore.actions.trigger(
$automationStore.selectedAutomation,
testData
)
}}
cancelText="Cancel" cancelText="Cancel"
> >
<div class="tabs-positioning">
<Tabs selected="Form" quiet <Tabs selected="Form" quiet
><Tab icon="Form" title="Form" ><Tab icon="Form" title="Form"
><AutomationBlockSetup block={trigger} /></Tab ><AutomationBlockSetup
>> {testData}
{schemaProperties}
block={trigger}
/></Tab
>
<Tab icon="FileJson" title="JSON"> <Tab icon="FileJson" title="JSON">
<Label>JSON</Label><TextArea /> <Label>JSON</Label><TextArea
value={JSON.stringify(
$automationStore.selectedAutomation.automation.testData,
null,
2
)}
error={failedParse}
on:change={e => parseTestJSON(e)}
/>
</Tab> </Tab>
</Tabs> </Tabs>
</div>
</ModalContent> </ModalContent>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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