@ -0,0 +1,15 @@ |
|||
# Security Policy |
|||
|
|||
## Versions |
|||
|
|||
As an open source product, we will only patch the latest major version for security vulnerabilities. Previous versions of budibase will not be retroactively patched. |
|||
|
|||
## Disclosing |
|||
|
|||
You can get in touch with us regarding a vulnerability via email at community@budibase.com. |
|||
|
|||
You can also disclose via huntr.dev. If you believe you have found a vulnerability, please disclose it on huntr and let us know. |
|||
|
|||
https://huntr.dev/bounties/disclose |
|||
|
|||
This will enable us to review the vulnerability and potentially reward you for your work. |
|||
@ -1,5 +1,6 @@ |
|||
const env = require("../src/environment") |
|||
|
|||
env._set("SELF_HOSTED", "1") |
|||
env._set("NODE_ENV", "jest") |
|||
env._set("JWT_SECRET", "test-jwtsecret") |
|||
env._set("LOG_LEVEL", "silent") |
|||
|
|||
@ -0,0 +1,22 @@ |
|||
const API = require("./api") |
|||
const env = require("../environment") |
|||
|
|||
const api = new API(env.ACCOUNT_PORTAL_URL) |
|||
|
|||
// TODO: Authorization
|
|||
|
|||
exports.getAccount = async email => { |
|||
const payload = { |
|||
email, |
|||
} |
|||
const response = await api.post(`/api/accounts/search`, { |
|||
body: payload, |
|||
}) |
|||
const json = await response.json() |
|||
|
|||
if (response.status !== 200) { |
|||
throw Error(`Error getting account by email ${email}`, json) |
|||
} |
|||
|
|||
return json[0] |
|||
} |
|||
@ -0,0 +1,44 @@ |
|||
const fetch = require("node-fetch") |
|||
class API { |
|||
constructor(host) { |
|||
this.host = host |
|||
} |
|||
|
|||
apiCall = |
|||
method => |
|||
async (url = "", options = {}) => { |
|||
if (!options.headers) { |
|||
options.headers = {} |
|||
} |
|||
|
|||
if (!options.headers["Content-Type"]) { |
|||
options.headers = { |
|||
"Content-Type": "application/json", |
|||
Accept: "application/json", |
|||
...options.headers, |
|||
} |
|||
} |
|||
|
|||
let json = options.headers["Content-Type"] === "application/json" |
|||
|
|||
const requestOptions = { |
|||
method: method, |
|||
body: json ? JSON.stringify(options.body) : options.body, |
|||
headers: options.headers, |
|||
// TODO: See if this is necessary
|
|||
credentials: "include", |
|||
} |
|||
|
|||
const resp = await fetch(`${this.host}${url}`, requestOptions) |
|||
|
|||
return resp |
|||
} |
|||
|
|||
post = this.apiCall("POST") |
|||
get = this.apiCall("GET") |
|||
patch = this.apiCall("PATCH") |
|||
del = this.apiCall("DELETE") |
|||
put = this.apiCall("PUT") |
|||
} |
|||
|
|||
module.exports = API |
|||
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 12 KiB |
@ -1,108 +0,0 @@ |
|||
<script> |
|||
import { sortBy } from "lodash/fp" |
|||
import { automationStore } from "builderStore" |
|||
import { ActionButton, Popover, Modal } from "@budibase/bbui" |
|||
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns" |
|||
import CreateWebhookModal from "../Shared/CreateWebhookModal.svelte" |
|||
|
|||
$: hasTrigger = $automationStore.selectedAutomation.hasTrigger() |
|||
$: tabs = [ |
|||
{ |
|||
label: "Trigger", |
|||
value: "TRIGGER", |
|||
icon: "Algorithm", |
|||
disabled: hasTrigger, |
|||
}, |
|||
{ |
|||
label: "Action", |
|||
value: "ACTION", |
|||
icon: "Actions", |
|||
disabled: !hasTrigger, |
|||
}, |
|||
{ |
|||
label: "Logic", |
|||
value: "LOGIC", |
|||
icon: "Filter", |
|||
disabled: !hasTrigger, |
|||
}, |
|||
] |
|||
|
|||
let selectedIndex |
|||
let anchors = [] |
|||
let popover |
|||
let webhookModal |
|||
$: selectedTab = selectedIndex == null ? null : tabs[selectedIndex].value |
|||
$: anchor = selectedIndex === -1 ? null : anchors[selectedIndex] |
|||
$: blocks = sortBy(entry => entry[1].name)( |
|||
Object.entries($automationStore.blockDefinitions[selectedTab] ?? {}) |
|||
) |
|||
|
|||
function onChangeTab(idx) { |
|||
selectedIndex = idx |
|||
popover.show() |
|||
} |
|||
|
|||
function closePopover() { |
|||
selectedIndex = null |
|||
popover.hide() |
|||
} |
|||
|
|||
function addBlockToAutomation(stepId, blockDefinition) { |
|||
const newBlock = $automationStore.selectedAutomation.constructBlock( |
|||
selectedTab, |
|||
stepId, |
|||
blockDefinition |
|||
) |
|||
automationStore.actions.addBlockToAutomation(newBlock) |
|||
closePopover() |
|||
if (stepId === "WEBHOOK") { |
|||
webhookModal.show() |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<div class="tab-container"> |
|||
{#each tabs as tab, idx} |
|||
<div bind:this={anchors[idx]}> |
|||
<ActionButton |
|||
quiet |
|||
size="S" |
|||
icon={tab.icon} |
|||
disabled={tab.disabled} |
|||
on:click={tab.disabled ? null : () => onChangeTab(idx)} |
|||
> |
|||
{tab.label} |
|||
</ActionButton> |
|||
</div> |
|||
{/each} |
|||
</div> |
|||
<Popover |
|||
on:close={() => (selectedIndex = null)} |
|||
bind:this={popover} |
|||
{anchor} |
|||
align="left" |
|||
> |
|||
<DropdownContainer> |
|||
{#each blocks as [stepId, blockDefinition]} |
|||
<DropdownItem |
|||
icon={blockDefinition.icon} |
|||
title={blockDefinition.name} |
|||
subtitle={blockDefinition.description} |
|||
on:click={() => addBlockToAutomation(stepId, blockDefinition)} |
|||
/> |
|||
{/each} |
|||
</DropdownContainer> |
|||
</Popover> |
|||
<Modal bind:this={webhookModal} width="30%"> |
|||
<CreateWebhookModal /> |
|||
</Modal> |
|||
|
|||
<style> |
|||
.tab-container { |
|||
display: flex; |
|||
flex-direction: row; |
|||
justify-content: flex-start; |
|||
align-items: center; |
|||
min-height: 24px; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,135 @@ |
|||
<script> |
|||
import { ModalContent, Layout, Detail, Body, Icon } from "@budibase/bbui" |
|||
import { automationStore } from "builderStore" |
|||
import { database } from "stores/backend" |
|||
import { externalActions } from "./ExternalActions" |
|||
$: instanceId = $database._id |
|||
|
|||
let selectedAction |
|||
let actionVal |
|||
let actions = Object.entries($automationStore.blockDefinitions.ACTION) |
|||
export let blockComplete |
|||
|
|||
const external = actions.reduce((acc, elm) => { |
|||
const [k, v] = elm |
|||
if (!v.internal) { |
|||
acc[k] = v |
|||
} |
|||
return acc |
|||
}, {}) |
|||
|
|||
const internal = actions.reduce((acc, elm) => { |
|||
const [k, v] = elm |
|||
if (v.internal) { |
|||
acc[k] = v |
|||
} |
|||
return acc |
|||
}, {}) |
|||
|
|||
const selectAction = action => { |
|||
actionVal = action |
|||
selectedAction = action.name |
|||
} |
|||
|
|||
async function addBlockToAutomation() { |
|||
const newBlock = $automationStore.selectedAutomation.constructBlock( |
|||
"ACTION", |
|||
actionVal.stepId, |
|||
actionVal |
|||
) |
|||
automationStore.actions.addBlockToAutomation(newBlock) |
|||
await automationStore.actions.save( |
|||
$automationStore.selectedAutomation?.automation |
|||
) |
|||
} |
|||
</script> |
|||
|
|||
<ModalContent |
|||
title="Create Automation" |
|||
confirmText="Save" |
|||
size="M" |
|||
disabled={!selectedAction} |
|||
onConfirm={() => { |
|||
blockComplete = true |
|||
addBlockToAutomation() |
|||
}} |
|||
> |
|||
<Body size="XS">Select an app or event.</Body> |
|||
<Layout noPadding> |
|||
<Body size="S">Apps</Body> |
|||
|
|||
<div class="item-list"> |
|||
{#each Object.entries(external) as [idx, action]} |
|||
<div |
|||
class="item" |
|||
class:selected={selectedAction === action.name} |
|||
on:click={() => selectAction(action)} |
|||
> |
|||
<div class="item-body"> |
|||
<img |
|||
width="20" |
|||
height="20" |
|||
src={externalActions[action.stepId].icon} |
|||
alt="zapier" |
|||
/> |
|||
<span class="icon-spacing"> |
|||
<Body size="XS">{idx.charAt(0).toUpperCase() + idx.slice(1)}</Body |
|||
></span |
|||
> |
|||
</div> |
|||
</div> |
|||
{/each} |
|||
</div> |
|||
|
|||
<Detail size="S">Actions</Detail> |
|||
|
|||
<div class="item-list"> |
|||
{#each Object.entries(internal) as [idx, action]} |
|||
<div |
|||
class="item" |
|||
class:selected={selectedAction === action.name} |
|||
on:click={() => selectAction(action)} |
|||
> |
|||
<div class="item-body"> |
|||
<Icon name={action.icon} /> |
|||
<span class="icon-spacing"> |
|||
<Body size="XS">{action.name}</Body></span |
|||
> |
|||
</div> |
|||
</div> |
|||
{/each} |
|||
</div> |
|||
</Layout> |
|||
</ModalContent> |
|||
|
|||
<style> |
|||
.icon-spacing { |
|||
margin-left: var(--spacing-m); |
|||
} |
|||
.item-body { |
|||
display: flex; |
|||
margin-left: var(--spacing-m); |
|||
} |
|||
.item-list { |
|||
display: grid; |
|||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); |
|||
grid-gap: var(--spectrum-alias-grid-baseline); |
|||
} |
|||
|
|||
.item { |
|||
cursor: pointer; |
|||
display: grid; |
|||
grid-gap: var(--spectrum-alias-grid-margin-xsmall); |
|||
padding: var(--spectrum-alias-item-padding-s); |
|||
background: var(--spectrum-alias-background-color-secondary); |
|||
transition: 0.3s all; |
|||
border: solid var(--spectrum-alias-border-color); |
|||
border-radius: 5px; |
|||
box-sizing: border-box; |
|||
border-width: 2px; |
|||
} |
|||
.item:hover, |
|||
.selected { |
|||
background: var(--spectrum-alias-background-color-tertiary); |
|||
} |
|||
</style> |
|||
|
Before Width: | Height: | Size: 326 B After Width: | Height: | Size: 353 B |
@ -0,0 +1,11 @@ |
|||
import DiscordLogo from "assets/discord.svg" |
|||
import ZapierLogo from "assets/zapier.png" |
|||
import IntegromatLogo from "assets/integromat.png" |
|||
import SlackLogo from "assets/slack.svg" |
|||
|
|||
export const externalActions = { |
|||
zapier: { name: "zapier", icon: ZapierLogo }, |
|||
discord: { name: "discord", icon: DiscordLogo }, |
|||
slack: { name: "slack", icon: SlackLogo }, |
|||
integromat: { name: "integromat", icon: IntegromatLogo }, |
|||
} |
|||
@ -1,86 +1,203 @@ |
|||
<script> |
|||
import { automationStore } from "builderStore" |
|||
import AutomationBlockTagline from "./AutomationBlockTagline.svelte" |
|||
import { Icon } from "@budibase/bbui" |
|||
import { |
|||
Icon, |
|||
Divider, |
|||
Layout, |
|||
Body, |
|||
Detail, |
|||
Modal, |
|||
Button, |
|||
StatusLight, |
|||
} from "@budibase/bbui" |
|||
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte" |
|||
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte" |
|||
import ResultsModal from "./ResultsModal.svelte" |
|||
import ActionModal from "./ActionModal.svelte" |
|||
import { database } from "stores/backend" |
|||
import { externalActions } from "./ExternalActions" |
|||
|
|||
export let onSelect |
|||
export let block |
|||
export let testDataModal |
|||
let selected |
|||
let webhookModal |
|||
let actionModal |
|||
let resultsModal |
|||
let setupToggled |
|||
let blockComplete |
|||
$: testResult = $automationStore.selectedAutomation.testResults?.steps.filter( |
|||
step => step.stepId === block.stepId |
|||
) |
|||
$: instanceId = $database._id |
|||
|
|||
$: isTrigger = block.type === "TRIGGER" |
|||
|
|||
$: selected = $automationStore.selectedBlock?.id === block.id |
|||
$: steps = |
|||
$automationStore.selectedAutomation?.automation?.definition?.steps ?? [] |
|||
|
|||
$: blockIdx = steps.findIndex(step => step.id === block.id) |
|||
$: allowDeleteTrigger = !steps.length |
|||
$: lastStep = !isTrigger && blockIdx + 1 === steps.length |
|||
|
|||
// 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]) |
|||
|
|||
function deleteStep() { |
|||
async function deleteStep() { |
|||
automationStore.actions.deleteAutomationBlock(block) |
|||
await automationStore.actions.save( |
|||
$automationStore.selectedAutomation?.automation |
|||
) |
|||
} |
|||
</script> |
|||
|
|||
<div |
|||
class={`block ${block.type} hoverable`} |
|||
class:selected |
|||
on:click={() => onSelect(block)} |
|||
on:click={() => { |
|||
onSelect(block) |
|||
}} |
|||
> |
|||
<header> |
|||
{#if block.type === "TRIGGER"} |
|||
<Icon name="Light" /> |
|||
<span>When this happens...</span> |
|||
{:else if block.type === "ACTION"} |
|||
<Icon name="FlashOn" /> |
|||
<span>Do this...</span> |
|||
{:else if block.type === "LOGIC"} |
|||
<Icon name="Branch2" /> |
|||
<span>Only continue if...</span> |
|||
{/if} |
|||
<div class="label"> |
|||
{#if block.type === "TRIGGER"}Trigger{:else}Step {blockIdx + 1}{/if} |
|||
<div class="blockSection"> |
|||
<div |
|||
on:click={() => { |
|||
blockComplete = !blockComplete |
|||
}} |
|||
class="splitHeader" |
|||
> |
|||
<div class="center-items"> |
|||
{#if externalActions[block.stepId]} |
|||
<img |
|||
alt={externalActions[block.stepId].name} |
|||
width="28px" |
|||
height="28px" |
|||
src={externalActions[block.stepId].icon} |
|||
/> |
|||
{:else} |
|||
<svg |
|||
width="28px" |
|||
height="28px" |
|||
class="spectrum-Icon" |
|||
style="color:grey;" |
|||
focusable="false" |
|||
> |
|||
<use xlink:href="#spectrum-icon-18-{block.icon}" /> |
|||
</svg> |
|||
{/if} |
|||
<div class="iconAlign"> |
|||
{#if isTrigger} |
|||
<Body size="XS">When this happens:</Body> |
|||
{:else} |
|||
<Body size="XS">Do this:</Body> |
|||
{/if} |
|||
|
|||
<Detail size="S">{block?.name?.toUpperCase() || ""}</Detail> |
|||
</div> |
|||
</div> |
|||
{#if testResult} |
|||
<span on:click={() => resultsModal.show()}> |
|||
<StatusLight |
|||
positive={isTrigger || testResult[0].outputs?.success} |
|||
negative={!testResult[0].outputs?.success} |
|||
><Body size="XS">View response</Body></StatusLight |
|||
> |
|||
</span> |
|||
{/if} |
|||
</div> |
|||
</div> |
|||
{#if !blockComplete} |
|||
<Divider noMargin /> |
|||
<div class="blockSection"> |
|||
<Layout noPadding gap="S"> |
|||
<div class="splitHeader"> |
|||
<div |
|||
on:click|stopPropagation={() => { |
|||
setupToggled = !setupToggled |
|||
}} |
|||
class="center-items" |
|||
> |
|||
{#if setupToggled} |
|||
<Icon size="M" name="ChevronDown" /> |
|||
{:else} |
|||
<Icon size="M" name="ChevronRight" /> |
|||
{/if} |
|||
<Detail size="S">Setup</Detail> |
|||
</div> |
|||
{#if !isTrigger} |
|||
<div on:click={() => deleteStep()}> |
|||
<Icon name="DeleteOutline" /> |
|||
</div> |
|||
{/if} |
|||
</div> |
|||
|
|||
{#if setupToggled} |
|||
<AutomationBlockSetup |
|||
schemaProperties={Object.entries(block.schema.inputs.properties)} |
|||
{block} |
|||
{webhookModal} |
|||
/> |
|||
{#if lastStep} |
|||
<Button on:click={() => testDataModal.show()} cta |
|||
>Finish and test automation</Button |
|||
> |
|||
{/if} |
|||
<Button |
|||
disabled={disableAddButton ? true : !hasCompletedInputs} |
|||
on:click={() => { |
|||
setupToggled = false |
|||
actionModal.show() |
|||
}} |
|||
primary={!isTrigger} |
|||
cta={isTrigger}>Add Action</Button |
|||
> |
|||
{/if} |
|||
</Layout> |
|||
</div> |
|||
{#if block.type !== "TRIGGER" || allowDeleteTrigger} |
|||
<div on:click|stopPropagation={deleteStep}><Icon name="Close" /></div> |
|||
{/if} |
|||
</header> |
|||
<hr /> |
|||
<p> |
|||
<AutomationBlockTagline {block} /> |
|||
</p> |
|||
{/if} |
|||
|
|||
<Modal bind:this={resultsModal} width="30%"> |
|||
<ResultsModal {isTrigger} {testResult} /> |
|||
</Modal> |
|||
|
|||
<Modal bind:this={actionModal} width="30%"> |
|||
<ActionModal bind:blockComplete /> |
|||
</Modal> |
|||
|
|||
<Modal bind:this={webhookModal} width="30%"> |
|||
<CreateWebhookModal /> |
|||
</Modal> |
|||
</div> |
|||
|
|||
<style> |
|||
.center-items { |
|||
display: flex; |
|||
align-items: center; |
|||
} |
|||
.splitHeader { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
} |
|||
.iconAlign { |
|||
padding: 0 0 0 var(--spacing-m); |
|||
display: inline-block; |
|||
} |
|||
.block { |
|||
width: 360px; |
|||
padding: 20px; |
|||
border-radius: var(--border-radius-m); |
|||
transition: 0.3s all ease; |
|||
box-shadow: 0 4px 30px 0 rgba(57, 60, 68, 0.08); |
|||
font-size: 16px; |
|||
background-color: var(--spectrum-global-color-gray-50); |
|||
background-color: var(--spectrum-alias-background-color-secondary); |
|||
color: var(--grey-9); |
|||
} |
|||
.block.selected, |
|||
.block:hover { |
|||
transform: scale(1.1); |
|||
box-shadow: 0 4px 30px 0 rgba(57, 60, 68, 0.15); |
|||
border: 1px solid var(--spectrum-global-color-gray-300); |
|||
border-radius: 4px 4px 4px 4px; |
|||
} |
|||
|
|||
header { |
|||
font-size: 16px; |
|||
font-weight: 600; |
|||
display: flex; |
|||
flex-direction: row; |
|||
justify-content: flex-start; |
|||
align-items: center; |
|||
gap: var(--spacing-xs); |
|||
} |
|||
header span { |
|||
flex: 1 1 auto; |
|||
} |
|||
header .label { |
|||
font-size: 14px; |
|||
padding: var(--spacing-s); |
|||
border-radius: var(--border-radius-m); |
|||
background-color: var(--grey-2); |
|||
color: var(--grey-8); |
|||
.blockSection { |
|||
padding: var(--spacing-xl); |
|||
} |
|||
</style> |
|||
|
|||
@ -0,0 +1,114 @@ |
|||
<script> |
|||
import { ModalContent, Icon, Detail, TextArea } from "@budibase/bbui" |
|||
|
|||
export let testResult |
|||
export let isTrigger |
|||
let inputToggled |
|||
let outputToggled |
|||
</script> |
|||
|
|||
<ModalContent |
|||
showCloseIcon={false} |
|||
showConfirmButton={false} |
|||
title="Test Automation" |
|||
cancelText="Close" |
|||
> |
|||
<div slot="header"> |
|||
<div style="float: right;"> |
|||
{#if isTrigger || testResult[0].outputs.success} |
|||
<div class="iconSuccess"> |
|||
<Icon size="S" name="CheckmarkCircle" /> |
|||
</div> |
|||
{:else} |
|||
<div class="iconFailure"> |
|||
<Icon size="S" name="CloseCircle" /> |
|||
</div> |
|||
{/if} |
|||
</div> |
|||
</div> |
|||
|
|||
<div |
|||
on:click={() => { |
|||
inputToggled = !inputToggled |
|||
}} |
|||
class="toggle splitHeader" |
|||
> |
|||
<div> |
|||
<div style="display: flex; align-items: center;"> |
|||
<span style="padding-left: var(--spacing-s);"> |
|||
<Detail size="S">Input</Detail> |
|||
</span> |
|||
</div> |
|||
</div> |
|||
<div> |
|||
{#if inputToggled} |
|||
<Icon size="M" name="ChevronDown" /> |
|||
{:else} |
|||
<Icon size="M" name="ChevronRight" /> |
|||
{/if} |
|||
</div> |
|||
</div> |
|||
{#if inputToggled} |
|||
<div class="text-area-container"> |
|||
<TextArea |
|||
disabled |
|||
value={JSON.stringify(testResult[0].inputs, null, 2)} |
|||
/> |
|||
</div> |
|||
{/if} |
|||
|
|||
<div |
|||
on:click={() => { |
|||
outputToggled = !outputToggled |
|||
}} |
|||
class="toggle splitHeader" |
|||
> |
|||
<div> |
|||
<div style="display: flex; align-items: center;"> |
|||
<span style="padding-left: var(--spacing-s);"> |
|||
<Detail size="S">Output</Detail> |
|||
</span> |
|||
</div> |
|||
</div> |
|||
<div> |
|||
{#if outputToggled} |
|||
<Icon size="M" name="ChevronDown" /> |
|||
{:else} |
|||
<Icon size="M" name="ChevronRight" /> |
|||
{/if} |
|||
</div> |
|||
</div> |
|||
{#if outputToggled} |
|||
<div class="text-area-container"> |
|||
<TextArea |
|||
disabled |
|||
value={JSON.stringify(testResult[0].outputs, null, 2)} |
|||
/> |
|||
</div> |
|||
{/if} |
|||
</ModalContent> |
|||
|
|||
<style> |
|||
.iconSuccess { |
|||
color: var(--spectrum-global-color-green-600); |
|||
} |
|||
|
|||
.iconFailure { |
|||
color: var(--spectrum-global-color-red-600); |
|||
} |
|||
|
|||
.splitHeader { |
|||
cursor: pointer; |
|||
display: flex; |
|||
justify-content: space-between; |
|||
} |
|||
|
|||
.toggle { |
|||
display: flex; |
|||
align-items: center; |
|||
} |
|||
|
|||
.text-area-container :global(textarea) { |
|||
height: 150px; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,91 @@ |
|||
<script> |
|||
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 failedParse = null |
|||
// clone the trigger so we're not mutating the reference |
|||
let trigger = cloneDeep( |
|||
$automationStore.selectedAutomation.automation.definition.trigger |
|||
) |
|||
let schemaProperties = Object.entries(trigger.schema.outputs.properties || {}) |
|||
|
|||
if (!$automationStore.selectedAutomation.automation.testData) { |
|||
$automationStore.selectedAutomation.automation.testData = {} |
|||
} |
|||
|
|||
// get the outputs so we can define the fields |
|||
|
|||
// check to see if there is existing test data in the store |
|||
$: testData = $automationStore.selectedAutomation.automation.testData |
|||
// Check the schema to see if required fields have been entered |
|||
$: isError = !trigger.schema.outputs.required.every( |
|||
required => testData[required] |
|||
) |
|||
|
|||
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="Test" |
|||
showConfirmButton={true} |
|||
disabled={isError} |
|||
onConfirm={() => { |
|||
automationStore.actions.addTestDataToAutomation(testData) |
|||
automationStore.actions.test( |
|||
$automationStore.selectedAutomation?.automation, |
|||
testData |
|||
) |
|||
}} |
|||
cancelText="Cancel" |
|||
> |
|||
<Tabs selected="Form" quiet |
|||
><Tab icon="Form" title="Form"> |
|||
<div class="tab-content-padding"> |
|||
<AutomationBlockSetup |
|||
bind:testData |
|||
{schemaProperties} |
|||
isTestModal |
|||
block={trigger} |
|||
/> |
|||
</div></Tab |
|||
> |
|||
<Tab icon="FileJson" title="JSON"> |
|||
<div class="tab-content-padding"> |
|||
<Label>JSON</Label> |
|||
<div class="text-area-container"> |
|||
<TextArea |
|||
value={JSON.stringify( |
|||
$automationStore.selectedAutomation.automation.testData, |
|||
null, |
|||
2 |
|||
)} |
|||
error={failedParse} |
|||
on:change={e => parseTestJSON(e)} |
|||
/> |
|||
</div> |
|||
</div> |
|||
</Tab> |
|||
</Tabs> |
|||
</ModalContent> |
|||
|
|||
<style> |
|||
.text-area-container :global(textarea) { |
|||
min-height: 200px; |
|||
height: 200px; |
|||
} |
|||
|
|||
.tab-content-padding { |
|||
padding: 0 var(--spacing-xl); |
|||
} |
|||
</style> |
|||
@ -0,0 +1,76 @@ |
|||
<script> |
|||
import { automationStore } from "builderStore" |
|||
import { notifications } from "@budibase/bbui" |
|||
import { Icon, Input, ModalContent, Modal } from "@budibase/bbui" |
|||
import analytics from "analytics" |
|||
|
|||
let name |
|||
let error = "" |
|||
let modal |
|||
|
|||
export let automation |
|||
export let onCancel = undefined |
|||
|
|||
export const show = () => { |
|||
name = automation?.name |
|||
modal.show() |
|||
} |
|||
export const hide = () => { |
|||
modal.hide() |
|||
} |
|||
|
|||
async function saveAutomation() { |
|||
const updatedAutomation = { |
|||
...automation, |
|||
name, |
|||
} |
|||
await automationStore.actions.save(updatedAutomation) |
|||
notifications.success(`Automation ${name} updated successfully.`) |
|||
analytics.captureEvent("Automation Saved", { name }) |
|||
hide() |
|||
} |
|||
|
|||
function checkValid(evt) { |
|||
name = evt.target.value |
|||
if (!name) { |
|||
error = "Name is required" |
|||
return |
|||
} |
|||
error = "" |
|||
} |
|||
</script> |
|||
|
|||
<Modal bind:this={modal} on:hide={onCancel}> |
|||
<ModalContent |
|||
title="Edit Automation" |
|||
confirmText="Save" |
|||
size="L" |
|||
onConfirm={saveAutomation} |
|||
disabled={error} |
|||
> |
|||
<Input bind:value={name} label="Name" on:input={checkValid} {error} /> |
|||
<a |
|||
slot="footer" |
|||
target="_blank" |
|||
href="https://docs.budibase.com/automate/introduction-to-automate" |
|||
> |
|||
<Icon name="InfoOutline" /> |
|||
<span>Learn about automations</span> |
|||
</a> |
|||
</ModalContent> |
|||
</Modal> |
|||
|
|||
<style> |
|||
a { |
|||
color: var(--ink); |
|||
font-size: 14px; |
|||
vertical-align: middle; |
|||
display: flex; |
|||
align-items: center; |
|||
text-decoration: none; |
|||
} |
|||
a span { |
|||
text-decoration: underline; |
|||
margin-left: var(--spectrum-alias-item-padding-s); |
|||
} |
|||
</style> |
|||
@ -1,96 +0,0 @@ |
|||
<script> |
|||
import { automationStore } from "builderStore" |
|||
import { database } from "stores/backend" |
|||
import { notifications, Button, Modal, Heading, Toggle } from "@budibase/bbui" |
|||
import AutomationBlockSetup from "./AutomationBlockSetup.svelte" |
|||
import CreateWebookModal from "../Shared/CreateWebhookModal.svelte" |
|||
|
|||
let webhookModal |
|||
|
|||
$: instanceId = $database._id |
|||
$: automation = $automationStore.selectedAutomation?.automation |
|||
$: automationLive = automation?.live |
|||
|
|||
function setAutomationLive(live) { |
|||
if (automationLive === live) { |
|||
return |
|||
} |
|||
automation.live = live |
|||
automationStore.actions.save({ instanceId, automation }) |
|||
if (live) { |
|||
notifications.info(`Automation ${automation.name} enabled.`) |
|||
} else { |
|||
notifications.error(`Automation ${automation.name} disabled.`) |
|||
} |
|||
} |
|||
|
|||
async function testAutomation() { |
|||
const result = await automationStore.actions.trigger({ |
|||
automation: $automationStore.selectedAutomation.automation, |
|||
}) |
|||
if (result.status === 200) { |
|||
notifications.success( |
|||
`Automation ${automation.name} triggered successfully.` |
|||
) |
|||
} else { |
|||
notifications.error(`Failed to trigger automation ${automation.name}.`) |
|||
} |
|||
} |
|||
|
|||
async function saveAutomation() { |
|||
await automationStore.actions.save({ |
|||
instanceId, |
|||
automation, |
|||
}) |
|||
notifications.success(`Automation ${automation.name} saved.`) |
|||
} |
|||
</script> |
|||
|
|||
<div class="title"> |
|||
<Heading size="S">Setup</Heading> |
|||
<Toggle |
|||
value={automationLive} |
|||
on:change={() => setAutomationLive(!automationLive)} |
|||
dataCy="activate-automation" |
|||
text="Live" |
|||
/> |
|||
</div> |
|||
{#if $automationStore.selectedBlock} |
|||
<AutomationBlockSetup |
|||
bind:block={$automationStore.selectedBlock} |
|||
{webhookModal} |
|||
/> |
|||
{:else if automation} |
|||
<div class="block-label">{automation.name}</div> |
|||
<Button secondary on:click={testAutomation}>Test Automation</Button> |
|||
{/if} |
|||
<Button |
|||
secondary |
|||
wide |
|||
data-cy="save-automation-setup" |
|||
on:click={saveAutomation} |
|||
> |
|||
Save Automation |
|||
</Button> |
|||
<Modal bind:this={webhookModal} width="30%"> |
|||
<CreateWebookModal /> |
|||
</Modal> |
|||
|
|||
<style> |
|||
.title { |
|||
display: flex; |
|||
flex-direction: row; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
gap: var(--spacing-xs); |
|||
} |
|||
.title :global(h1) { |
|||
flex: 1 1 auto; |
|||
} |
|||
|
|||
.block-label { |
|||
font-size: var(--spectrum-global-dimension-font-size-75); |
|||
font-weight: 600; |
|||
color: var(--grey-7); |
|||
} |
|||
</style> |
|||
@ -0,0 +1,22 @@ |
|||
<script> |
|||
import { ActionGroup, ActionButton } from "@budibase/bbui" |
|||
import { store } from "builderStore" |
|||
</script> |
|||
|
|||
<ActionGroup compact> |
|||
<ActionButton |
|||
icon="DeviceDesktop" |
|||
selected={$store.previewDevice === "desktop"} |
|||
on:click={() => store.actions.preview.setDevice("desktop")} |
|||
/> |
|||
<ActionButton |
|||
icon="DeviceTablet" |
|||
selected={$store.previewDevice === "tablet"} |
|||
on:click={() => store.actions.preview.setDevice("tablet")} |
|||
/> |
|||
<ActionButton |
|||
icon="DevicePhone" |
|||
selected={$store.previewDevice === "mobile"} |
|||
on:click={() => store.actions.preview.setDevice("mobile")} |
|||
/> |
|||
</ActionGroup> |
|||
@ -0,0 +1,140 @@ |
|||
<script> |
|||
import { get } from "svelte/store" |
|||
import { |
|||
ActionButton, |
|||
Modal, |
|||
ModalContent, |
|||
Layout, |
|||
ColorPicker, |
|||
Label, |
|||
Select, |
|||
Button, |
|||
} from "@budibase/bbui" |
|||
import { store } from "builderStore" |
|||
import AppThemeSelect from "./AppThemeSelect.svelte" |
|||
|
|||
let modal |
|||
|
|||
const defaultTheme = { |
|||
primaryColor: "var(--spectrum-global-color-blue-600)", |
|||
primaryColorHover: "var(--spectrum-global-color-blue-500)", |
|||
buttonBorderRadius: "16px", |
|||
navBackground: "var(--spectrum-global-color-gray-100)", |
|||
navTextColor: "var(--spectrum-global-color-gray-800)", |
|||
} |
|||
|
|||
const buttonBorderRadiusOptions = [ |
|||
{ |
|||
label: "None", |
|||
value: "0", |
|||
}, |
|||
{ |
|||
label: "Small", |
|||
value: "4px", |
|||
}, |
|||
{ |
|||
label: "Medium", |
|||
value: "8px", |
|||
}, |
|||
{ |
|||
label: "Large", |
|||
value: "16px", |
|||
}, |
|||
] |
|||
|
|||
const updateProperty = property => { |
|||
return e => { |
|||
store.actions.customTheme.save({ |
|||
...get(store).customTheme, |
|||
[property]: e.detail, |
|||
}) |
|||
} |
|||
} |
|||
|
|||
const resetTheme = () => { |
|||
store.actions.customTheme.save(null) |
|||
} |
|||
</script> |
|||
|
|||
<div class="container"> |
|||
<ActionButton icon="Brush" on:click={modal.show}>Theme</ActionButton> |
|||
</div> |
|||
<Modal bind:this={modal}> |
|||
<ModalContent |
|||
showConfirmButton={false} |
|||
cancelText="Close" |
|||
showCloseIcon={false} |
|||
title="Theme settings" |
|||
> |
|||
<Layout noPadding gap="S"> |
|||
<div class="setting"> |
|||
<Label size="L">Theme</Label> |
|||
<AppThemeSelect /> |
|||
</div> |
|||
<div class="setting"> |
|||
<Label size="L">Button roundness</Label> |
|||
<div class="select-wrapper"> |
|||
<Select |
|||
placeholder={null} |
|||
value={$store.customTheme?.buttonBorderRadius || |
|||
defaultTheme.buttonBorderRadius} |
|||
on:change={updateProperty("buttonBorderRadius")} |
|||
options={buttonBorderRadiusOptions} |
|||
/> |
|||
</div> |
|||
</div> |
|||
<div class="setting"> |
|||
<Label size="L">Primary color</Label> |
|||
<ColorPicker |
|||
spectrumTheme={$store.theme} |
|||
value={$store.customTheme?.primaryColor || defaultTheme.primaryColor} |
|||
on:change={updateProperty("primaryColor")} |
|||
/> |
|||
</div> |
|||
<div class="setting"> |
|||
<Label size="L">Primary color (hover)</Label> |
|||
<ColorPicker |
|||
spectrumTheme={$store.theme} |
|||
value={$store.customTheme?.primaryColorHover || |
|||
defaultTheme.primaryColorHover} |
|||
on:change={updateProperty("primaryColorHover")} |
|||
/> |
|||
</div> |
|||
<div class="setting"> |
|||
<Label size="L">Navigation bar background color</Label> |
|||
<ColorPicker |
|||
spectrumTheme={$store.theme} |
|||
value={$store.customTheme?.navBackground || |
|||
defaultTheme.navBackground} |
|||
on:change={updateProperty("navBackground")} |
|||
/> |
|||
</div> |
|||
<div class="setting"> |
|||
<Label size="L">Navigation bar text color</Label> |
|||
<ColorPicker |
|||
spectrumTheme={$store.theme} |
|||
value={$store.customTheme?.navTextColor || defaultTheme.navTextColor} |
|||
on:change={updateProperty("navTextColor")} |
|||
/> |
|||
</div> |
|||
</Layout> |
|||
<div slot="footer"> |
|||
<Button secondary quiet on:click={resetTheme}>Reset</Button> |
|||
</div> |
|||
</ModalContent> |
|||
</Modal> |
|||
|
|||
<style> |
|||
.container { |
|||
padding-right: 8px; |
|||
} |
|||
.setting { |
|||
display: flex; |
|||
flex-direction: row; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
} |
|||
.select-wrapper { |
|||
width: 100px; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,65 @@ |
|||
<script> |
|||
import { Select, Label, Combobox, Checkbox, Body } from "@budibase/bbui" |
|||
import { onMount } from "svelte" |
|||
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte" |
|||
import { getAllStateVariables } from "builderStore/dataBinding" |
|||
|
|||
export let parameters |
|||
export let bindings = [] |
|||
|
|||
const keyOptions = getAllStateVariables() |
|||
const typeOptions = [ |
|||
{ |
|||
label: "Set value", |
|||
value: "set", |
|||
}, |
|||
{ |
|||
label: "Delete value", |
|||
value: "delete", |
|||
}, |
|||
] |
|||
|
|||
onMount(() => { |
|||
if (!parameters.type) { |
|||
parameters.type = "set" |
|||
} |
|||
}) |
|||
</script> |
|||
|
|||
<div class="root"> |
|||
<Label small>Type</Label> |
|||
<Select |
|||
placeholder={null} |
|||
bind:value={parameters.type} |
|||
options={typeOptions} |
|||
/> |
|||
<Label small>Key</Label> |
|||
<Combobox bind:value={parameters.key} options={keyOptions} /> |
|||
{#if parameters.type === "set"} |
|||
<Label small>Value</Label> |
|||
<DrawerBindableInput |
|||
{bindings} |
|||
value={parameters.value} |
|||
on:change={e => (parameters.value = e.detail)} |
|||
/> |
|||
<div /> |
|||
<Checkbox bind:value={parameters.persist} text="Persist this value" /> |
|||
<div /> |
|||
<Body size="XS"> |
|||
Persisted values will remain even after reloading the page or closing the |
|||
browser. |
|||
</Body> |
|||
{/if} |
|||
</div> |
|||
|
|||
<style> |
|||
.root { |
|||
display: grid; |
|||
column-gap: var(--spacing-l); |
|||
row-gap: var(--spacing-s); |
|||
grid-template-columns: 60px 1fr; |
|||
align-items: center; |
|||
max-width: 400px; |
|||
margin: 0 auto; |
|||
} |
|||
</style> |
|||