Merge pull request #630 from Budibase/async-workflow-blocks
Async loading external automation steps
This commit is contained in:
commit
403da250b9
|
@ -61,7 +61,7 @@
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 20px;
|
bottom: var(--spacing-xl);
|
||||||
right: 30px;
|
right: 30px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
|
@ -80,7 +80,7 @@
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
footer > button:first-child {
|
footer > button:first-child {
|
||||||
margin-right: 20px;
|
margin-right: var(--spacing-m);
|
||||||
}
|
}
|
||||||
|
|
||||||
.play-button.highlighted {
|
.play-button.highlighted {
|
||||||
|
|
|
@ -10,6 +10,6 @@
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
svg {
|
svg {
|
||||||
margin: 8px 0;
|
margin: var(--spacing-m) 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
Before Width: | Height: | Size: 290 B After Width: | Height: | Size: 303 B |
|
@ -29,10 +29,11 @@
|
||||||
.replace(/}}/, "}}</b>")
|
.replace(/}}/, "}}</b>")
|
||||||
|
|
||||||
// Extract schema paths for any input bindings
|
// Extract schema paths for any input bindings
|
||||||
const inputPaths = formattedTagline
|
let inputPaths = formattedTagline.match(/{{\s*\S+\s*}}/g) || []
|
||||||
.match(/{{\s*\S+\s*}}/g)
|
inputPaths = inputPaths.map(path => path.replace(/[{}]/g, "").trim())
|
||||||
.map(x => x.replace(/[{}]/g, "").trim())
|
const schemaPaths = inputPaths.map(path =>
|
||||||
const schemaPaths = inputPaths.map(x => x.replace(/\./g, ".properties."))
|
path.replace(/\./g, ".properties.")
|
||||||
|
)
|
||||||
|
|
||||||
// Replace any enum bindings with their pretty equivalents
|
// Replace any enum bindings with their pretty equivalents
|
||||||
schemaPaths.forEach((path, idx) => {
|
schemaPaths.forEach((path, idx) => {
|
||||||
|
|
|
@ -37,7 +37,7 @@
|
||||||
<style>
|
<style>
|
||||||
section {
|
section {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
padding: 20px 40px;
|
padding: 40px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
@ -7,7 +7,8 @@
|
||||||
let selected
|
let selected
|
||||||
|
|
||||||
$: selected = $automationStore.selectedBlock?.id === block.id
|
$: selected = $automationStore.selectedBlock?.id === block.id
|
||||||
$: steps = $automationStore.selectedAutomation?.automation?.definition?.steps ?? []
|
$: steps =
|
||||||
|
$automationStore.selectedAutomation?.automation?.definition?.steps ?? []
|
||||||
$: blockIdx = steps.findIndex(step => step.id === block.id)
|
$: blockIdx = steps.findIndex(step => step.id === block.id)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -46,22 +46,18 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
i {
|
|
||||||
color: #adaec4;
|
|
||||||
}
|
|
||||||
|
|
||||||
i:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
margin: var(--spacing-xl) 0 0 0;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.live {
|
i {
|
||||||
color: var(--primary);
|
color: var(--grey-6);
|
||||||
|
}
|
||||||
|
i.live {
|
||||||
|
color: var(--purple);
|
||||||
}
|
}
|
||||||
|
|
||||||
li {
|
li {
|
||||||
|
@ -70,51 +66,23 @@
|
||||||
|
|
||||||
.automation-item {
|
.automation-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
border-radius: 5px;
|
flex-direction: row;
|
||||||
padding-left: 12px;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 36px;
|
border-radius: var(--border-radius-m);
|
||||||
margin-bottom: 4px;
|
padding: var(--spacing-s) var(--spacing-m);
|
||||||
|
margin-bottom: var(--spacing-xs);
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
}
|
}
|
||||||
|
|
||||||
.automation-item i {
|
.automation-item i {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
margin-right: 10px;
|
margin-right: var(--spacing-m);
|
||||||
}
|
}
|
||||||
|
|
||||||
.automation-item:hover {
|
.automation-item:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background: var(--grey-1);
|
background: var(--grey-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.automation-item.selected {
|
.automation-item.selected {
|
||||||
background: var(--grey-2);
|
background: var(--grey-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.new-automation-button {
|
|
||||||
cursor: pointer;
|
|
||||||
border: 1px solid var(--grey-4);
|
|
||||||
border-radius: 3px;
|
|
||||||
width: 100%;
|
|
||||||
padding: 8px 16px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
background: white;
|
|
||||||
color: var(--ink);
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
transition: all 2ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
.new-automation-button:hover {
|
|
||||||
background: var(--grey-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
color: var(--ink);
|
|
||||||
font-size: 16px;
|
|
||||||
margin-right: 4px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -22,67 +22,74 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
<header>
|
<header>
|
||||||
<i class="ri-stackshare-line" />
|
<i class="ri-stackshare-line" />
|
||||||
Create Automation
|
Create Automation
|
||||||
</header>
|
</header>
|
||||||
<div>
|
<div class="content">
|
||||||
<Input bind:value={name} label="Name" />
|
<Input bind:value={name} label="Name" />
|
||||||
</div>
|
</div>
|
||||||
<footer>
|
<footer>
|
||||||
<a href="https://docs.budibase.com">
|
<a href="https://docs.budibase.com">
|
||||||
<i class="ri-information-line" />
|
<i class="ri-information-line" />
|
||||||
Learn about automations
|
<span>Learn about automations</span>
|
||||||
</a>
|
</a>
|
||||||
<ActionButton secondary on:click={onClosed}>Cancel</ActionButton>
|
<ActionButton secondary on:click={onClosed}>Cancel</ActionButton>
|
||||||
<ActionButton disabled={!valid} on:click={createAutomation}>Save</ActionButton>
|
<ActionButton disabled={!valid} on:click={createAutomation}>
|
||||||
|
Save
|
||||||
|
</ActionButton>
|
||||||
</footer>
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.container {
|
||||||
|
padding: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
|
||||||
header {
|
header {
|
||||||
font-size: 24px;
|
font-size: var(--font-size-xl);
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
padding: 30px;
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
header i {
|
header i {
|
||||||
margin-right: 10px;
|
margin-right: var(--spacing-m);
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
background: var(--blue-light);
|
background: var(--purple);
|
||||||
color: var(--grey-4);
|
color: var(--white);
|
||||||
padding: 8px;
|
padding: var(--spacing-s);
|
||||||
|
border-radius: var(--border-radius-m);
|
||||||
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
div {
|
.content {
|
||||||
padding: 0 30px 30px 30px;
|
padding: var(--spacing-xl) 0;
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-auto-flow: column;
|
grid-auto-flow: column;
|
||||||
grid-gap: 5px;
|
grid-gap: var(--spacing-m);
|
||||||
grid-auto-columns: 3fr 1fr 1fr;
|
grid-auto-columns: 3fr 1fr 1fr;
|
||||||
padding: 20px;
|
|
||||||
background: var(--grey-1);
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
footer a {
|
footer a {
|
||||||
color: var(--primary);
|
color: var(--ink);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
footer a span {
|
||||||
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
footer i {
|
footer i {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
margin-right: 10px;
|
margin-right: var(--spacing-m);
|
||||||
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
class="hoverable"
|
class="hoverable"
|
||||||
class:selected={selectedTab === 'ADD'}
|
class:selected={selectedTab === 'ADD'}
|
||||||
on:click={() => (selectedTab = 'ADD')}>
|
on:click={() => (selectedTab = 'ADD')}>
|
||||||
Add step
|
Steps
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</header>
|
</header>
|
||||||
|
@ -37,11 +37,11 @@
|
||||||
background: none;
|
background: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 20px;
|
margin-bottom: var(--spacing-xl);
|
||||||
}
|
}
|
||||||
|
|
||||||
.automation-header {
|
.automation-header {
|
||||||
margin-right: 20px;
|
margin-right: var(--spacing-xl);
|
||||||
}
|
}
|
||||||
|
|
||||||
span:not(.selected) {
|
span:not(.selected) {
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
function addBlockToAutomation() {
|
function addBlockToAutomation() {
|
||||||
automationStore.actions.addBlockToAutomation({
|
automationStore.actions.addBlockToAutomation({
|
||||||
...blockDefinition,
|
...blockDefinition,
|
||||||
args: blockDefinition.args || {},
|
inputs: blockDefinition.inputs || {},
|
||||||
stepId,
|
stepId,
|
||||||
type: blockType,
|
type: blockType,
|
||||||
})
|
})
|
||||||
|
@ -33,26 +33,15 @@
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 20px auto;
|
grid-template-columns: 20px auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-top: 16px;
|
margin-top: var(--spacing-s);
|
||||||
padding: 12px;
|
padding: var(--spacing-m);
|
||||||
border-radius: var(--border-radius-m);
|
border-radius: var(--border-radius-m);
|
||||||
}
|
}
|
||||||
|
|
||||||
.automation-block:hover {
|
.automation-block:hover {
|
||||||
background-color: var(--grey-1);
|
background-color: var(--grey-1);
|
||||||
}
|
}
|
||||||
|
.automation-block:first-child {
|
||||||
.automation-text {
|
margin-top: 0;
|
||||||
margin-left: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
height: 40px;
|
|
||||||
width: 40px;
|
|
||||||
background: var(--blue-light);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
i {
|
i {
|
||||||
|
@ -60,14 +49,16 @@
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
h4 {
|
.automation-text {
|
||||||
|
margin-left: 16px;
|
||||||
|
}
|
||||||
|
.automation-text h4 {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
.automation-text p {
|
||||||
p {
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--grey-7);
|
color: var(--grey-7);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { sortBy } from "lodash/fp"
|
||||||
import { automationStore } from "builderStore"
|
import { automationStore } from "builderStore"
|
||||||
import AutomationBlock from "./AutomationBlock.svelte"
|
import AutomationBlock from "./AutomationBlock.svelte"
|
||||||
import FlatButtonGroup from "components/userInterface/FlatButtonGroup.svelte"
|
import FlatButtonGroup from "components/userInterface/FlatButtonGroup.svelte"
|
||||||
|
|
||||||
let selectedTab = "TRIGGER"
|
let selectedTab = "TRIGGER"
|
||||||
let buttonProps = []
|
let buttonProps = []
|
||||||
$: blocks = Object.entries($automationStore.blockDefinitions[selectedTab])
|
$: blocks = sortBy(entry => entry[1].name)(
|
||||||
|
Object.entries($automationStore.blockDefinitions[selectedTab])
|
||||||
|
)
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
if ($automationStore.selectedAutomation.hasTrigger()) {
|
if ($automationStore.selectedAutomation.hasTrigger()) {
|
||||||
|
@ -37,3 +40,9 @@
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#blocklist {
|
||||||
|
margin-top: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -66,7 +66,7 @@
|
||||||
<RecordSelector bind:value={block.inputs[key]} {bindings} />
|
<RecordSelector bind:value={block.inputs[key]} {bindings} />
|
||||||
{:else if value.type === 'string' || value.type === 'number'}
|
{:else if value.type === 'string' || value.type === 'number'}
|
||||||
<BindableInput
|
<BindableInput
|
||||||
type={value.type}
|
type="string"
|
||||||
thin
|
thin
|
||||||
bind:value={block.inputs[key]}
|
bind:value={block.inputs[key]}
|
||||||
{bindings} />
|
{bindings} />
|
||||||
|
|
|
@ -26,7 +26,8 @@
|
||||||
</header>
|
</header>
|
||||||
<div>
|
<div>
|
||||||
<p>
|
<p>
|
||||||
Are you sure you want to delete this automation? This action can't be undone.
|
Are you sure you want to delete this automation? This action can't be
|
||||||
|
undone.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<footer>
|
<footer>
|
||||||
|
|
|
@ -44,7 +44,7 @@
|
||||||
thin
|
thin
|
||||||
bind:value={value[field]}
|
bind:value={value[field]}
|
||||||
label={field}
|
label={field}
|
||||||
type={schema.type}
|
type="string"
|
||||||
{bindings} />
|
{bindings} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -24,7 +24,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteAutomationBlock() {
|
function deleteAutomationBlock() {
|
||||||
automationStore.actions.deleteAutomationBlock($automationStore.selectedBlock)
|
automationStore.actions.deleteAutomationBlock(
|
||||||
|
$automationStore.selectedBlock
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function testAutomation() {
|
async function testAutomation() {
|
||||||
|
@ -56,10 +58,24 @@
|
||||||
Setup
|
Setup
|
||||||
</span>
|
</span>
|
||||||
</header>
|
</header>
|
||||||
|
<div class="content">
|
||||||
{#if $automationStore.selectedBlock}
|
{#if $automationStore.selectedBlock}
|
||||||
<AutomationBlockSetup bind:block={$automationStore.selectedBlock} />
|
<AutomationBlockSetup bind:block={$automationStore.selectedBlock} />
|
||||||
|
{:else if $automationStore.selectedAutomation}
|
||||||
|
<div class="block-label">
|
||||||
|
Automation
|
||||||
|
<b>{automation.name}</b>
|
||||||
|
</div>
|
||||||
|
<Button secondary wide on:click={testAutomation}>Test Automation</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<Button green wide data-cy="save-automation-setup" on:click={saveAutomation}>
|
{#if $automationStore.selectedBlock}
|
||||||
|
<Button
|
||||||
|
green
|
||||||
|
wide
|
||||||
|
data-cy="save-automation-setup"
|
||||||
|
on:click={saveAutomation}>
|
||||||
Save Automation
|
Save Automation
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
@ -67,19 +83,9 @@
|
||||||
red
|
red
|
||||||
wide
|
wide
|
||||||
on:click={deleteAutomationBlock}>
|
on:click={deleteAutomationBlock}>
|
||||||
Delete Block
|
Delete Step
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
|
||||||
{:else if $automationStore.selectedAutomation}
|
{:else if $automationStore.selectedAutomation}
|
||||||
<div class="panel">
|
|
||||||
<div class="panel-body">
|
|
||||||
<div class="block-label">
|
|
||||||
Automation
|
|
||||||
<b>{automation.name}</b>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Button secondary wide on:click={testAutomation}>Test Automation</Button>
|
|
||||||
<div class="buttons">
|
|
||||||
<Button
|
<Button
|
||||||
green
|
green
|
||||||
wide
|
wide
|
||||||
|
@ -88,9 +94,9 @@
|
||||||
Save Automation
|
Save Automation
|
||||||
</Button>
|
</Button>
|
||||||
<Button red wide on:click={deleteAutomation}>Delete Automation</Button>
|
<Button red wide on:click={deleteAutomation}>Delete Automation</Button>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -98,29 +104,24 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
justify-content: space-between;
|
justify-content: flex-start;
|
||||||
}
|
align-items: stretch;
|
||||||
|
|
||||||
.panel-body {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
header {
|
header {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-family: inter;
|
font-family: inter, sans-serif;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 20px;
|
margin-bottom: var(--spacing-xl);
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
}
|
}
|
||||||
|
header > span {
|
||||||
|
color: var(--grey-5);
|
||||||
|
margin-right: var(--spacing-xl);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
.selected {
|
.selected {
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
}
|
}
|
||||||
|
@ -129,31 +130,15 @@
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: var(--grey-7);
|
color: var(--grey-7);
|
||||||
margin-bottom: 20px;
|
margin-bottom: var(--spacing-xl);
|
||||||
}
|
}
|
||||||
|
|
||||||
header > span {
|
.content {
|
||||||
color: var(--grey-5);
|
flex: 1 0 auto;
|
||||||
margin-right: 20px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 14px;
|
|
||||||
color: var(--ink);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttons {
|
.buttons {
|
||||||
position: absolute;
|
|
||||||
bottom: 20px;
|
|
||||||
display: grid;
|
display: grid;
|
||||||
width: 260px;
|
gap: var(--spacing-m);
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.access-level label {
|
|
||||||
font-weight: normal;
|
|
||||||
color: var(--ink);
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,52 +1,39 @@
|
||||||
<!-- routify:options index=3 -->
|
|
||||||
<script>
|
<script>
|
||||||
import { automationStore } from "builderStore"
|
import { automationStore } from "builderStore"
|
||||||
import { AutomationPanel, SetupPanel } from "components/automation"
|
import { AutomationPanel, SetupPanel } from "components/automation"
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- routify:options index=3 -->
|
||||||
<div class="root">
|
<div class="root">
|
||||||
<div class="nav">
|
<div class="nav">
|
||||||
<div class="inner">
|
|
||||||
<AutomationPanel />
|
<AutomationPanel />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
{#if $automationStore.selectedAutomation}
|
{#if $automationStore.selectedAutomation}
|
||||||
<div class="nav">
|
<div class="nav">
|
||||||
<div class="inner">
|
|
||||||
<SetupPanel />
|
<SetupPanel />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.content {
|
.content {
|
||||||
position: relative;
|
position: relative;
|
||||||
background: var(--grey-1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.root {
|
.root {
|
||||||
height: 100%;
|
height: calc(100% - 60px);
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 300px minmax(0, 1fr) 300px;
|
grid-template-columns: 300px minmax(510px, 1fr) 300px;
|
||||||
background: var(--grey-1);
|
background: var(--grey-1);
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav {
|
.nav {
|
||||||
overflow: auto;
|
overflow-y: auto;
|
||||||
width: 300px;
|
|
||||||
background: var(--white);
|
background: var(--white);
|
||||||
}
|
padding: var(--spacing-xl);
|
||||||
|
|
||||||
.inner {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
<!-- routify:options index=1 -->
|
|
||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import { store, backendUiStore } from "builderStore"
|
import { store, backendUiStore } from "builderStore"
|
||||||
|
@ -6,6 +5,7 @@
|
||||||
import ModelNavigator from "components/nav/ModelNavigator/ModelNavigator.svelte"
|
import ModelNavigator from "components/nav/ModelNavigator/ModelNavigator.svelte"
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- routify:options index=1 -->
|
||||||
<div class="root">
|
<div class="root">
|
||||||
<div class="nav">
|
<div class="nav">
|
||||||
<ModelNavigator />
|
<ModelNavigator />
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
<!-- routify:options index=1 -->
|
|
||||||
<script>
|
<script>
|
||||||
import { store, backendUiStore } from "builderStore"
|
import { store, backendUiStore } from "builderStore"
|
||||||
import { goto } from "@sveltech/routify"
|
import { goto } from "@sveltech/routify"
|
||||||
|
@ -38,6 +37,7 @@
|
||||||
const lastPartOfName = c => (c ? last(c.split("/")) : "")
|
const lastPartOfName = c => (c ? last(c.split("/")) : "")
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- routify:options index=1 -->
|
||||||
<div class="root">
|
<div class="root">
|
||||||
|
|
||||||
<div class="ui-nav">
|
<div class="ui-nav">
|
||||||
|
|
|
@ -50,6 +50,7 @@
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"chmodr": "^1.2.0",
|
"chmodr": "^1.2.0",
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
|
"download": "^8.0.0",
|
||||||
"electron-is-dev": "^1.2.0",
|
"electron-is-dev": "^1.2.0",
|
||||||
"electron-unhandled": "^3.0.2",
|
"electron-unhandled": "^3.0.2",
|
||||||
"electron-updater": "^4.3.1",
|
"electron-updater": "^4.3.1",
|
||||||
|
|
|
@ -90,7 +90,7 @@ exports.destroy = async function(ctx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.getActionList = async function(ctx) {
|
exports.getActionList = async function(ctx) {
|
||||||
ctx.body = actions.BUILTIN_DEFINITIONS
|
ctx.body = actions.DEFINITIONS
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.getTriggerList = async function(ctx) {
|
exports.getTriggerList = async function(ctx) {
|
||||||
|
@ -105,7 +105,7 @@ module.exports.getDefinitionList = async function(ctx) {
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
logic: logic.BUILTIN_DEFINITIONS,
|
logic: logic.BUILTIN_DEFINITIONS,
|
||||||
trigger: triggers.BUILTIN_DEFINITIONS,
|
trigger: triggers.BUILTIN_DEFINITIONS,
|
||||||
action: actions.BUILTIN_DEFINITIONS,
|
action: actions.DEFINITIONS,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,18 +3,18 @@ const validateJs = require("validate.js")
|
||||||
const newid = require("../../db/newid")
|
const newid = require("../../db/newid")
|
||||||
|
|
||||||
function emitEvent(eventType, ctx, record) {
|
function emitEvent(eventType, ctx, record) {
|
||||||
// add syntactic sugar for mustache later
|
let event = {
|
||||||
if (record._id) {
|
|
||||||
record.id = record._id
|
|
||||||
}
|
|
||||||
if (record._rev) {
|
|
||||||
record.revision = record._rev
|
|
||||||
}
|
|
||||||
ctx.eventEmitter &&
|
|
||||||
ctx.eventEmitter.emit(eventType, {
|
|
||||||
record,
|
record,
|
||||||
instanceId: ctx.user.instanceId,
|
instanceId: ctx.user.instanceId,
|
||||||
})
|
}
|
||||||
|
// add syntactic sugar for mustache later
|
||||||
|
if (record._id) {
|
||||||
|
event.id = record._id
|
||||||
|
}
|
||||||
|
if (record._rev) {
|
||||||
|
event.revision = record._rev
|
||||||
|
}
|
||||||
|
ctx.eventEmitter && ctx.eventEmitter.emit(eventType, event)
|
||||||
}
|
}
|
||||||
|
|
||||||
validateJs.extend(validateJs.validators.datetime, {
|
validateJs.extend(validateJs.validators.datetime, {
|
||||||
|
|
|
@ -66,6 +66,15 @@ describe("/automations", () => {
|
||||||
automation = { ...automation, ...TEST_AUTOMATION }
|
automation = { ...automation, ...TEST_AUTOMATION }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const triggerWorkflow = async (automationId) => {
|
||||||
|
return await request
|
||||||
|
.post(`/api/automations/${automationId}/trigger`)
|
||||||
|
.send({ name: "Test", description: "TEST" })
|
||||||
|
.set(defaultHeaders(app._id, instance._id))
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(200)
|
||||||
|
}
|
||||||
|
|
||||||
describe("get definitions", () => {
|
describe("get definitions", () => {
|
||||||
it("returns a list of definitions for actions", async () => {
|
it("returns a list of definitions for actions", async () => {
|
||||||
const res = await request
|
const res = await request
|
||||||
|
@ -160,16 +169,15 @@ describe("/automations", () => {
|
||||||
TEST_AUTOMATION.definition.trigger.inputs.modelId = model._id
|
TEST_AUTOMATION.definition.trigger.inputs.modelId = model._id
|
||||||
TEST_AUTOMATION.definition.steps[0].inputs.record.modelId = model._id
|
TEST_AUTOMATION.definition.steps[0].inputs.record.modelId = model._id
|
||||||
await createAutomation()
|
await createAutomation()
|
||||||
const res = await request
|
// this looks a bit mad but we don't actually have a way to wait for a response from the automation to
|
||||||
.post(`/api/automations/${automation._id}/trigger`)
|
// know that it has finished all of its actions - this is currently the best way
|
||||||
.send({ name: "Test" })
|
// also when this runs in CI it is very temper-mental so for now trying to make run stable by repeating until it works
|
||||||
.set(defaultHeaders(app._id, instance._id))
|
// TODO: update when workflow logs are a thing
|
||||||
.expect('Content-Type', /json/)
|
for (let tries = 0; tries < MAX_RETRIES; tries++) {
|
||||||
.expect(200)
|
const res = await triggerWorkflow(automation._id)
|
||||||
expect(res.body.message).toEqual(`Automation ${automation._id} has been triggered.`)
|
expect(res.body.message).toEqual(`Automation ${automation._id} has been triggered.`)
|
||||||
expect(res.body.automation.name).toEqual(TEST_AUTOMATION.name)
|
expect(res.body.automation.name).toEqual(TEST_AUTOMATION.name)
|
||||||
// wait for automation to complete in background
|
await delay(500)
|
||||||
for (let tries = 0; tries < MAX_RETRIES; tries++) {
|
|
||||||
let elements = await getAllFromModel(request, app._id, instance._id, model._id)
|
let elements = await getAllFromModel(request, app._id, instance._id, model._id)
|
||||||
// don't test it unless there are values to test
|
// don't test it unless there are values to test
|
||||||
if (elements.length === 1) {
|
if (elements.length === 1) {
|
||||||
|
@ -178,7 +186,6 @@ describe("/automations", () => {
|
||||||
expect(elements[0].description).toEqual("TEST")
|
expect(elements[0].description).toEqual("TEST")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await delay(500)
|
|
||||||
}
|
}
|
||||||
throw "Failed to find the records"
|
throw "Failed to find the records"
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,27 +1,91 @@
|
||||||
const sendEmail = require("./steps/sendEmail")
|
const sendEmail = require("./steps/sendEmail")
|
||||||
const saveRecord = require("./steps/saveRecord")
|
const saveRecord = require("./steps/saveRecord")
|
||||||
|
const updateRecord = require("./steps/updateRecord")
|
||||||
const deleteRecord = require("./steps/deleteRecord")
|
const deleteRecord = require("./steps/deleteRecord")
|
||||||
const createUser = require("./steps/createUser")
|
const createUser = require("./steps/createUser")
|
||||||
|
const environment = require("../environment")
|
||||||
|
const download = require("download")
|
||||||
|
const fetch = require("node-fetch")
|
||||||
|
const path = require("path")
|
||||||
|
const os = require("os")
|
||||||
|
const fs = require("fs")
|
||||||
|
const Sentry = require("@sentry/node")
|
||||||
|
|
||||||
|
const DEFAULT_BUCKET =
|
||||||
|
"https://prod-budi-automations.s3-eu-west-1.amazonaws.com"
|
||||||
|
const DEFAULT_DIRECTORY = ".budibase-automations"
|
||||||
|
const AUTOMATION_MANIFEST = "manifest.json"
|
||||||
const BUILTIN_ACTIONS = {
|
const BUILTIN_ACTIONS = {
|
||||||
SEND_EMAIL: sendEmail.run,
|
SEND_EMAIL: sendEmail.run,
|
||||||
SAVE_RECORD: saveRecord.run,
|
SAVE_RECORD: saveRecord.run,
|
||||||
|
UPDATE_RECORD: updateRecord.run,
|
||||||
DELETE_RECORD: deleteRecord.run,
|
DELETE_RECORD: deleteRecord.run,
|
||||||
CREATE_USER: createUser.run,
|
CREATE_USER: createUser.run,
|
||||||
}
|
}
|
||||||
|
|
||||||
const BUILTIN_DEFINITIONS = {
|
const BUILTIN_DEFINITIONS = {
|
||||||
SEND_EMAIL: sendEmail.definition,
|
SEND_EMAIL: sendEmail.definition,
|
||||||
SAVE_RECORD: saveRecord.definition,
|
SAVE_RECORD: saveRecord.definition,
|
||||||
|
UPDATE_RECORD: updateRecord.definition,
|
||||||
DELETE_RECORD: deleteRecord.definition,
|
DELETE_RECORD: deleteRecord.definition,
|
||||||
CREATE_USER: createUser.definition,
|
CREATE_USER: createUser.definition,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let AUTOMATION_BUCKET = environment.AUTOMATION_BUCKET
|
||||||
|
let AUTOMATION_DIRECTORY = environment.AUTOMATION_DIRECTORY
|
||||||
|
let MANIFEST = null
|
||||||
|
|
||||||
|
function buildBundleName(pkgName, version) {
|
||||||
|
return `${pkgName}@${version}.min.js`
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadPackage(name, version, bundleName) {
|
||||||
|
await download(
|
||||||
|
`${AUTOMATION_BUCKET}/${name}/${version}/${bundleName}`,
|
||||||
|
AUTOMATION_DIRECTORY
|
||||||
|
)
|
||||||
|
return require(path.join(AUTOMATION_DIRECTORY, bundleName))
|
||||||
|
}
|
||||||
|
|
||||||
module.exports.getAction = async function(actionName) {
|
module.exports.getAction = async function(actionName) {
|
||||||
if (BUILTIN_ACTIONS[actionName] != null) {
|
if (BUILTIN_ACTIONS[actionName] != null) {
|
||||||
return BUILTIN_ACTIONS[actionName]
|
return BUILTIN_ACTIONS[actionName]
|
||||||
}
|
}
|
||||||
// TODO: load async actions here
|
// env setup to get async packages
|
||||||
|
if (!MANIFEST || !MANIFEST.packages || !MANIFEST.packages[actionName]) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const pkg = MANIFEST.packages[actionName]
|
||||||
|
const bundleName = buildBundleName(pkg.stepId, pkg.version)
|
||||||
|
try {
|
||||||
|
return require(path.join(AUTOMATION_DIRECTORY, bundleName))
|
||||||
|
} catch (err) {
|
||||||
|
return downloadPackage(pkg.stepId, pkg.version, bundleName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module.exports.init = async function() {
|
||||||
|
// set defaults
|
||||||
|
if (!AUTOMATION_DIRECTORY) {
|
||||||
|
AUTOMATION_DIRECTORY = path.join(os.homedir(), DEFAULT_DIRECTORY)
|
||||||
|
}
|
||||||
|
if (!AUTOMATION_BUCKET) {
|
||||||
|
AUTOMATION_BUCKET = DEFAULT_BUCKET
|
||||||
|
}
|
||||||
|
if (!fs.existsSync(AUTOMATION_DIRECTORY)) {
|
||||||
|
fs.mkdirSync(AUTOMATION_DIRECTORY, { recursive: true })
|
||||||
|
}
|
||||||
|
// env setup to get async packages
|
||||||
|
try {
|
||||||
|
let response = await fetch(`${AUTOMATION_BUCKET}/${AUTOMATION_MANIFEST}`)
|
||||||
|
MANIFEST = await response.json()
|
||||||
|
module.exports.DEFINITIONS =
|
||||||
|
MANIFEST && MANIFEST.packages
|
||||||
|
? Object.assign(MANIFEST.packages, BUILTIN_DEFINITIONS)
|
||||||
|
: BUILTIN_DEFINITIONS
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.captureException(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.DEFINITIONS = BUILTIN_DEFINITIONS
|
||||||
module.exports.BUILTIN_DEFINITIONS = BUILTIN_DEFINITIONS
|
module.exports.BUILTIN_DEFINITIONS = BUILTIN_DEFINITIONS
|
||||||
|
|
|
@ -0,0 +1,116 @@
|
||||||
|
const CouchDB = require("../db")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When running mustache statements to execute on the context of the automation it possible user's may input mustache
|
||||||
|
* in a few different forms, some of which are invalid but are logically valid. An example of this would be the mustache
|
||||||
|
* statement "{{steps[0].revision}}" here it is obvious the user is attempting to access an array or object using array
|
||||||
|
* like operators. These are not supported by Mustache and therefore the statement will fail. This function will clean up
|
||||||
|
* the mustache statement so it instead reads as "{{steps.0.revision}}" which is valid and will work. It may also be expanded
|
||||||
|
* to include any other mustache statement cleanup that has been deemed necessary for the system.
|
||||||
|
*
|
||||||
|
* @param {string} string The string which *may* contain mustache statements, it is OK if it does not contain any.
|
||||||
|
* @returns {string} The string that was input with cleaned up mustache statements as required.
|
||||||
|
*/
|
||||||
|
module.exports.cleanMustache = string => {
|
||||||
|
let charToReplace = {
|
||||||
|
"[": ".",
|
||||||
|
"]": "",
|
||||||
|
}
|
||||||
|
let regex = new RegExp(/{{[^}}]*}}/g)
|
||||||
|
let matches = string.match(regex)
|
||||||
|
if (matches == null) {
|
||||||
|
return string
|
||||||
|
}
|
||||||
|
for (let match of matches) {
|
||||||
|
let baseIdx = string.indexOf(match)
|
||||||
|
for (let key of Object.keys(charToReplace)) {
|
||||||
|
let idxChar = match.indexOf(key)
|
||||||
|
if (idxChar !== -1) {
|
||||||
|
string =
|
||||||
|
string.slice(baseIdx, baseIdx + idxChar) +
|
||||||
|
charToReplace[key] +
|
||||||
|
string.slice(baseIdx + idxChar + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When values are input to the system generally they will be of type string as this is required for mustache. This can
|
||||||
|
* generate some odd scenarios as the Schema of the automation requires a number but the builder might supply a string
|
||||||
|
* with mustache syntax to get the number from the rest of the context. To support this the server has to make sure that
|
||||||
|
* the post mustache statement can be cast into the correct type, this function does this for numbers and booleans.
|
||||||
|
*
|
||||||
|
* @param {object} inputs An object of inputs, please note this will not recurse down into any objects within, it simply
|
||||||
|
* cleanses the top level inputs, however it can be used by recursively calling it deeper into the object structures if
|
||||||
|
* the schema is known.
|
||||||
|
* @param {object} schema The defined schema of the inputs, in the form of JSON schema. The schema definition of an
|
||||||
|
* automation is the likely use case of this, however validate.js syntax can be converted closely enough to use this by
|
||||||
|
* wrapping the schema properties in a top level "properties" object.
|
||||||
|
* @returns {object} The inputs object which has had all the various types supported by this function converted to their
|
||||||
|
* primitive types.
|
||||||
|
*/
|
||||||
|
module.exports.cleanInputValues = (inputs, schema) => {
|
||||||
|
if (schema == null) {
|
||||||
|
return inputs
|
||||||
|
}
|
||||||
|
for (let inputKey of Object.keys(inputs)) {
|
||||||
|
let input = inputs[inputKey]
|
||||||
|
if (typeof input !== "string") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
let propSchema = schema.properties[inputKey]
|
||||||
|
if (!propSchema) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (propSchema.type === "boolean") {
|
||||||
|
let lcInput = input.toLowerCase()
|
||||||
|
if (lcInput === "true") {
|
||||||
|
inputs[inputKey] = true
|
||||||
|
}
|
||||||
|
if (lcInput === "false") {
|
||||||
|
inputs[inputKey] = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (propSchema.type === "number") {
|
||||||
|
let floatInput = parseFloat(input)
|
||||||
|
if (!isNaN(floatInput)) {
|
||||||
|
inputs[inputKey] = floatInput
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return inputs
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a record input like a save or update record we need to clean the inputs against a schema that is not part of
|
||||||
|
* the automation but is instead part of the Table/Model. This function will get the model schema and use it to instead
|
||||||
|
* perform the cleanInputValues function on the input record.
|
||||||
|
*
|
||||||
|
* @param {string} instanceId The instance which the Table/Model is contained under.
|
||||||
|
* @param {string} modelId The ID of the Table/Model which the schema is to be retrieved for.
|
||||||
|
* @param {object} record The input record structure which requires clean-up after having been through mustache statements.
|
||||||
|
* @returns {Promise<Object>} The cleaned up records object, will should now have all the required primitive types.
|
||||||
|
*/
|
||||||
|
module.exports.cleanUpRecord = async (instanceId, modelId, record) => {
|
||||||
|
const db = new CouchDB(instanceId)
|
||||||
|
const model = await db.get(modelId)
|
||||||
|
|
||||||
|
return module.exports.cleanInputValues(record, { properties: model.schema })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A utility function for the cleanUpRecord, which can be used if only the record ID is known (not the model ID) to clean
|
||||||
|
* up a record after mustache statements have been replaced. This is specifically useful for the update record action.
|
||||||
|
*
|
||||||
|
* @param {string} instanceId The instance which the Table/Model is contained under.
|
||||||
|
* @param {string} recordId The ID of the record from which the modelId will be extracted, to get the Table/Model schema.
|
||||||
|
* @param {object} record The input record structure which requires clean-up after having been through mustache statements.
|
||||||
|
* @returns {Promise<Object>} The cleaned up records object, which will now have all the required primitive types.
|
||||||
|
*/
|
||||||
|
module.exports.cleanUpRecordById = async (instanceId, recordId, record) => {
|
||||||
|
const db = new CouchDB(instanceId)
|
||||||
|
const foundRecord = await db.get(recordId)
|
||||||
|
return module.exports.cleanUpRecord(instanceId, foundRecord.modelId, record)
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
const triggers = require("./triggers")
|
const triggers = require("./triggers")
|
||||||
|
const actions = require("./actions")
|
||||||
const environment = require("../environment")
|
const environment = require("../environment")
|
||||||
const workerFarm = require("worker-farm")
|
const workerFarm = require("worker-farm")
|
||||||
const singleThread = require("./thread")
|
const singleThread = require("./thread")
|
||||||
|
@ -21,6 +22,7 @@ function runWorker(job) {
|
||||||
* This module is built purely to kick off the worker farm and manage the inputs/outputs
|
* This module is built purely to kick off the worker farm and manage the inputs/outputs
|
||||||
*/
|
*/
|
||||||
module.exports.init = function() {
|
module.exports.init = function() {
|
||||||
|
actions.init().then(() => {
|
||||||
triggers.automationQueue.process(async job => {
|
triggers.automationQueue.process(async job => {
|
||||||
if (environment.BUDIBASE_ENVIRONMENT === "PRODUCTION") {
|
if (environment.BUDIBASE_ENVIRONMENT === "PRODUCTION") {
|
||||||
await runWorker(job)
|
await runWorker(job)
|
||||||
|
@ -28,4 +30,5 @@ module.exports.init = function() {
|
||||||
await singleThread(job)
|
await singleThread(job)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
const recordController = require("../../api/controllers/record")
|
const recordController = require("../../api/controllers/record")
|
||||||
|
const automationUtils = require("../automationUtils")
|
||||||
|
|
||||||
module.exports.definition = {
|
module.exports.definition = {
|
||||||
name: "Save Record",
|
name: "Save Record",
|
||||||
|
@ -60,6 +61,11 @@ module.exports.run = async function({ inputs, instanceId }) {
|
||||||
if (inputs.record == null || inputs.record.modelId == null) {
|
if (inputs.record == null || inputs.record.modelId == null) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
inputs.record = await automationUtils.cleanUpRecord(
|
||||||
|
instanceId,
|
||||||
|
inputs.record.modelId,
|
||||||
|
inputs.record
|
||||||
|
)
|
||||||
// have to clean up the record, remove the model from it
|
// have to clean up the record, remove the model from it
|
||||||
const ctx = {
|
const ctx = {
|
||||||
params: {
|
params: {
|
||||||
|
|
|
@ -0,0 +1,100 @@
|
||||||
|
const recordController = require("../../api/controllers/record")
|
||||||
|
const automationUtils = require("../automationUtils")
|
||||||
|
|
||||||
|
module.exports.definition = {
|
||||||
|
name: "Update Record",
|
||||||
|
tagline: "Update a {{inputs.enriched.model.name}} record",
|
||||||
|
icon: "ri-refresh-fill",
|
||||||
|
description: "Update a record to your database",
|
||||||
|
type: "ACTION",
|
||||||
|
stepId: "UPDATE_RECORD",
|
||||||
|
inputs: {},
|
||||||
|
schema: {
|
||||||
|
inputs: {
|
||||||
|
properties: {
|
||||||
|
record: {
|
||||||
|
type: "object",
|
||||||
|
customType: "record",
|
||||||
|
title: "Record",
|
||||||
|
},
|
||||||
|
recordId: {
|
||||||
|
type: "string",
|
||||||
|
title: "Record ID",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["record", "recordId"],
|
||||||
|
},
|
||||||
|
outputs: {
|
||||||
|
properties: {
|
||||||
|
record: {
|
||||||
|
type: "object",
|
||||||
|
customType: "record",
|
||||||
|
description: "The updated record",
|
||||||
|
},
|
||||||
|
response: {
|
||||||
|
type: "object",
|
||||||
|
description: "The response from the table",
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
type: "boolean",
|
||||||
|
description: "Whether the action was successful",
|
||||||
|
},
|
||||||
|
id: {
|
||||||
|
type: "string",
|
||||||
|
description: "The identifier of the updated record",
|
||||||
|
},
|
||||||
|
revision: {
|
||||||
|
type: "string",
|
||||||
|
description: "The revision of the updated record",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["success", "id", "revision"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.run = async function({ inputs, instanceId }) {
|
||||||
|
if (inputs.recordId == null || inputs.record == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
inputs.record = await automationUtils.cleanUpRecordById(
|
||||||
|
instanceId,
|
||||||
|
inputs.recordId,
|
||||||
|
inputs.record
|
||||||
|
)
|
||||||
|
// clear any falsy properties so that they aren't updated
|
||||||
|
for (let propKey of Object.keys(inputs.record)) {
|
||||||
|
if (!inputs.record[propKey] || inputs.record[propKey] === "") {
|
||||||
|
delete inputs.record[propKey]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// have to clean up the record, remove the model from it
|
||||||
|
const ctx = {
|
||||||
|
params: {
|
||||||
|
id: inputs.recordId,
|
||||||
|
},
|
||||||
|
request: {
|
||||||
|
body: inputs.record,
|
||||||
|
},
|
||||||
|
user: { instanceId },
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await recordController.patch(ctx)
|
||||||
|
return {
|
||||||
|
record: ctx.body,
|
||||||
|
response: ctx.message,
|
||||||
|
id: ctx.body._id,
|
||||||
|
revision: ctx.body._rev,
|
||||||
|
success: ctx.status === 200,
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
response: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,39 +1,15 @@
|
||||||
const mustache = require("mustache")
|
const mustache = require("mustache")
|
||||||
const actions = require("./actions")
|
const actions = require("./actions")
|
||||||
const logic = require("./logic")
|
const logic = require("./logic")
|
||||||
|
const automationUtils = require("./automationUtils")
|
||||||
|
|
||||||
const FILTER_STEP_ID = logic.BUILTIN_DEFINITIONS.FILTER.stepId
|
const FILTER_STEP_ID = logic.BUILTIN_DEFINITIONS.FILTER.stepId
|
||||||
|
|
||||||
function cleanMustache(string) {
|
|
||||||
let charToReplace = {
|
|
||||||
"[": ".",
|
|
||||||
"]": "",
|
|
||||||
}
|
|
||||||
let regex = new RegExp(/{{[^}}]*}}/g)
|
|
||||||
let matches = string.match(regex)
|
|
||||||
if (matches == null) {
|
|
||||||
return string
|
|
||||||
}
|
|
||||||
for (let match of matches) {
|
|
||||||
let baseIdx = string.indexOf(match)
|
|
||||||
for (let key of Object.keys(charToReplace)) {
|
|
||||||
let idxChar = match.indexOf(key)
|
|
||||||
if (idxChar !== -1) {
|
|
||||||
string =
|
|
||||||
string.slice(baseIdx, baseIdx + idxChar) +
|
|
||||||
charToReplace[key] +
|
|
||||||
string.slice(baseIdx + idxChar + 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return string
|
|
||||||
}
|
|
||||||
|
|
||||||
function recurseMustache(inputs, context) {
|
function recurseMustache(inputs, context) {
|
||||||
for (let key of Object.keys(inputs)) {
|
for (let key of Object.keys(inputs)) {
|
||||||
let val = inputs[key]
|
let val = inputs[key]
|
||||||
if (typeof val === "string") {
|
if (typeof val === "string") {
|
||||||
val = cleanMustache(inputs[key])
|
val = automationUtils.cleanMustache(inputs[key])
|
||||||
inputs[key] = mustache.render(val, context)
|
inputs[key] = mustache.render(val, context)
|
||||||
}
|
}
|
||||||
// this covers objects and arrays
|
// this covers objects and arrays
|
||||||
|
@ -77,7 +53,12 @@ class Orchestrator {
|
||||||
for (let step of automation.definition.steps) {
|
for (let step of automation.definition.steps) {
|
||||||
let stepFn = await this.getStepFunctionality(step.type, step.stepId)
|
let stepFn = await this.getStepFunctionality(step.type, step.stepId)
|
||||||
step.inputs = recurseMustache(step.inputs, this._context)
|
step.inputs = recurseMustache(step.inputs, this._context)
|
||||||
|
step.inputs = automationUtils.cleanInputValues(
|
||||||
|
step.inputs,
|
||||||
|
step.schema.inputs
|
||||||
|
)
|
||||||
// instanceId is always passed
|
// instanceId is always passed
|
||||||
|
try {
|
||||||
const outputs = await stepFn({
|
const outputs = await stepFn({
|
||||||
inputs: step.inputs,
|
inputs: step.inputs,
|
||||||
instanceId: this._instanceId,
|
instanceId: this._instanceId,
|
||||||
|
@ -86,6 +67,9 @@ class Orchestrator {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
this._context.steps.push(outputs)
|
this._context.steps.push(outputs)
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Automation error - ${step.stepId} - ${err}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,8 +36,16 @@ const BUILTIN_DEFINITIONS = {
|
||||||
customType: "record",
|
customType: "record",
|
||||||
description: "The new record that was saved",
|
description: "The new record that was saved",
|
||||||
},
|
},
|
||||||
|
id: {
|
||||||
|
type: "string",
|
||||||
|
description: "Record ID - can be used for updating",
|
||||||
},
|
},
|
||||||
required: ["record"],
|
revision: {
|
||||||
|
type: "string",
|
||||||
|
description: "Revision of record",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["record", "id"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
type: "TRIGGER",
|
type: "TRIGGER",
|
||||||
|
@ -69,7 +77,7 @@ const BUILTIN_DEFINITIONS = {
|
||||||
description: "The record that was deleted",
|
description: "The record that was deleted",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
required: ["record"],
|
required: ["record", "id"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
type: "TRIGGER",
|
type: "TRIGGER",
|
||||||
|
@ -124,6 +132,7 @@ async function fillRecordOutput(automation, params) {
|
||||||
const db = new CouchDB(params.instanceId)
|
const db = new CouchDB(params.instanceId)
|
||||||
try {
|
try {
|
||||||
let model = await db.get(modelId)
|
let model = await db.get(modelId)
|
||||||
|
let record = {}
|
||||||
for (let schemaKey of Object.keys(model.schema)) {
|
for (let schemaKey of Object.keys(model.schema)) {
|
||||||
if (params[schemaKey] != null) {
|
if (params[schemaKey] != null) {
|
||||||
continue
|
continue
|
||||||
|
@ -131,19 +140,20 @@ async function fillRecordOutput(automation, params) {
|
||||||
let propSchema = model.schema[schemaKey]
|
let propSchema = model.schema[schemaKey]
|
||||||
switch (propSchema.constraints.type) {
|
switch (propSchema.constraints.type) {
|
||||||
case "string":
|
case "string":
|
||||||
params[schemaKey] = FAKE_STRING
|
record[schemaKey] = FAKE_STRING
|
||||||
break
|
break
|
||||||
case "boolean":
|
case "boolean":
|
||||||
params[schemaKey] = FAKE_BOOL
|
record[schemaKey] = FAKE_BOOL
|
||||||
break
|
break
|
||||||
case "number":
|
case "number":
|
||||||
params[schemaKey] = FAKE_NUMBER
|
record[schemaKey] = FAKE_NUMBER
|
||||||
break
|
break
|
||||||
case "datetime":
|
case "datetime":
|
||||||
params[schemaKey] = FAKE_DATETIME
|
record[schemaKey] = FAKE_DATETIME
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
params.record = record
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw "Failed to find model for trigger"
|
throw "Failed to find model for trigger"
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,8 @@ module.exports = {
|
||||||
COUCH_DB_URL: process.env.COUCH_DB_URL,
|
COUCH_DB_URL: process.env.COUCH_DB_URL,
|
||||||
SALT_ROUNDS: process.env.SALT_ROUNDS,
|
SALT_ROUNDS: process.env.SALT_ROUNDS,
|
||||||
LOGGER: process.env.LOGGER,
|
LOGGER: process.env.LOGGER,
|
||||||
|
AUTOMATION_DIRECTORY: process.env.AUTOMATION_DIRECTORY,
|
||||||
|
AUTOMATION_BUCKET: process.env.AUTOMATION_BUCKET,
|
||||||
BUDIBASE_ENVIRONMENT: process.env.BUDIBASE_ENVIRONMENT,
|
BUDIBASE_ENVIRONMENT: process.env.BUDIBASE_ENVIRONMENT,
|
||||||
SENDGRID_API_KEY: process.env.SENDGRID_API_KEY,
|
SENDGRID_API_KEY: process.env.SENDGRID_API_KEY,
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue