make alerts live, more hooks, app notifications

This commit is contained in:
Martin McKeaveney 2020-05-28 23:31:55 +01:00
parent a3bebe3330
commit 50a582a726
22 changed files with 208 additions and 92 deletions

View File

@ -38,6 +38,7 @@
]
},
"dependencies": {
"@beyonk/svelte-notifications": "^2.0.3",
"@budibase/client": "^0.0.32",
"@nx-js/compiler-util": "^2.0.0",
"codemirror": "^5.51.0",

View File

@ -152,7 +152,7 @@ export default {
{
find: "builderStore",
replacement: path.resolve(projectRootDir, "src/builderStore"),
},
}
],
customResolver,
}),

View File

@ -7,6 +7,8 @@
import AppNotification, {
showAppNotification,
} from "components/common/AppNotification.svelte"
import { NotificationDisplay } from '@beyonk/svelte-notifications'
function showErrorBanner() {
showAppNotification({
@ -24,8 +26,7 @@
$basepath = "/_builder"
</script>
<AppNotification />
<NotificationDisplay />
<Modal>
<Router {routes} />
</Modal>

View File

@ -77,7 +77,8 @@
}
.budibase__input {
width: 250px;
width: 100%;
max-width: 250px;
height: 35px;
border-radius: 3px;
border: 1px solid #DBDBDB;

View File

@ -1,6 +1,7 @@
import mustache from "mustache"
// TODO: tidy up import
import blockDefinitions from "../../../pages/[application]/workflow/WorkflowPanel/blockDefinitions"
import { generate } from "shortid"
/**
* Class responsible for the traversing of the workflow definition.
@ -14,7 +15,10 @@ export default class Workflow {
addBlock(block) {
let node = this.workflow.definition
while (node.next) node = node.next
node.next = block
node.next = {
id: generate(),
...block
}
}
updateBlock(updatedBlock, id) {
@ -70,7 +74,7 @@ export default class Workflow {
type: block.type,
params: block.params,
args,
heading: block.actionId,
heading: definition.actionId,
body: mustache.render(tagline, args),
})

View File

@ -82,7 +82,7 @@ const workflowActions = store => ({
},
deleteWorkflowBlock: block => {
store.update(state => {
state.currentWorkflow.deleteBlock(block._id)
state.currentWorkflow.deleteBlock(block.id)
state.selectedWorkflowBlock = null
return state
})

View File

@ -32,6 +32,7 @@
$: {
events = Object.keys(component)
// TODO: use real events
.filter(propName => ["onChange", "onClick", "onLoad"].includes(propName))
.map(propName => ({
name: propName,

View File

@ -9,7 +9,7 @@
EVENT_TYPE_MEMBER_NAME,
allHandlers,
} from "components/common/eventHandlers"
import { store } from "builderStore"
import { store, workflowStore } from "builderStore"
import StateBindingOptions from "../PropertyCascader/StateBindingOptions.svelte"
import { ArrowDownIcon } from "components/common/Icons/"
@ -22,6 +22,13 @@
<div class="handler-option">
<span>{parameter.name}</span>
<div class="handler-input">
{#if parameter.name === 'workflow'}
<select class="budibase__input" {onChange} value={parameter.value}>
{#each $workflowStore.workflows as workflow}
<option value={workflow._id}>{workflow.name}</option>
{/each}
</select>
{:else}
<Input on:change={onChange} value={parameter.value} />
<button on:click={() => (isOpen = !isOpen)}>
<div class="icon" style={`transform: rotate(${isOpen ? 0 : 90}deg);`}>
@ -35,6 +42,7 @@
isOpen = false
}} />
{/if}
{/if}
</div>
</div>

View File

@ -56,11 +56,10 @@
on:click={() => $goto(`/settings`)}>
<SettingsIcon />
</span>
<span
class:active={false}
class="topnavitemright"
on:click={() => (location = `/${application}`)}>
<span class:active={false} class="topnavitemright">
<a href={`/${application}`} target="_blank">
<PreviewIcon />
</a>
</span>
</div>
</div>
@ -84,6 +83,11 @@
flex-direction: column;
}
a {
text-transform: none;
color: var(--ink-lighter);
}
.top-nav {
flex: 0 0 auto;
height: 60px;

View File

@ -1,5 +1,6 @@
<script>
import { store, backendUiStore, workflowStore } from "builderStore"
import { notifier } from '@beyonk/svelte-notifications'
import api from "builderStore/api"
import ActionButton from "components/common/ActionButton.svelte"
@ -16,6 +17,7 @@
workflow: $workflowStore.currentWorkflow.workflow,
})
onClosed()
notifier.danger("Workflow deleted.")
}
</script>

View File

@ -1,13 +1,14 @@
<script>
import { onMount, getContext } from "svelte"
import { backendUiStore, workflowStore } from "builderStore"
import { notifier } from "@beyonk/svelte-notifications";
import api from "builderStore/api"
import WorkflowBlockSetup from "./WorkflowBlockSetup.svelte"
import DeleteWorkflowModal from "./DeleteWorkflowModal.svelte"
const { open, close } = getContext("simple-modal")
$: workflow = $workflowStore.currentWorkflow
$: workflow = $workflowStore.currentWorkflow && $workflowStore.currentWorkflow.workflow
$: workflowBlock = $workflowStore.selectedWorkflowBlock
function deleteWorkflow() {
@ -21,8 +22,8 @@
}
function deleteWorkflowBlock() {
// TODO: implement, need to put IDs against workflow blocks
workflowStore.actions.deleteWorkflowBlock(workflowBlock)
notifier.info("Workflow block deleted.");
}
</script>

View File

@ -1,5 +1,5 @@
<script>
import { backendUiStore } from "builderStore"
import { backendUiStore, store } from "builderStore"
export let workflowBlock
@ -8,6 +8,7 @@
$: workflowParams = workflowBlock.params
? Object.entries(workflowBlock.params)
: []
$: components = Object.values($store.components).filter(comp => comp.name)
// $: workflowArgs = workflowBlock.args ? Object.keys(workflowBlock.args) : []
</script>
@ -18,7 +19,15 @@
<div class="uk-margin block-field">
<label class="uk-form-label">{parameter}</label>
<div class="uk-form-controls">
{#if type === 'number'}
{#if Array.isArray(type)}
<select
class="budibase__input"
bind:value={workflowBlock.args[parameter]}>
{#each type as option}
<option value={option}>{option}</option>
{/each}
</select>
{:else if type === 'number'}
<input
type="number"
class="budibase__input"
@ -32,11 +41,11 @@
{/each}
</select>
{:else if type === 'component'}
<!-- <select>
{#each $store.components as question}
<option value={question}>{question.text}</option>
<select class="budibase__input">
{#each components as component}
<option value={component.id}>{component.name}</option>
{/each}
</select> -->
</select>
{:else if type === 'string'}
<input
type="text"

View File

@ -1,17 +1,23 @@
<script>
import { onMount } from "svelte"
import { workflowStore, backendUiStore } from "builderStore"
import { notifier } from "@beyonk/svelte-notifications"
import Flowchart from "./svelte-flows/Flowchart.svelte"
import api from "builderStore/api"
let canvas
let workflow
let selectedWorkflow
let uiTree
let instanceId = $backendUiStore.selectedDatabase._id
$: workflow = $workflowStore.currentWorkflow
// TODO: better naming
$: selectedWorkflow = $workflowStore.currentWorkflow
$: if (workflow) uiTree = workflow ? workflow.createUiTree() : []
$: workflowLive = selectedWorkflow && selectedWorkflow.workflow.live
$: if (selectedWorkflow)
uiTree = selectedWorkflow ? selectedWorkflow.createUiTree() : []
$: instanceId = $backendUiStore.selectedDatabase._id
function onDelete(block) {
// TODO finish
@ -24,17 +30,37 @@
return state
})
}
function setWorkflowLive(live) {
const { workflow } = selectedWorkflow
workflow.live = live
workflowStore.actions.save({ instanceId, workflow })
if (live) {
notifier.info(`Workflow ${workflow.name} enabled.`)
} else {
notifier.danger(`Workflow ${workflow.name} disabled.`)
}
}
</script>
<section>
<Flowchart blocks={uiTree} {onSelect} on:delete={onDelete} />
<footer>
<button class="stop-button hoverable">
<i class="ri-stop-fill" />
{#if selectedWorkflow}
<button
class:highlighted={workflowLive}
class:hoverable={workflowLive}
class="stop-button hoverable">
<i class="ri-stop-fill" on:click={() => setWorkflowLive(false)} />
</button>
<button class="play-button hoverable">
<button
class:highlighted={!workflowLive}
class:hoverable={!workflowLive}
class="play-button hoverable"
on:click={() => setWorkflowLive(true)}>
<i class="ri-play-fill" />
</button>
{/if}
</footer>
</section>
@ -61,11 +87,11 @@
margin-right: 24px;
}
.play-button:hover {
.play-button.highlighted {
background: var(--primary);
}
.stop-button:hover {
.stop-button.highlighted {
background: var(--coral);
}
</style>

View File

@ -0,0 +1,9 @@
<svg
width="9"
height="75"
viewBox="0 0 9 75"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path d="M5.0625 70H9L4.5 75L0 70H3.9375V65H5.0625V70Z" fill="#ADAEC4" />
<rect x="4" width="1" height="65" fill="#ADAEC4" />
</svg>

After

Width:  |  Height:  |  Size: 241 B

View File

@ -1,5 +1,6 @@
<script>
import FlowItem from "./FlowItem.svelte"
import Arrow from "./Arrow.svelte";
export let blocks = []
export let onSelect
@ -9,7 +10,7 @@
{#each blocks as block, idx}
<FlowItem {onSelect} {block} />
{#if idx !== blocks.length - 1}
<i class="ri-arrow-down-line" />
<Arrow />
{/if}
{/each}
</section>

View File

@ -1,16 +1,27 @@
<script>
import { fade } from "svelte/transition"
export let onSelect
export let block
function selectBlock() {
onSelect(block)
}
console.log(block)
</script>
<div class={`${block.type} hoverable`} on:click={selectBlock}>
<header>{block.heading}</header>
<div transition:fade class={`${block.type} hoverable`} on:click={selectBlock}>
<header>
{#if block.type === 'TRIGGER'}
<i class="ri-lightbulb-fill" />
When this happens...
{:else if block.type === 'ACTION'}
<i class="ri-flashlight-fill" />
Do this...
{:else if block.type === 'LOGIC'}
<i class="ri-pause-fill" />
Only continue if...
{/if}
</header>
<hr />
<p>
{@html block.body}
@ -21,7 +32,6 @@
div {
width: 320px;
padding: 20px;
margin-bottom: 60px;
border-radius: 5px;
transition: 0.3s all;
box-shadow: 0 4px 30px 0 rgba(57, 60, 68, 0.08);
@ -30,6 +40,18 @@
color: var(--white);
}
header {
font-size: 16px;
font-weight: 500;
display: flex;
align-items: center;
}
header i {
font-size: 20px;
margin-right: 5px;
}
.ACTION {
background-color: var(--white);
color: var(--font);

View File

@ -52,16 +52,21 @@
grid-gap: 5px;
grid-auto-flow: column;
grid-auto-columns: 1fr 1fr 1fr;
margin-bottom: 10px;
}
.subtabs span {
transition: 0.3s all;
text-align: center;
color: var(--font);
color: var(--dark-grey);
font-weight: 500;
padding: 10px;
}
.subtabs span.selected {
border-bottom: 4px solid var(--primary);
background: var(--dark-grey);
color: var(--white);
border-radius: 2px;
}
.subtabs span:not(.selected) {

View File

@ -1,5 +1,6 @@
<script>
import { store, backendUiStore, workflowStore } from "builderStore"
import { notifier } from '@beyonk/svelte-notifications'
import api from "builderStore/api"
import ActionButton from "components/common/ActionButton.svelte"
@ -17,6 +18,7 @@
instanceId,
})
onClosed()
notifier.success(`Workflow ${name} created.`)
}
</script>

View File

@ -1,5 +1,6 @@
<script>
import Modal from "svelte-simple-modal"
import { notifier } from "@beyonk/svelte-notifications";
import { onMount, getContext } from "svelte"
import { backendUiStore, workflowStore } from "builderStore"
import api from "builderStore/api"
@ -8,7 +9,7 @@
const { open, close } = getContext("simple-modal")
$: currentWorkflowId =
$workflowStore.currentWorkflow && $workflowStore.currentWorkflow._id
$workflowStore.currentWorkflow && $workflowStore.currentWorkflow.workflow._id
function newWorkflow() {
open(
@ -24,12 +25,14 @@
workflowStore.actions.fetch($backendUiStore.selectedDatabase._id)
})
function saveWorkflow() {
async function saveWorkflow() {
const workflow = $workflowStore.currentWorkflow.workflow
// TODO: Clean up args
workflowStore.actions.save({
await workflowStore.actions.save({
instanceId: $backendUiStore.selectedDatabase._id,
workflow: $workflowStore.currentWorkflow.workflow,
workflow
})
notifier.success(`Workflow ${workflow.name} saved.`);
}
</script>
@ -82,7 +85,7 @@
display: flex;
align-items: center;
border-radius: 3px;
height: 40px;
height: 32px;
font-weight: 500;
}

View File

@ -5,17 +5,6 @@
import api from "builderStore/api"
import blockDefinitions from "./blockDefinitions"
const WORKFLOW_TABS = [
{
name: "Workflows",
key: "WORKFLOWS",
},
{
name: "Add",
key: "ADD",
},
]
let selectedTab = "WORKFLOWS"
let definitions = []
</script>
@ -57,15 +46,4 @@
span:not(.selected) {
color: var(--dark-grey);
}
.delete-workflow-button {
font-family: Roboto;
width: 100%;
border: solid 1px #f2f2f2;
border-radius: 2px;
background: var(--white);
height: 32px;
font-size: 12px;
font-weight: 500;
}
</style>

View File

@ -2,7 +2,7 @@ const ACTION = {
SET_STATE: {
name: "Update UI State",
tagline: "Update <b>{{path}}</b> to <b>{{value}}</b>",
icon: "",
icon: "ri-refresh-line",
description: "Update your User Interface with some data.",
environment: "CLIENT",
params: {
@ -38,8 +38,8 @@ const ACTION = {
},
},
FIND_RECORD: {
description: "Delete a record from your database.",
icon: "ri-delete-bin-line",
description: "Find a record in your database.",
icon: "ri-search-line",
name: "Find Record",
environment: "SERVER",
params: {
@ -59,7 +59,7 @@ const ACTION = {
},
SEND_EMAIL: {
description: "Send an email.",
tagline: "Send email to {{to}}",
tagline: "Send email to <b>{{to}}</b>",
icon: "ri-mail-open-fill",
name: "Send Email",
environment: "SERVER",
@ -73,9 +73,9 @@ const ACTION = {
}
const TRIGGER = {
SAVE_RECORD: {
RECORD_SAVED: {
name: "Record Saved",
icon: "ri-delete-bin-line",
icon: "ri-save-line",
tagline: "Record is added to {{model}}",
description: "Save a record to your database.",
environment: "SERVER",
@ -83,39 +83,72 @@ const TRIGGER = {
model: "model",
},
},
RECORD_DELETED: {
name: "Record Deleted",
icon: "ri-delete-bin-line",
tagline: "Record is deleted from <b>{{model}}</b>",
description: "Fired when a record is deleted from your database.",
environment: "SERVER",
params: {
model: "model"
},
},
CLICK: {
name: "Click",
icon: "ri-cursor-line",
tagline: "{{component}} is clicked",
description: "Trigger when you click on an element in the UI.",
environment: "CLIENT",
params: {
component: "component"
}
},
LOAD: {
name: "Load",
icon: "ri-loader-line",
tagline: "{{component}} is loaded",
description: "Trigger an element has finished loading.",
environment: "CLIENT",
params: {
component: "component"
}
},
INPUT: {
name: "Input",
icon: "ri-text",
description: "Trigger when you environment into an input box.",
tagline: "Text entered into {{component}",
description: "Trigger when you type into an input box.",
environment: "CLIENT",
params: {
component: "component"
}
},
}
const LOGIC = {
FILTER: {
name: "Filter",
tagline: "{{key}} {{condition}} {{value}}",
tagline: "{{field}} <b>{{condition}}</b> {{value}}",
icon: "ri-git-branch-line",
description: "Filter any workflows which do not meet certain conditions.",
environment: "CLIENT",
params: {
if: "string",
field: "string",
condition: [
"equals"
],
value: "string"
},
},
DELAY: {
name: "Delay",
icon: "ri-git-branch-line",
icon: "ri-time-fill",
tagline: "Delay for <b>{{time}}</b> milliseconds",
description: "Delay the workflow until an amount of time has passed.",
environment: "CLIENT",
params: {
time: "number",
},
},
}

View File

@ -1,7 +1,7 @@
import get from "lodash/fp/get"
/**
* The workflow orhestrator is a class responsible for executing workflows.
* The workflow orchestrator is a class responsible for executing workflows.
* It relies on the strategy pattern, which allows composable behaviour to be
* passed into its execute() function. This allows custom execution behaviour based
* on where the orchestrator is run.
@ -30,6 +30,7 @@ export default class Orchestrator {
// Execute a workflow from a running budibase app
export const clientStrategy = {
delay: ms => new Promise(resolve => setTimeout(resolve, ms)),
context: {},
bindContextArgs: function(args, api) {
const mappedArgs = { ...args }
@ -80,6 +81,10 @@ export const clientStrategy = {
SET_STATE: block.args,
}
}
if (block.actionId === "DELAY") {
await this.delay(block.args.time)
}
}
// this workflow block gets executed on the server
@ -102,6 +107,6 @@ export const clientStrategy = {
console.log("workflowContext", this.context)
// TODO: clean this up, don't pass all those args
this.run({ workflow: workflow.next, instanceId, api })
await this.run({ workflow: workflow.next, instanceId, api })
},
}