Update styles across entire automations page

This commit is contained in:
Andrew Kingston 2020-10-26 12:18:34 +00:00
parent d356f8d609
commit 0553da5a03
27 changed files with 467 additions and 496 deletions

View File

@ -3,14 +3,14 @@ import api from "../../api"
import Automation from "./Automation" import Automation from "./Automation"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
const automationActions = store => ({ const automationActions = (store) => ({
fetch: async () => { fetch: async () => {
const responses = await Promise.all([ const responses = await Promise.all([
api.get(`/api/automations`), api.get(`/api/automations`),
api.get(`/api/automations/definitions/list`), api.get(`/api/automations/definitions/list`),
]) ])
const jsonResponses = await Promise.all(responses.map(x => x.json())) const jsonResponses = await Promise.all(responses.map((x) => x.json()))
store.update(state => { store.update((state) => {
state.automations = jsonResponses[0] state.automations = jsonResponses[0]
state.blockDefinitions = { state.blockDefinitions = {
TRIGGER: jsonResponses[1].trigger, TRIGGER: jsonResponses[1].trigger,
@ -31,7 +31,7 @@ const automationActions = store => ({
const CREATE_AUTOMATION_URL = `/api/automations` const CREATE_AUTOMATION_URL = `/api/automations`
const response = await api.post(CREATE_AUTOMATION_URL, automation) const response = await api.post(CREATE_AUTOMATION_URL, automation)
const json = await response.json() const json = await response.json()
store.update(state => { store.update((state) => {
state.automations = [...state.automations, json.automation] state.automations = [...state.automations, json.automation]
store.actions.select(json.automation) store.actions.select(json.automation)
return state return state
@ -41,9 +41,9 @@ const automationActions = store => ({
const UPDATE_AUTOMATION_URL = `/api/automations` const UPDATE_AUTOMATION_URL = `/api/automations`
const response = await api.put(UPDATE_AUTOMATION_URL, automation) const response = await api.put(UPDATE_AUTOMATION_URL, automation)
const json = await response.json() const json = await response.json()
store.update(state => { store.update((state) => {
const existingIdx = state.automations.findIndex( const existingIdx = state.automations.findIndex(
existing => existing._id === automation._id (existing) => existing._id === automation._id
) )
state.automations.splice(existingIdx, 1, json.automation) state.automations.splice(existingIdx, 1, json.automation)
state.automations = state.automations state.automations = state.automations
@ -56,9 +56,9 @@ const automationActions = store => ({
const DELETE_AUTOMATION_URL = `/api/automations/${_id}/${_rev}` const DELETE_AUTOMATION_URL = `/api/automations/${_id}/${_rev}`
await api.delete(DELETE_AUTOMATION_URL) await api.delete(DELETE_AUTOMATION_URL)
store.update(state => { store.update((state) => {
const existingIdx = state.automations.findIndex( const existingIdx = state.automations.findIndex(
existing => existing._id === _id (existing) => existing._id === _id
) )
state.automations.splice(existingIdx, 1) state.automations.splice(existingIdx, 1)
state.automations = state.automations state.automations = state.automations
@ -72,24 +72,24 @@ const automationActions = store => ({
const TRIGGER_AUTOMATION_URL = `/api/automations/${_id}/trigger` const TRIGGER_AUTOMATION_URL = `/api/automations/${_id}/trigger`
return await api.post(TRIGGER_AUTOMATION_URL) return await api.post(TRIGGER_AUTOMATION_URL)
}, },
select: automation => { select: (automation) => {
store.update(state => { store.update((state) => {
state.selectedAutomation = new Automation(cloneDeep(automation)) state.selectedAutomation = new Automation(cloneDeep(automation))
state.selectedBlock = null state.selectedBlock = null
return 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))
state.selectedBlock = newBlock state.selectedBlock = newBlock
return state return state
}) })
}, },
deleteAutomationBlock: block => { deleteAutomationBlock: (block) => {
store.update(state => { store.update((state) => {
const idx = state.selectedAutomation.automation.definition.steps.findIndex( const idx = state.selectedAutomation.automation.definition.steps.findIndex(
x => x.id === block.id (x) => x.id === block.id
) )
state.selectedAutomation.deleteBlock(block.id) state.selectedAutomation.deleteBlock(block.id)

View File

@ -1,93 +1,31 @@
<script> <script>
import { afterUpdate } from "svelte"
import { automationStore, backendUiStore } from "builderStore" import { automationStore, backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
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
$: automationLive = automation?.live $: automationLive = automation?.live
$: instanceId = $backendUiStore.selectedDatabase._id $: instanceId = $backendUiStore.selectedDatabase._id
$: automationCount = $automationStore.automations?.length ?? 0
function onSelect(block) { function onSelect(block) {
automationStore.update(state => { automationStore.update((state) => {
state.selectedBlock = block state.selectedBlock = block
return state return state
}) })
} }
function setAutomationLive(live) {
automation.live = live
automationStore.actions.save({ instanceId, automation })
if (live) {
notifier.info(`Automation ${automation.name} enabled.`)
} else {
notifier.danger(`Automation ${automation.name} disabled.`)
}
}
</script> </script>
<section>
<Flowchart {automation} {onSelect} />
</section>
<footer>
{#if automation} {#if automation}
<button <BlockList />
class:highlighted={automationLive} <Flowchart {automation} {onSelect} />
class:hoverable={automationLive} {:else if automationCount === 0}
class="stop-button hoverable"> <i>Create your first automation to get started</i>
<i class="ri-stop-fill" on:click={() => setAutomationLive(false)} /> {:else}<i>Select an automation to edit</i>{/if}
</button>
<button
class:highlighted={!automationLive}
class:hoverable={!automationLive}
class="play-button hoverable"
data-cy="activate-automation"
on:click={() => setAutomationLive(true)}>
<i class="ri-play-fill" />
</button>
{/if}
</footer>
<style> <style>
section { i {
display: flex; font-size: var(--font-size-m);
flex-direction: column; color: var(--grey-5);
justify-content: flex-start;
align-items: center;
overflow: auto;
height: 100%;
position: relative;
}
footer {
position: absolute;
bottom: var(--spacing-xl);
right: 30px;
display: flex;
align-items: flex-end;
}
footer > button {
border-radius: 100%;
color: var(--white);
width: 76px;
height: 76px;
border: none;
background: #adaec4;
font-size: 45px;
display: flex;
align-items: center;
justify-content: center;
}
footer > button:first-child {
margin-right: var(--spacing-m);
}
.play-button.highlighted {
background: var(--purple);
}
.stop-button.highlighted {
background: var(--red);
} }
</style> </style>

View File

@ -0,0 +1,129 @@
<script>
import { sortBy } from "lodash/fp"
import { automationStore } from "builderStore"
import { DropdownMenu } from "@budibase/bbui"
import { DropdownContainer, DropdownItem } from "../../common/Dropdowns"
import analytics from "analytics"
$: hasTrigger = $automationStore.selectedAutomation.hasTrigger()
$: tabs = [
{
label: "Trigger",
value: "TRIGGER",
icon: "ri-organization-chart",
disabled: hasTrigger,
},
{
label: "Action",
value: "ACTION",
icon: "ri-flow-chart",
disabled: !hasTrigger,
},
{
label: "Logic",
value: "LOGIC",
icon: "ri-filter-line",
disabled: !hasTrigger,
},
]
let buttonProps = []
let selectedIndex
let anchors = []
let popover
$: 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 = {
...blockDefinition,
inputs: blockDefinition.inputs || {},
stepId,
type: selectedTab,
}
automationStore.actions.addBlockToAutomation(newBlock)
analytics.captureEvent("Added Automation Block", {
name: blockDefinition.name,
})
closePopover()
}
</script>
<div class="tab-container">
{#each tabs as tab, idx}
<div
bind:this={anchors[idx]}
class="tab"
class:disabled={tab.disabled}
on:click={tab.disabled ? null : () => onChangeTab(idx)}
class:active={idx === selectedIndex}>
{#if tab.icon}<i class={tab.icon} />{/if}
<span>{tab.label}</span>
<i class="ri-arrow-down-s-line arrow" />
</div>
{/each}
</div>
<DropdownMenu
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>
</DropdownMenu>
<style>
.tab-container {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: var(--spacing-l);
min-height: 24px;
}
.tab {
color: var(--grey-7);
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: var(--spacing-xs);
font-size: var(--font-size-xs);
}
.tab span {
font-weight: 500;
user-select: none;
}
.tab.active,
.tab:not(.disabled):hover {
color: var(--ink);
cursor: pointer;
}
.tab.disabled {
color: var(--grey-5);
}
.tab i:not(:last-child) {
font-size: 16px;
}
</style>

View File

@ -18,16 +18,9 @@
blocks = blocks.concat(automation.definition.steps || []) blocks = blocks.concat(automation.definition.steps || [])
} }
} }
$: automationCount = $automationStore.automations?.length ?? 0
</script> </script>
{#if automationCount === 0} {#if !blocks.length}<i>Add a trigger to your automation to get started</i>{/if}
<i>Create your first automation to get started</i>
{:else if automation == null}
<i>Select an automation to edit</i>
{:else if !blocks.length}
<i>Add some steps to your automation to get started</i>
{/if}
<section class="canvas"> <section class="canvas">
{#each blocks as block, idx (block.id)} {#each blocks as block, idx (block.id)}
<div <div
@ -44,19 +37,18 @@
</section> </section>
<style> <style>
i {
font-size: var(--font-size-xl);
color: var(--grey-4);
padding: var(--spacing-xl) 40px;
align-self: flex-start;
}
section { section {
position: absolute; margin: 0 -40px calc(-1 * var(--spacing-l)) -40px;
padding: 40px; padding: var(--spacing-l) 40px 0 40px;
display: flex; display: flex;
align-items: center; align-items: center;
flex-direction: column; flex-direction: column;
overflow-y: auto;
flex: 1 1 auto;
}
/* Fix for firefox not respecting bottom padding in scrolling containers */
section > *:last-child {
padding-bottom: 40px;
} }
.block { .block {
@ -65,4 +57,9 @@
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
} }
i {
font-size: var(--font-size-m);
color: var(--grey-5);
}
</style> </style>

View File

@ -9,7 +9,12 @@
$: selected = $automationStore.selectedBlock?.id === block.id $: selected = $automationStore.selectedBlock?.id === block.id
$: steps = $: steps =
$automationStore.selectedAutomation?.automation?.definition?.steps ?? [] $automationStore.selectedAutomation?.automation?.definition?.steps ?? []
$: blockIdx = steps.findIndex(step => step.id === block.id) $: blockIdx = steps.findIndex((step) => step.id === block.id)
$: allowDeleteTrigger = !steps.length
function deleteStep() {
automationStore.actions.deleteAutomationBlock(block)
}
</script> </script>
<div <div
@ -30,6 +35,9 @@
<div class="label"> <div class="label">
{#if block.type === 'TRIGGER'}Trigger{:else}Step {blockIdx + 1}{/if} {#if block.type === 'TRIGGER'}Trigger{:else}Step {blockIdx + 1}{/if}
</div> </div>
{#if block.type !== 'TRIGGER' || allowDeleteTrigger}
<i on:click|stopPropagation={deleteStep} class="delete ri-close-line" />
{/if}
</header> </header>
<hr /> <hr />
<p> <p>
@ -61,6 +69,7 @@
flex-direction: row; flex-direction: row;
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
gap: var(--spacing-xs);
} }
header span { header span {
flex: 1 1 auto; flex: 1 1 auto;
@ -74,7 +83,13 @@
} }
header i { header i {
font-size: 20px; font-size: 20px;
margin-right: 5px; }
header i.delete {
opacity: 0.5;
}
header i.delete:hover {
cursor: pointer;
opacity: 1;
} }
.ACTION { .ACTION {

View File

@ -0,0 +1,34 @@
<script>
import { onMount } from "svelte"
import { automationStore } from "builderStore"
import NavItem from "components/common/NavItem.svelte"
import EditAutomationPopover from "./EditAutomationPopover.svelte"
$: selectedAutomationId = $automationStore.selectedAutomation?.automation?._id
onMount(() => {
automationStore.actions.fetch()
})
</script>
<div class="automations-list">
{#each $automationStore.automations as automation, idx}
<NavItem
border={idx > 0}
icon="ri-stackshare-line"
text={automation.name}
selected={automation._id === selectedAutomationId}
on:click={() => automationStore.actions.select(automation)}>
<EditAutomationPopover {automation} />
</NavItem>
{/each}
</div>
<style>
.automations-list {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
}
</style>

View File

@ -1,79 +0,0 @@
<script>
import { onMount } from "svelte"
import { automationStore } from "builderStore"
import CreateAutomationModal from "./CreateAutomationModal.svelte"
import { Button, Modal } from "@budibase/bbui"
let modal
$: selectedAutomationId = $automationStore.selectedAutomation?.automation?._id
onMount(() => {
automationStore.actions.fetch()
})
</script>
<section>
<Button primary wide on:click={modal.show}>Create New Automation</Button>
<ul>
{#each $automationStore.automations as automation}
<li
class="automation-item"
class:selected={automation._id === selectedAutomationId}
on:click={() => automationStore.actions.select(automation)}>
<i class="ri-stackshare-line" class:live={automation.live} />
{automation.name}
</li>
{/each}
</ul>
</section>
<Modal bind:this={modal}>
<CreateAutomationModal />
</Modal>
<style>
section {
display: flex;
flex-direction: column;
}
ul {
list-style-type: none;
padding: 0;
margin: var(--spacing-xl) 0 0 0;
flex: 1;
}
i {
color: var(--grey-6);
}
i.live {
color: var(--ink);
}
li {
font-size: 14px;
}
.automation-item {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
border-radius: var(--border-radius-m);
padding: var(--spacing-s) var(--spacing-m);
margin-bottom: var(--spacing-xs);
color: var(--ink);
}
.automation-item i {
font-size: 24px;
margin-right: var(--spacing-m);
}
.automation-item:hover {
cursor: pointer;
background: var(--grey-1);
}
.automation-item.selected {
background: var(--grey-2);
}
</style>

View File

@ -1,50 +1,41 @@
<script> <script>
import { automationStore } from "builderStore" import AutomationList from "./AutomationList.svelte"
import AutomationList from "./AutomationList/AutomationList.svelte" import CreateAutomationModal from "./CreateAutomationModal.svelte"
import BlockList from "./BlockList/BlockList.svelte" import { Modal } from "@budibase/bbui"
import { Heading } from "@budibase/bbui" import { automationStore, backendUiStore } from "builderStore"
import { Spacer } from "@budibase/bbui" import { notifier } from "builderStore/store/notifications"
let selectedTab = "AUTOMATIONS" let selectedTab = "AUTOMATIONS"
let modal
</script> </script>
<Heading black small> <div class="title">
<span <h1>Automations</h1>
data-cy="automation-list" <i on:click={modal.show} class="ri-add-circle-fill" />
class="hoverable automation-header" </div>
class:selected={selectedTab === 'AUTOMATIONS'}
on:click={() => (selectedTab = 'AUTOMATIONS')}>
Automations
</span>
{#if $automationStore.selectedAutomation}
<span
data-cy="add-automation-component"
class="hoverable"
class:selected={selectedTab === 'ADD'}
on:click={() => (selectedTab = 'ADD')}>
Steps
</span>
{/if}
</Heading>
<Spacer medium />
{#if selectedTab === 'AUTOMATIONS'}
<AutomationList /> <AutomationList />
{:else if selectedTab === 'ADD'} <Modal bind:this={modal}>
<BlockList /> <CreateAutomationModal />
{/if} </Modal>
<style> <style>
header { .title {
font-size: 18px;
font-weight: 600;
background: none;
display: flex; display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: var(--spacing-xl);
} }
.title h1 {
.automation-header { font-size: var(--font-size-m);
margin-right: var(--spacing-xl); font-weight: 500;
margin: 0;
}
.title i {
font-size: 20px;
}
.title i:hover {
cursor: pointer;
color: var(--blue);
} }
span:not(.selected) { span:not(.selected) {

View File

@ -1,68 +0,0 @@
<script>
import { automationStore } from "builderStore"
import analytics from "analytics"
export let blockDefinition
export let stepId
export let blockType
function addBlockToAutomation() {
automationStore.actions.addBlockToAutomation({
...blockDefinition,
inputs: blockDefinition.inputs || {},
stepId,
type: blockType,
})
analytics.captureEvent("Added Automation Block", {
name: blockDefinition.name,
})
}
</script>
<div
class="automation-block hoverable"
on:click={addBlockToAutomation}
data-cy={stepId}>
<div><i class={blockDefinition.icon} /></div>
<div class="automation-text">
<h4>{blockDefinition.name}</h4>
<p>{blockDefinition.description}</p>
</div>
</div>
<style>
.automation-block {
display: grid;
grid-template-columns: 20px auto;
align-items: center;
margin-top: var(--spacing-s);
padding: var(--spacing-m);
border-radius: var(--border-radius-m);
}
.automation-block:hover {
background-color: var(--grey-1);
}
.automation-block:first-child {
margin-top: 0;
}
i {
color: var(--grey-7);
font-size: 20px;
}
.automation-text {
margin-left: 16px;
}
.automation-text h4 {
font-size: 14px;
font-weight: 500;
margin-bottom: 5px;
margin-top: 0;
}
.automation-text p {
font-size: 12px;
color: var(--grey-7);
margin: 0;
}
</style>

View File

@ -1,48 +0,0 @@
<script>
import { sortBy } from "lodash/fp"
import { automationStore } from "builderStore"
import AutomationBlock from "./AutomationBlock.svelte"
import FlatButtonGroup from "components/userInterface/FlatButtonGroup.svelte"
let selectedTab = "TRIGGER"
let buttonProps = []
$: blocks = sortBy(entry => entry[1].name)(
Object.entries($automationStore.blockDefinitions[selectedTab])
)
$: {
if ($automationStore.selectedAutomation.hasTrigger()) {
buttonProps = [
{ value: "ACTION", text: "Action" },
{ value: "LOGIC", text: "Logic" },
]
if (selectedTab === "TRIGGER") {
selectedTab = "ACTION"
}
} else {
buttonProps = [{ value: "TRIGGER", text: "Trigger" }]
if (selectedTab !== "TRIGGER") {
selectedTab = "TRIGGER"
}
}
}
function onChangeTab(tab) {
selectedTab = tab
}
</script>
<section>
<FlatButtonGroup value={selectedTab} {buttonProps} onChange={onChangeTab} />
<div id="blocklist">
{#each blocks as [stepId, blockDefinition]}
<AutomationBlock {blockDefinition} {stepId} blockType={selectedTab} />
{/each}
</div>
</section>
<style>
#blocklist {
margin-top: var(--spacing-xl);
}
</style>

View File

@ -0,0 +1,86 @@
<script>
import { automationStore, backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import { DropdownMenu } from "@budibase/bbui"
import { DropdownContainer, DropdownItem } from "components/common/Dropdowns"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
export let automation
let anchor
let dropdown
let confirmDeleteDialog
$: instanceId = $backendUiStore.selectedDatabase._id
function showModal() {
dropdown.hide()
confirmDeleteDialog.show()
}
async function deleteAutomation() {
await automationStore.actions.delete({
instanceId,
automation,
})
notifier.success("Automation deleted.")
}
</script>
<div bind:this={anchor} class="icon" on:click={dropdown.show}>
<i class="ri-more-line" />
</div>
<DropdownMenu align="left" {anchor} bind:this={dropdown}>
<DropdownContainer>
<DropdownItem
icon="ri-delete-bin-line"
title="Delete"
on:click={showModal} />
</DropdownContainer>
</DropdownMenu>
<ConfirmDialog
bind:this={confirmDeleteDialog}
okText="Delete Automation"
onOk={deleteAutomation}
title="Confirm Delete">
Are you sure you wish to delete the automation
<i>{automation.name}?</i>
This action cannot be undone.
</ConfirmDialog>
<style>
div.icon {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
}
div.icon i {
font-size: 16px;
}
ul {
list-style: none;
margin: 0;
padding: var(--spacing-s) 0;
}
li {
display: flex;
font-family: var(--font-sans);
font-size: var(--font-size-xs);
color: var(--ink);
padding: var(--spacing-s) var(--spacing-m);
margin: auto 0;
align-items: center;
cursor: pointer;
}
li:hover {
background-color: var(--grey-2);
}
li:active {
color: var(--blue);
}
</style>

View File

@ -1,3 +0,0 @@
export { default as AutomationPanel } from "./AutomationPanel.svelte"
export { default as BlockList } from "./BlockList/BlockList.svelte"
export { default as AutomationList } from "./AutomationList/AutomationList.svelte"

View File

@ -1,6 +1,6 @@
<script> <script>
import TableSelector from "./ParamInputs/TableSelector.svelte" import TableSelector from "./TableSelector.svelte"
import RowSelector from "./ParamInputs/RowSelector.svelte" import RowSelector from "./RowSelector.svelte"
import { Input, TextArea, Select, Label } from "@budibase/bbui" import { Input, TextArea, Select, Label } from "@budibase/bbui"
import { automationStore } from "builderStore" import { automationStore } from "builderStore"
import BindableInput from "../../userInterface/BindableInput.svelte" import BindableInput from "../../userInterface/BindableInput.svelte"
@ -22,7 +22,7 @@
if (automation.trigger) { if (automation.trigger) {
allSteps = [automation.trigger, ...allSteps] allSteps = [automation.trigger, ...allSteps]
} }
const blockIdx = allSteps.findIndex(step => step.id === block.id) const blockIdx = allSteps.findIndex((step) => step.id === block.id)
// Extract all outputs from all previous steps as available bindings // Extract all outputs from all previous steps as available bindings
let bindings = [] let bindings = []
@ -44,13 +44,12 @@
} }
</script> </script>
<div class="container" data-cy="automation-block-setup">
<div class="block-label">{block.name}</div> <div class="block-label">{block.name}</div>
{#each inputs as [key, value]} {#each inputs as [key, value]}
<div class="bb-margin-xl block-field"> <div class="block-field">
<Label extraSmall grey>{value.title}</Label> <Label extraSmall grey>{value.title}</Label>
{#if value.type === 'string' && value.enum} {#if value.type === 'string' && value.enum}
<Select bind:value={block.inputs[key]} thin secondary> <Select bind:value={block.inputs[key]} extraThin secondary>
<option value="">Choose an option</option> <option value="">Choose an option</option>
{#each value.enum as option, idx} {#each value.enum as option, idx}
<option value={option}> <option value={option}>
@ -59,7 +58,7 @@
{/each} {/each}
</Select> </Select>
{:else if value.customType === 'password'} {:else if value.customType === 'password'}
<Input type="password" thin bind:value={block.inputs[key]} /> <Input type="password" extraThin bind:value={block.inputs[key]} />
{:else if value.customType === 'table'} {:else if value.customType === 'table'}
<TableSelector bind:value={block.inputs[key]} /> <TableSelector bind:value={block.inputs[key]} />
{:else if value.customType === 'row'} {:else if value.customType === 'row'}
@ -67,13 +66,12 @@
{:else if value.type === 'string' || value.type === 'number'} {:else if value.type === 'string' || value.type === 'number'}
<BindableInput <BindableInput
type="string" type="string"
thin extraThin
bind:value={block.inputs[key]} bind:value={block.inputs[key]}
{bindings} /> {bindings} />
{/if} {/if}
</div> </div>
{/each} {/each}
</div>
<style> <style>
.block-field { .block-field {
@ -82,7 +80,7 @@
.block-label { .block-label {
font-weight: 500; font-weight: 500;
font-size: 14px; font-size: var(--font-size-xs);
color: var(--grey-7); color: var(--grey-7);
} }

View File

@ -1,12 +1,14 @@
<script> <script>
import { backendUiStore } from "builderStore" import { backendUiStore } from "builderStore"
import { Input, Select, Label } from "@budibase/bbui" import { Input, Select, Label } from "@budibase/bbui"
import BindableInput from "../../../userInterface/BindableInput.svelte" import BindableInput from "../../userInterface/BindableInput.svelte"
export let value export let value
export let bindings export let bindings
$: table = $backendUiStore.tables.find(table => table._id === value?.tableId) $: table = $backendUiStore.tables.find(
(table) => table._id === value?.tableId
)
$: schemaFields = Object.entries(table?.schema ?? {}) $: schemaFields = Object.entries(table?.schema ?? {})
// Ensure any nullish tableId values get set to empty string so // Ensure any nullish tableId values get set to empty string so
@ -19,7 +21,7 @@
</script> </script>
<div class="block-field"> <div class="block-field">
<Select bind:value={value.tableId} thin secondary> <Select bind:value={value.tableId} extraThin secondary>
<option value="">Choose an option</option> <option value="">Choose an option</option>
{#each $backendUiStore.tables as table} {#each $backendUiStore.tables as table}
<option value={table._id}>{table.name}</option> <option value={table._id}>{table.name}</option>
@ -31,7 +33,7 @@
<div class="schema-fields"> <div class="schema-fields">
{#each schemaFields as [field, schema]} {#each schemaFields as [field, schema]}
{#if schemaHasOptions(schema)} {#if schemaHasOptions(schema)}
<Select label={field} thin secondary bind:value={value[field]}> <Select label={field} extraThin secondary bind:value={value[field]}>
<option value="">Choose an option</option> <option value="">Choose an option</option>
{#each schema.constraints.inclusion as option} {#each schema.constraints.inclusion as option}
<option value={option}>{option}</option> <option value={option}>{option}</option>
@ -39,7 +41,7 @@
</Select> </Select>
{:else if schema.type === 'string' || schema.type === 'number'} {:else if schema.type === 'string' || schema.type === 'number'}
<BindableInput <BindableInput
thin extraThin
bind:value={value[field]} bind:value={value[field]}
label={field} label={field}
type="string" type="string"

View File

@ -2,23 +2,27 @@
import { backendUiStore, automationStore } from "builderStore" import { backendUiStore, automationStore } from "builderStore"
import { notifier } from "builderStore/store/notifications" import { notifier } from "builderStore/store/notifications"
import AutomationBlockSetup from "./AutomationBlockSetup.svelte" import AutomationBlockSetup from "./AutomationBlockSetup.svelte"
import { Button, Input, Label } from "@budibase/bbui" import { Button } from "@budibase/bbui"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
let selectedTab = "SETUP" let selectedTab = "SETUP"
let confirmDeleteDialog let confirmDeleteDialog
$: instanceId = $backendUiStore.selectedDatabase._id $: instanceId = $backendUiStore.selectedDatabase._id
$: automation = $automationStore.selectedAutomation?.automation $: automation = $automationStore.selectedAutomation?.automation
$: allowDeleteBlock =
$automationStore.selectedBlock?.type !== "TRIGGER" ||
!automation?.definition?.steps?.length
$: name = automation?.name ?? "" $: name = automation?.name ?? ""
$: automationLive = automation?.live
function deleteAutomationBlock() { function setAutomationLive(live) {
automationStore.actions.deleteAutomationBlock( if (automation.live === live) {
$automationStore.selectedBlock return
) }
automation.live = live
automationStore.actions.save({ instanceId, automation })
if (live) {
notifier.info(`Automation ${automation.name} enabled.`)
} else {
notifier.danger(`Automation ${automation.name} disabled.`)
}
} }
async function testAutomation() { async function testAutomation() {
@ -39,110 +43,74 @@
}) })
notifier.success(`Automation ${automation.name} saved.`) notifier.success(`Automation ${automation.name} saved.`)
} }
async function deleteAutomation() {
await automationStore.actions.delete({
instanceId,
automation,
})
notifier.success("Automation deleted.")
}
</script> </script>
<section> <div class="title">
<header> <h1>Setup</h1>
<span <i
class="hoverable" class:highlighted={automationLive}
class:selected={selectedTab === 'SETUP'} class:hoverable={automationLive}
on:click={() => (selectedTab = 'SETUP')}> on:click={() => setAutomationLive(false)}
Setup class="ri-stop-circle-fill" />
</span> <i
</header> class:highlighted={!automationLive}
<div class="content"> class:hoverable={!automationLive}
data-cy="activate-automation"
on:click={() => setAutomationLive(true)}
class="ri-play-circle-fill" />
</div>
{#if $automationStore.selectedBlock} {#if $automationStore.selectedBlock}
<AutomationBlockSetup bind:block={$automationStore.selectedBlock} /> <AutomationBlockSetup bind:block={$automationStore.selectedBlock} />
{:else if $automationStore.selectedAutomation} {:else if $automationStore.selectedAutomation}
<div class="block-label">Automation <b>{automation.name}</b></div> <div class="block-label">{automation.name}</div>
<Button secondary wide on:click={testAutomation}>Test Automation</Button> <Button secondary wide on:click={testAutomation}>Test Automation</Button>
{/if} {/if}
</div>
<div class="buttons">
{#if $automationStore.selectedBlock}
<Button <Button
green secondary
wide wide
data-cy="save-automation-setup" data-cy="save-automation-setup"
on:click={saveAutomation}> on:click={saveAutomation}>
Save Automation Save Automation
</Button> </Button>
<Button
disabled={!allowDeleteBlock}
red
wide
on:click={deleteAutomationBlock}>
Delete Step
</Button>
{:else if $automationStore.selectedAutomation}
<Button
green
wide
data-cy="save-automation-setup"
on:click={saveAutomation}>
Save Automation
</Button>
<Button red wide on:click={() => confirmDeleteDialog.show()}>
Delete Automation
</Button>
{/if}
</div>
</section>
<ConfirmDialog
bind:this={confirmDeleteDialog}
title="Confirm Delete"
body={`Are you sure you wish to delete the automation '${name}'?`}
okText="Delete Automation"
onOk={deleteAutomation} />
<style> <style>
section { .title {
display: flex;
flex-direction: column;
height: 100%;
justify-content: flex-start;
align-items: stretch;
}
header {
font-size: 18px;
font-weight: 600;
font-family: inter, sans-serif;
display: flex; display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: var(--spacing-xl); gap: var(--spacing-xs);
color: var(--ink);
} }
header > span { .title h1 {
font-size: var(--font-size-m);
font-weight: 500;
margin: 0;
flex: 1 1 auto;
}
.title i {
font-size: 20px;
color: var(--grey-5); color: var(--grey-5);
margin-right: var(--spacing-xl);
cursor: pointer;
} }
.selected { .title i.highlighted {
color: var(--ink); color: var(--ink);
} }
.title i.hoverable:hover {
cursor: pointer;
color: var(--blue);
}
.block-label { .block-label {
font-size: var(--font-size-xs);
font-weight: 500; font-weight: 500;
font-size: 14px;
color: var(--grey-7); color: var(--grey-7);
margin-bottom: var(--spacing-xl);
} }
.content { .footer {
flex: 1 0 auto; flex: 1 1 auto;
} display: flex;
flex-direction: column;
.buttons { justify-content: flex-end;
display: grid; align-items: stretch;
gap: var(--spacing-m);
} }
</style> </style>

View File

@ -6,7 +6,7 @@
</script> </script>
<div class="block-field"> <div class="block-field">
<Select bind:value secondary thin> <Select bind:value secondary extraThin>
<option value="">Choose an option</option> <option value="">Choose an option</option>
{#each $backendUiStore.tables as table} {#each $backendUiStore.tables as table}
<option value={table._id}>{table.name}</option> <option value={table._id}>{table.name}</option>

View File

@ -1 +0,0 @@
export { default as SetupPanel } from "./SetupPanel.svelte"

View File

@ -43,7 +43,7 @@
width: 24px; width: 24px;
background: var(--grey-4); background: var(--grey-4);
right: var(--spacing-s); right: var(--spacing-s);
bottom: 9px; bottom: 5px;
} }
button:hover { button:hover {
background: var(--grey-5); background: var(--grey-5);

View File

@ -1,6 +1,7 @@
<script> <script>
import { automationStore } from "builderStore" import { automationStore } from "builderStore"
import { AutomationPanel, SetupPanel } from "components/automation" import AutomationPanel from "components/automation/AutomationPanel/AutomationPanel.svelte"
import SetupPanel from "components/automation/SetupPanel/SetupPanel.svelte"
</script> </script>
<!-- routify:options index=3 --> <!-- routify:options index=3 -->
@ -19,21 +20,32 @@
</div> </div>
<style> <style>
.content {
position: relative;
}
.root { .root {
height: calc(100% - 60px); height: calc(100% - 60px);
display: grid; display: grid;
grid-template-columns: 300px minmax(510px, 1fr) 300px; grid-template-columns: 260px minmax(510px, 1fr) 260px;
background: var(--grey-1); background: var(--grey-2);
line-height: 1;
} }
.nav { .nav {
overflow-y: auto; overflow-y: auto;
background: var(--white); background: var(--white);
padding: var(--spacing-xl) var(--spacing-xl); padding: var(--spacing-l) var(--spacing-xl);
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
gap: var(--spacing-l);
}
.content {
position: relative;
padding: var(--spacing-l) 40px;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
gap: var(--spacing-l);
overflow: hidden;
} }
</style> </style>

View File

@ -1,5 +1,5 @@
<script> <script>
import { AutomationBuilder } from "components/automation" import AutomationBuilder from "components/automation/AutomationBuilder/AutomationBuilder.svelte"
</script> </script>
<AutomationBuilder /> <AutomationBuilder />

View File

@ -11,7 +11,7 @@
<style> <style>
i { i {
font-size: var(--font-size-xl); font-size: var(--font-size-m);
color: var(--grey-4); color: var(--grey-5);
} }
</style> </style>

View File

@ -27,7 +27,7 @@
<style> <style>
i { i {
font-size: var(--font-size-xl); font-size: var(--font-size-m);
color: var(--grey-4); color: var(--grey-5);
} }
</style> </style>

View File

@ -11,7 +11,7 @@
<style> <style>
i { i {
font-size: var(--font-size-xl); font-size: var(--font-size-m);
color: var(--grey-4); color: var(--grey-4);
} }
</style> </style>