Merge branch 'rename-workflow-automation' of github.com:Budibase/budibase into async-workflow-blocks

This commit is contained in:
mike12345567 2020-09-21 14:51:48 +01:00
commit eb494b4698
107 changed files with 8208 additions and 684 deletions

View File

@ -28,5 +28,8 @@
"format": "prettier --write \"{,!(node_modules)/**/}*.{js,jsx,svelte}\"",
"test:e2e": "lerna run cy:test",
"test:e2e:ci": "lerna run cy:ci"
},
"dependencies": {
"@fortawesome/fontawesome": "^1.1.8"
}
}

View File

@ -4,6 +4,6 @@ node_modules_win
package-lock.json
release/
dist/
cypress/screenshots
cypress/videos
routify
cypress/videos
cypress/screenshots

View File

@ -1,27 +1,27 @@
context("Create a workflow", () => {
context("Create a automation", () => {
before(() => {
cy.server()
cy.visit("localhost:4001/_builder")
cy.createApp(
"Workflow Test App",
"This app is used to test that workflows do in fact work!"
"Automation Test App",
"This app is used to test that automations do in fact work!"
)
})
// https://on.cypress.io/interacting-with-elements
it("should create a workflow", () => {
it("should create a automation", () => {
cy.createTestTableWithData()
cy.contains("workflow").click()
cy.contains("Create New Workflow").click()
cy.contains("automate").click()
cy.contains("Create New Automation").click()
cy.get("input").type("Add Record")
cy.contains("Save").click()
// Add trigger
cy.get("[data-cy=add-workflow-component]").click()
cy.get("[data-cy=add-automation-component]").click()
cy.get("[data-cy=RECORD_SAVED]").click()
cy.get("[data-cy=workflow-block-setup]").within(() => {
cy.get("[data-cy=automation-block-setup]").within(() => {
cy.get("select")
.first()
.select("dog")
@ -29,7 +29,7 @@ context("Create a workflow", () => {
// Create action
cy.get("[data-cy=SAVE_RECORD]").click()
cy.get("[data-cy=workflow-block-setup]").within(() => {
cy.get("[data-cy=automation-block-setup]").within(() => {
cy.get("select")
.first()
.select("dog")
@ -42,10 +42,10 @@ context("Create a workflow", () => {
})
// Save
cy.contains("Save Workflow").click()
cy.contains("Save Automation").click()
// Activate Workflow
cy.get("[data-cy=activate-workflow]").click()
// Activate Automation
cy.get("[data-cy=activate-automation]").click()
cy.contains("Add Record").should("be.visible")
cy.get(".stop-button.highlighted").should("be.visible")
})

View File

@ -63,9 +63,10 @@
}
},
"dependencies": {
"@budibase/bbui": "^1.33.0",
"@budibase/bbui": "^1.34.2",
"@budibase/client": "^0.1.21",
"@budibase/colorpicker": "^1.0.1",
"@fortawesome/fontawesome-free": "^5.14.0",
"@sentry/browser": "5.19.1",
"@svelteschool/svelte-forms": "^0.7.0",
"britecharts": "^2.16.0",

View File

@ -1,7 +1,8 @@
const apiCall = method => async (url, body) => {
const headers = {
"Content-Type": "application/json",
}
const apiCall = method => async (
url,
body,
headers = { "Content-Type": "application/json" }
) => {
const response = await fetch(url, {
method: method,
body: body && JSON.stringify(body),

View File

@ -1,28 +0,0 @@
/**
* buildStateOrigins
*
* Builds an object that details all the bound state in the application, and what updates it.
*
* @param screenDefinition - the screen definition metadata.
* @returns {Object} an object with the client state values and how they are managed.
*/
export const buildStateOrigins = screenDefinition => {
const origins = {}
function traverse(propValue) {
for (let key in propValue) {
if (!Array.isArray(propValue[key])) continue
if (key === "_children") propValue[key].forEach(traverse)
for (let element of propValue[key]) {
if (element["##eventHandlerType"] === "Set State")
origins[element.parameters.path] = element
}
}
}
traverse(screenDefinition.props)
return origins
}

View File

@ -1,11 +1,11 @@
import { getStore } from "./store"
import { getBackendUiStore } from "./store/backend"
import { getWorkflowStore } from "./store/workflow/"
import { getAutomationStore } from "./store/automation/"
import analytics from "../analytics"
export const store = getStore()
export const backendUiStore = getBackendUiStore()
export const workflowStore = getWorkflowStore()
export const automationStore = getAutomationStore()
export const initialise = async () => {
try {

View File

@ -1,59 +1,59 @@
import { generate } from "shortid"
/**
* Class responsible for the traversing of the workflow definition.
* Workflow definitions are stored in linked lists.
* Class responsible for the traversing of the automation definition.
* Automation definitions are stored in linked lists.
*/
export default class Workflow {
constructor(workflow) {
this.workflow = workflow
export default class Automation {
constructor(automation) {
this.automation = automation
}
hasTrigger() {
return this.workflow.definition.trigger
return this.automation.definition.trigger
}
addBlock(block) {
// Make sure to add trigger if doesn't exist
if (!this.hasTrigger() && block.type === "TRIGGER") {
const trigger = { id: generate(), ...block }
this.workflow.definition.trigger = trigger
this.automation.definition.trigger = trigger
return trigger
}
const newBlock = { id: generate(), ...block }
this.workflow.definition.steps = [
...this.workflow.definition.steps,
this.automation.definition.steps = [
...this.automation.definition.steps,
newBlock,
]
return newBlock
}
updateBlock(updatedBlock, id) {
const { steps, trigger } = this.workflow.definition
const { steps, trigger } = this.automation.definition
if (trigger && trigger.id === id) {
this.workflow.definition.trigger = updatedBlock
this.automation.definition.trigger = updatedBlock
return
}
const stepIdx = steps.findIndex(step => step.id === id)
if (stepIdx < 0) throw new Error("Block not found.")
steps.splice(stepIdx, 1, updatedBlock)
this.workflow.definition.steps = steps
this.automation.definition.steps = steps
}
deleteBlock(id) {
const { steps, trigger } = this.workflow.definition
const { steps, trigger } = this.automation.definition
if (trigger && trigger.id === id) {
this.workflow.definition.trigger = null
this.automation.definition.trigger = null
return
}
const stepIdx = steps.findIndex(step => step.id === id)
if (stepIdx < 0) throw new Error("Block not found.")
steps.splice(stepIdx, 1)
this.workflow.definition.steps = steps
this.automation.definition.steps = steps
}
}

View File

@ -0,0 +1,126 @@
import { writable } from "svelte/store"
import api from "../../api"
import Automation from "./Automation"
import { cloneDeep } from "lodash/fp"
const automationActions = store => ({
fetch: async () => {
const responses = await Promise.all([
api.get(`/api/automations`),
api.get(`/api/automations/definitions/list`),
])
const jsonResponses = await Promise.all(responses.map(x => x.json()))
store.update(state => {
state.automations = jsonResponses[0]
state.blockDefinitions = {
TRIGGER: jsonResponses[1].trigger,
ACTION: jsonResponses[1].action,
LOGIC: jsonResponses[1].logic,
}
return state
})
},
create: async ({ name }) => {
const automation = {
name,
type: "automation",
definition: {
steps: [],
},
}
const CREATE_AUTOMATION_URL = `/api/automations`
const response = await api.post(CREATE_AUTOMATION_URL, automation)
const json = await response.json()
store.update(state => {
state.automations = [...state.automations, json.automation]
store.actions.select(json.automation)
return state
})
},
save: async ({ automation }) => {
const UPDATE_AUTOMATION_URL = `/api/automations`
const response = await api.put(UPDATE_AUTOMATION_URL, automation)
const json = await response.json()
store.update(state => {
const existingIdx = state.automations.findIndex(
existing => existing._id === automation._id
)
state.automations.splice(existingIdx, 1, json.automation)
state.automations = state.automations
store.actions.select(json.automation)
return state
})
},
delete: async ({ automation }) => {
const { _id, _rev } = automation
const DELETE_AUTOMATION_URL = `/api/automations/${_id}/${_rev}`
await api.delete(DELETE_AUTOMATION_URL)
store.update(state => {
const existingIdx = state.automations.findIndex(
existing => existing._id === _id
)
state.automations.splice(existingIdx, 1)
state.automations = state.automations
state.selectedAutomation = null
state.selectedBlock = null
return state
})
},
trigger: async ({ automation }) => {
const { _id } = automation
const TRIGGER_AUTOMATION_URL = `/api/automations/${_id}/trigger`
return await api.post(TRIGGER_AUTOMATION_URL)
},
select: automation => {
store.update(state => {
state.selectedAutomation = new Automation(cloneDeep(automation))
state.selectedBlock = null
return state
})
},
addBlockToAutomation: block => {
store.update(state => {
const newBlock = state.selectedAutomation.addBlock(cloneDeep(block))
state.selectedBlock = newBlock
return state
})
},
deleteAutomationBlock: block => {
store.update(state => {
const idx = state.selectedAutomation.automation.definition.steps.findIndex(
x => x.id === block.id
)
state.selectedAutomation.deleteBlock(block.id)
// Select next closest step
const steps = state.selectedAutomation.automation.definition.steps
let nextSelectedBlock
if (steps[idx] != null) {
nextSelectedBlock = steps[idx]
} else if (steps[idx - 1] != null) {
nextSelectedBlock = steps[idx - 1]
} else {
nextSelectedBlock =
state.selectedAutomation.automation.definition.trigger || null
}
state.selectedBlock = nextSelectedBlock
return state
})
},
})
export const getAutomationStore = () => {
const INITIAL_AUTOMATION_STATE = {
automations: [],
blockDefinitions: {
TRIGGER: [],
ACTION: [],
LOGIC: [],
},
selectedAutomation: null,
}
const store = writable(INITIAL_AUTOMATION_STATE)
store.actions = automationActions(store)
return store
}

View File

@ -0,0 +1,48 @@
import Automation from "../Automation"
import TEST_AUTOMATION from "./testAutomation"
const TEST_BLOCK = {
id: "AUXJQGZY7",
name: "Delay",
icon: "ri-time-fill",
tagline: "Delay for <b>{{time}}</b> milliseconds",
description: "Delay the automation until an amount of time has passed.",
params: { time: "number" },
type: "LOGIC",
args: { time: "5000" },
stepId: "DELAY",
}
describe("Automation Data Object", () => {
let automation
beforeEach(() => {
automation = new Automation({ ...TEST_AUTOMATION })
})
it("adds a automation block to the automation", () => {
automation.addBlock(TEST_BLOCK)
expect(automation.automation.definition)
})
it("updates a automation block with new attributes", () => {
const firstBlock = automation.automation.definition.steps[0]
const updatedBlock = {
...firstBlock,
name: "UPDATED",
}
automation.updateBlock(updatedBlock, firstBlock.id)
expect(automation.automation.definition.steps[0]).toEqual(updatedBlock)
})
it("deletes a automation block successfully", () => {
const { steps } = automation.automation.definition
const originalLength = steps.length
const lastBlock = steps[steps.length - 1]
automation.deleteBlock(lastBlock.id)
expect(automation.automation.definition.steps.length).toBeLessThan(
originalLength
)
})
})

View File

@ -1,5 +1,5 @@
export default {
name: "Test workflow",
name: "Test automation",
definition: {
steps: [
{
@ -68,7 +68,7 @@ export default {
stepId: "RECORD_SAVED",
},
},
type: "workflow",
type: "automation",
ok: true,
id: "b384f861f4754e1693835324a7fcca62",
rev: "1-aa1c2cbd868ef02e26f8fad531dd7e37",

View File

@ -28,6 +28,11 @@ export const getBackendUiStore = () => {
},
},
records: {
save: () =>
store.update(state => {
state.selectedView = state.selectedView
return state
}),
delete: () =>
store.update(state => {
state.selectedView = state.selectedView

View File

@ -1,126 +0,0 @@
import { writable } from "svelte/store"
import api from "../../api"
import Workflow from "./Workflow"
import { cloneDeep } from "lodash/fp"
const workflowActions = store => ({
fetch: async () => {
const responses = await Promise.all([
api.get(`/api/workflows`),
api.get(`/api/workflows/definitions/list`),
])
const jsonResponses = await Promise.all(responses.map(x => x.json()))
store.update(state => {
state.workflows = jsonResponses[0]
state.blockDefinitions = {
TRIGGER: jsonResponses[1].trigger,
ACTION: jsonResponses[1].action,
LOGIC: jsonResponses[1].logic,
}
return state
})
},
create: async ({ name }) => {
const workflow = {
name,
type: "workflow",
definition: {
steps: [],
},
}
const CREATE_WORKFLOW_URL = `/api/workflows`
const response = await api.post(CREATE_WORKFLOW_URL, workflow)
const json = await response.json()
store.update(state => {
state.workflows = [...state.workflows, json.workflow]
store.actions.select(json.workflow)
return state
})
},
save: async ({ workflow }) => {
const UPDATE_WORKFLOW_URL = `/api/workflows`
const response = await api.put(UPDATE_WORKFLOW_URL, workflow)
const json = await response.json()
store.update(state => {
const existingIdx = state.workflows.findIndex(
existing => existing._id === workflow._id
)
state.workflows.splice(existingIdx, 1, json.workflow)
state.workflows = state.workflows
store.actions.select(json.workflow)
return state
})
},
delete: async ({ workflow }) => {
const { _id, _rev } = workflow
const DELETE_WORKFLOW_URL = `/api/workflows/${_id}/${_rev}`
await api.delete(DELETE_WORKFLOW_URL)
store.update(state => {
const existingIdx = state.workflows.findIndex(
existing => existing._id === _id
)
state.workflows.splice(existingIdx, 1)
state.workflows = state.workflows
state.selectedWorkflow = null
state.selectedBlock = null
return state
})
},
trigger: async ({ workflow }) => {
const { _id } = workflow
const TRIGGER_WORKFLOW_URL = `/api/workflows/${_id}/trigger`
return await api.post(TRIGGER_WORKFLOW_URL)
},
select: workflow => {
store.update(state => {
state.selectedWorkflow = new Workflow(cloneDeep(workflow))
state.selectedBlock = null
return state
})
},
addBlockToWorkflow: block => {
store.update(state => {
const newBlock = state.selectedWorkflow.addBlock(cloneDeep(block))
state.selectedBlock = newBlock
return state
})
},
deleteWorkflowBlock: block => {
store.update(state => {
const idx = state.selectedWorkflow.workflow.definition.steps.findIndex(
x => x.id === block.id
)
state.selectedWorkflow.deleteBlock(block.id)
// Select next closest step
const steps = state.selectedWorkflow.workflow.definition.steps
let nextSelectedBlock
if (steps[idx] != null) {
nextSelectedBlock = steps[idx]
} else if (steps[idx - 1] != null) {
nextSelectedBlock = steps[idx - 1]
} else {
nextSelectedBlock =
state.selectedWorkflow.workflow.definition.trigger || null
}
state.selectedBlock = nextSelectedBlock
return state
})
},
})
export const getWorkflowStore = () => {
const INITIAL_WORKFLOW_STATE = {
workflows: [],
blockDefinitions: {
TRIGGER: [],
ACTION: [],
LOGIC: [],
},
selectedWorkflow: null,
}
const store = writable(INITIAL_WORKFLOW_STATE)
store.actions = workflowActions(store)
return store
}

View File

@ -1,48 +0,0 @@
import Workflow from "../Workflow"
import TEST_WORKFLOW from "./testWorkflow"
const TEST_BLOCK = {
id: "AUXJQGZY7",
name: "Delay",
icon: "ri-time-fill",
tagline: "Delay for <b>{{time}}</b> milliseconds",
description: "Delay the workflow until an amount of time has passed.",
params: { time: "number" },
type: "LOGIC",
args: { time: "5000" },
stepId: "DELAY",
}
describe("Workflow Data Object", () => {
let workflow
beforeEach(() => {
workflow = new Workflow({ ...TEST_WORKFLOW })
})
it("adds a workflow block to the workflow", () => {
workflow.addBlock(TEST_BLOCK)
expect(workflow.workflow.definition)
})
it("updates a workflow block with new attributes", () => {
const firstBlock = workflow.workflow.definition.steps[0]
const updatedBlock = {
...firstBlock,
name: "UPDATED",
}
workflow.updateBlock(updatedBlock, firstBlock.id)
expect(workflow.workflow.definition.steps[0]).toEqual(updatedBlock)
})
it("deletes a workflow block successfully", () => {
const { steps } = workflow.workflow.definition
const originalLength = steps.length
const lastBlock = steps[steps.length - 1]
workflow.deleteBlock(lastBlock.id)
expect(workflow.workflow.definition.steps.length).toBeLessThan(
originalLength
)
})
})

View File

@ -1,48 +1,48 @@
<script>
import { afterUpdate } from "svelte"
import { workflowStore, backendUiStore } from "builderStore"
import { automationStore, backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import Flowchart from "./flowchart/FlowChart.svelte"
$: workflow = $workflowStore.selectedWorkflow?.workflow
$: workflowLive = workflow?.live
$: automation = $automationStore.selectedAutomation?.automation
$: automationLive = automation?.live
$: instanceId = $backendUiStore.selectedDatabase._id
function onSelect(block) {
workflowStore.update(state => {
automationStore.update(state => {
state.selectedBlock = block
return state
})
}
function setWorkflowLive(live) {
workflow.live = live
workflowStore.actions.save({ instanceId, workflow })
function setAutomationLive(live) {
automation.live = live
automationStore.actions.save({ instanceId, automation })
if (live) {
notifier.info(`Workflow ${workflow.name} enabled.`)
notifier.info(`Automation ${automation.name} enabled.`)
} else {
notifier.danger(`Workflow ${workflow.name} disabled.`)
notifier.danger(`Automation ${automation.name} disabled.`)
}
}
</script>
<section>
<Flowchart {workflow} {onSelect} />
<Flowchart {automation} {onSelect} />
</section>
<footer>
{#if workflow}
{#if automation}
<button
class:highlighted={workflowLive}
class:hoverable={workflowLive}
class:highlighted={automationLive}
class:hoverable={automationLive}
class="stop-button hoverable">
<i class="ri-stop-fill" on:click={() => setWorkflowLive(false)} />
<i class="ri-stop-fill" on:click={() => setAutomationLive(false)} />
</button>
<button
class:highlighted={!workflowLive}
class:hoverable={!workflowLive}
class:highlighted={!automationLive}
class:hoverable={!automationLive}
class="play-button hoverable"
data-cy="activate-workflow"
on:click={() => setWorkflowLive(true)}>
data-cy="activate-automation"
on:click={() => setAutomationLive(true)}>
<i class="ri-play-fill" />
</button>
{/if}

View File

@ -4,17 +4,17 @@
import { flip } from "svelte/animate"
import { fade, fly } from "svelte/transition"
export let workflow
export let automation
export let onSelect
let blocks
$: {
blocks = []
if (workflow) {
if (workflow.definition.trigger) {
blocks.push(workflow.definition.trigger)
if (automation) {
if (automation.definition.trigger) {
blocks.push(automation.definition.trigger)
}
blocks = blocks.concat(workflow.definition.steps || [])
blocks = blocks.concat(automation.definition.steps || [])
}
}
</script>

View File

@ -1,13 +1,13 @@
<script>
import { workflowStore } from "builderStore"
import WorkflowBlockTagline from "./WorkflowBlockTagline.svelte"
import { automationStore } from "builderStore"
import AutomationBlockTagline from "./AutomationBlockTagline.svelte"
export let onSelect
export let block
let selected
$: selected = $workflowStore.selectedBlock?.id === block.id
$: steps = $workflowStore.selectedWorkflow?.workflow?.definition?.steps ?? []
$: selected = $automationStore.selectedBlock?.id === block.id
$: steps = $automationStore.selectedAutomation?.automation?.definition?.steps ?? []
$: blockIdx = steps.findIndex(step => step.id === block.id)
</script>
@ -32,7 +32,7 @@
</header>
<hr />
<p>
<WorkflowBlockTagline {block} />
<AutomationBlockTagline {block} />
</p>
</div>

View File

@ -2,17 +2,17 @@
import Modal from "svelte-simple-modal"
import { notifier } from "builderStore/store/notifications"
import { onMount, getContext } from "svelte"
import { backendUiStore, workflowStore } from "builderStore"
import CreateWorkflowModal from "./CreateWorkflowModal.svelte"
import { backendUiStore, automationStore } from "builderStore"
import CreateAutomationModal from "./CreateAutomationModal.svelte"
import { Button } from "@budibase/bbui"
const { open, close } = getContext("simple-modal")
$: selectedWorkflowId = $workflowStore.selectedWorkflow?.workflow?._id
$: selectedAutomationId = $automationStore.selectedAutomation?.automation?._id
function newWorkflow() {
function newAutomation() {
open(
CreateWorkflowModal,
CreateAutomationModal,
{
onClosed: close,
},
@ -21,20 +21,20 @@
}
onMount(() => {
workflowStore.actions.fetch()
automationStore.actions.fetch()
})
</script>
<section>
<Button purple wide on:click={newWorkflow}>Create New Workflow</Button>
<Button purple wide on:click={newAutomation}>Create New Automation</Button>
<ul>
{#each $workflowStore.workflows as workflow}
{#each $automationStore.automations as automation}
<li
class="workflow-item"
class:selected={workflow._id === selectedWorkflowId}
on:click={() => workflowStore.actions.select(workflow)}>
<i class="ri-stackshare-line" class:live={workflow.live} />
{workflow.name}
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>
@ -68,7 +68,7 @@
font-size: 14px;
}
.workflow-item {
.automation-item {
display: flex;
border-radius: 5px;
padding-left: 12px;
@ -78,21 +78,21 @@
color: var(--ink);
}
.workflow-item i {
.automation-item i {
font-size: 24px;
margin-right: 10px;
}
.workflow-item:hover {
.automation-item:hover {
cursor: pointer;
background: var(--grey-1);
}
.workflow-item.selected {
.automation-item.selected {
background: var(--grey-2);
}
.new-workflow-button {
.new-automation-button {
cursor: pointer;
border: 1px solid var(--grey-4);
border-radius: 3px;
@ -108,7 +108,7 @@
transition: all 2ms;
}
.new-workflow-button:hover {
.new-automation-button:hover {
background: var(--grey-1);
}

View File

@ -1,5 +1,5 @@
<script>
import { store, backendUiStore, workflowStore } from "builderStore"
import { store, backendUiStore, automationStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import ActionButton from "components/common/ActionButton.svelte"
import { Input } from "@budibase/bbui"
@ -12,19 +12,19 @@
$: instanceId = $backendUiStore.selectedDatabase._id
$: appId = $store.appId
async function createWorkflow() {
await workflowStore.actions.create({
async function createAutomation() {
await automationStore.actions.create({
name,
instanceId,
})
onClosed()
notifier.success(`Workflow ${name} created.`)
notifier.success(`Automation ${name} created.`)
}
</script>
<header>
<i class="ri-stackshare-line" />
Create Workflow
Create Automation
</header>
<div>
<Input bind:value={name} label="Name" />
@ -32,10 +32,10 @@
<footer>
<a href="https://docs.budibase.com">
<i class="ri-information-line" />
Learn about workflows
Learn about automations
</a>
<ActionButton secondary on:click={onClosed}>Cancel</ActionButton>
<ActionButton disabled={!valid} on:click={createWorkflow}>Save</ActionButton>
<ActionButton disabled={!valid} on:click={createAutomation}>Save</ActionButton>
</footer>
<style>

View File

@ -1,22 +1,22 @@
<script>
import { workflowStore } from "builderStore"
import WorkflowList from "./WorkflowList/WorkflowList.svelte"
import { automationStore } from "builderStore"
import AutomationList from "./AutomationList/AutomationList.svelte"
import BlockList from "./BlockList/BlockList.svelte"
let selectedTab = "WORKFLOWS"
let selectedTab = "AUTOMATIONS"
</script>
<header>
<span
data-cy="workflow-list"
class="hoverable workflow-header"
class:selected={selectedTab === 'WORKFLOWS'}
on:click={() => (selectedTab = 'WORKFLOWS')}>
Workflows
data-cy="automation-list"
class="hoverable automation-header"
class:selected={selectedTab === 'AUTOMATIONS'}
on:click={() => (selectedTab = 'AUTOMATIONS')}>
Automations
</span>
{#if $workflowStore.selectedWorkflow}
{#if $automationStore.selectedAutomation}
<span
data-cy="add-workflow-component"
data-cy="add-automation-component"
class="hoverable"
class:selected={selectedTab === 'ADD'}
on:click={() => (selectedTab = 'ADD')}>
@ -24,8 +24,8 @@
</span>
{/if}
</header>
{#if selectedTab === 'WORKFLOWS'}
<WorkflowList />
{#if selectedTab === 'AUTOMATIONS'}
<AutomationList />
{:else if selectedTab === 'ADD'}
<BlockList />
{/if}
@ -40,7 +40,7 @@
margin-bottom: 20px;
}
.workflow-header {
.automation-header {
margin-right: 20px;
}

View File

@ -1,12 +1,12 @@
<script>
import { workflowStore } from "builderStore"
import { automationStore } from "builderStore"
export let blockDefinition
export let stepId
export let blockType
function addBlockToWorkflow() {
workflowStore.actions.addBlockToWorkflow({
function addBlockToAutomation() {
automationStore.actions.addBlockToAutomation({
...blockDefinition,
args: blockDefinition.args || {},
stepId,
@ -16,20 +16,20 @@
</script>
<div
class="workflow-block hoverable"
on:click={addBlockToWorkflow}
class="automation-block hoverable"
on:click={addBlockToAutomation}
data-cy={stepId}>
<div>
<i class={blockDefinition.icon} />
</div>
<div class="workflow-text">
<div class="automation-text">
<h4>{blockDefinition.name}</h4>
<p>{blockDefinition.description}</p>
</div>
</div>
<style>
.workflow-block {
.automation-block {
display: grid;
grid-template-columns: 20px auto;
align-items: center;
@ -38,11 +38,11 @@
border-radius: var(--border-radius-m);
}
.workflow-block:hover {
.automation-block:hover {
background-color: var(--grey-1);
}
.workflow-text {
.automation-text {
margin-left: 16px;
}

View File

@ -1,14 +1,14 @@
<script>
import { workflowStore } from "builderStore"
import WorkflowBlock from "./WorkflowBlock.svelte"
import { automationStore } from "builderStore"
import AutomationBlock from "./AutomationBlock.svelte"
import FlatButtonGroup from "components/userInterface/FlatButtonGroup.svelte"
let selectedTab = "TRIGGER"
let buttonProps = []
$: blocks = Object.entries($workflowStore.blockDefinitions[selectedTab])
$: blocks = Object.entries($automationStore.blockDefinitions[selectedTab])
$: {
if ($workflowStore.selectedWorkflow.hasTrigger()) {
if ($automationStore.selectedAutomation.hasTrigger()) {
buttonProps = [
{ value: "ACTION", text: "Action" },
{ value: "LOGIC", text: "Logic" },
@ -33,7 +33,7 @@
<FlatButtonGroup value={selectedTab} {buttonProps} onChange={onChangeTab} />
<div id="blocklist">
{#each blocks as [stepId, blockDefinition]}
<WorkflowBlock {blockDefinition} {stepId} blockType={selectedTab} />
<AutomationBlock {blockDefinition} {stepId} blockType={selectedTab} />
{/each}
</div>
</section>

View File

@ -0,0 +1,3 @@
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

@ -2,25 +2,25 @@
import ModelSelector from "./ParamInputs/ModelSelector.svelte"
import RecordSelector from "./ParamInputs/RecordSelector.svelte"
import { Input, TextArea, Select, Label } from "@budibase/bbui"
import { workflowStore } from "builderStore"
import { automationStore } from "builderStore"
import BindableInput from "../../userInterface/BindableInput.svelte"
export let block
$: inputs = Object.entries(block.schema?.inputs?.properties || {})
$: bindings = getAvailableBindings(
block,
$workflowStore.selectedWorkflow?.workflow?.definition
$automationStore.selectedAutomation?.automation?.definition
)
function getAvailableBindings(block, workflow) {
if (!block || !workflow) {
function getAvailableBindings(block, automation) {
if (!block || !automation) {
return []
}
// Find previous steps to the selected one
let allSteps = [...workflow.steps]
if (workflow.trigger) {
allSteps = [workflow.trigger, ...allSteps]
let allSteps = [...automation.steps]
if (automation.trigger) {
allSteps = [automation.trigger, ...allSteps]
}
const blockIdx = allSteps.findIndex(step => step.id === block.id)
@ -44,7 +44,7 @@
}
</script>
<div class="container" data-cy="workflow-block-setup">
<div class="container" data-cy="automation-block-setup">
<div class="block-label">{block.name}</div>
{#each inputs as [key, value]}
<div class="bb-margin-xl block-field">

View File

@ -1,5 +1,5 @@
<script>
import { store, backendUiStore, workflowStore } from "builderStore"
import { store, backendUiStore, automationStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import ActionButton from "components/common/ActionButton.svelte"
@ -10,32 +10,32 @@
$: valid = !!name
$: instanceId = $backendUiStore.selectedDatabase._id
async function deleteWorkflow() {
await workflowStore.actions.delete({
async function deleteAutomation() {
await automationStore.actions.delete({
instanceId,
workflow: $workflowStore.selectedWorkflow.workflow,
automation: $automationStore.selectedAutomation.automation,
})
onClosed()
notifier.danger("Workflow deleted.")
notifier.danger("Automation deleted.")
}
</script>
<header>
<i class="ri-stackshare-line" />
Delete Workflow
Delete Automation
</header>
<div>
<p>
Are you sure you want to delete this workflow? This action can't be undone.
Are you sure you want to delete this automation? This action can't be undone.
</p>
</div>
<footer>
<a href="https://docs.budibase.com">
<i class="ri-information-line" />
Learn about workflows
Learn about automations
</a>
<ActionButton on:click={onClosed}>Cancel</ActionButton>
<ActionButton alert on:click={deleteWorkflow}>Delete</ActionButton>
<ActionButton alert on:click={deleteAutomation}>Delete</ActionButton>
</footer>
<style>

View File

@ -38,7 +38,7 @@
<option value={option}>{option}</option>
{/each}
</Select>
{:else if schema.type === "string"}
{:else if schema.type === "string" || schema.type === "number"}
<BindableInput
thin
bind:value={value[field]}

View File

@ -1,49 +1,49 @@
<script>
import { getContext } from "svelte"
import { backendUiStore, workflowStore } from "builderStore"
import { backendUiStore, automationStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import WorkflowBlockSetup from "./WorkflowBlockSetup.svelte"
import DeleteWorkflowModal from "./DeleteWorkflowModal.svelte"
import AutomationBlockSetup from "./AutomationBlockSetup.svelte"
import DeleteAutomationModal from "./DeleteAutomationModal.svelte"
import { Button, Input, Label } from "@budibase/bbui"
const { open, close } = getContext("simple-modal")
let selectedTab = "SETUP"
$: workflow = $workflowStore.selectedWorkflow?.workflow
$: automation = $automationStore.selectedAutomation?.automation
$: allowDeleteBlock =
$workflowStore.selectedBlock?.type !== "TRIGGER" ||
!workflow?.definition?.steps?.length
$automationStore.selectedBlock?.type !== "TRIGGER" ||
!automation?.definition?.steps?.length
function deleteWorkflow() {
function deleteAutomation() {
open(
DeleteWorkflowModal,
DeleteAutomationModal,
{ onClosed: close },
{ styleContent: { padding: "0" } }
)
}
function deleteWorkflowBlock() {
workflowStore.actions.deleteWorkflowBlock($workflowStore.selectedBlock)
function deleteAutomationBlock() {
automationStore.actions.deleteAutomationBlock($automationStore.selectedBlock)
}
async function testWorkflow() {
const result = await workflowStore.actions.trigger({
workflow: $workflowStore.selectedWorkflow.workflow,
async function testAutomation() {
const result = await automationStore.actions.trigger({
automation: $automationStore.selectedAutomation.automation,
})
if (result.status === 200) {
notifier.success(`Workflow ${workflow.name} triggered successfully.`)
notifier.success(`Automation ${automation.name} triggered successfully.`)
} else {
notifier.danger(`Failed to trigger workflow ${workflow.name}.`)
notifier.danger(`Failed to trigger automation ${automation.name}.`)
}
}
async function saveWorkflow() {
await workflowStore.actions.save({
async function saveAutomation() {
await automationStore.actions.save({
instanceId: $backendUiStore.selectedDatabase._id,
workflow,
automation,
})
notifier.success(`Workflow ${workflow.name} saved.`)
notifier.success(`Automation ${automation.name} saved.`)
}
</script>
@ -56,38 +56,38 @@
Setup
</span>
</header>
{#if $workflowStore.selectedBlock}
<WorkflowBlockSetup bind:block={$workflowStore.selectedBlock} />
{#if $automationStore.selectedBlock}
<AutomationBlockSetup bind:block={$automationStore.selectedBlock} />
<div class="buttons">
<Button green wide data-cy="save-workflow-setup" on:click={saveWorkflow}>
Save Workflow
<Button green wide data-cy="save-automation-setup" on:click={saveAutomation}>
Save Automation
</Button>
<Button
disabled={!allowDeleteBlock}
red
wide
on:click={deleteWorkflowBlock}>
on:click={deleteAutomationBlock}>
Delete Block
</Button>
</div>
{:else if $workflowStore.selectedWorkflow}
{:else if $automationStore.selectedAutomation}
<div class="panel">
<div class="panel-body">
<div class="block-label">
Workflow
<b>{workflow.name}</b>
Automation
<b>{automation.name}</b>
</div>
</div>
<Button secondary wide on:click={testWorkflow}>Test Workflow</Button>
<Button secondary wide on:click={testAutomation}>Test Automation</Button>
<div class="buttons">
<Button
green
wide
data-cy="save-workflow-setup"
on:click={saveWorkflow}>
Save Workflow
data-cy="save-automation-setup"
on:click={saveAutomation}>
Save Automation
</Button>
<Button red wide on:click={deleteWorkflow}>Delete Workflow</Button>
<Button red wide on:click={deleteAutomation}>Delete Automation</Button>
</div>
</div>
{/if}

View File

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

View File

@ -0,0 +1,299 @@
<script>
import { notifier } from "builderStore/store/notifications"
import { Heading, Body, Button } from "@budibase/bbui"
import { FILE_TYPES } from "constants/backend"
import api from "builderStore/api"
const BYTES_IN_KB = 1000
const BYTES_IN_MB = 1000000
export let files = []
export let fileSizeLimit = BYTES_IN_MB * 20
let selectedImageIdx = 0
let fileDragged = false
$: selectedImage = files[selectedImageIdx]
function determineFileIcon(extension) {
const ext = extension.toLowerCase()
if (FILE_TYPES.IMAGE.includes(ext)) return "ri-image-2-line"
if (FILE_TYPES.CODE.includes(ext)) return "ri-terminal-box-line"
return "ri-file-line"
}
async function processFiles(fileList) {
const fileArray = Array.from(fileList)
if (fileArray.some(file => file.size >= fileSizeLimit)) {
notifier.danger(
`Files cannot exceed ${fileSizeLimit /
BYTES_IN_MB}MB. Please try again with smaller files.`
)
return
}
const filesToProcess = fileArray.map(({ name, path, size }) => ({
name,
path,
size,
}))
const response = await api.post(`/api/attachments/process`, {
files: filesToProcess,
})
const processedFiles = await response.json()
files = [...processedFiles, ...files]
selectedImageIdx = 0
}
async function removeFile() {
files.splice(selectedImageIdx, 1)
files = files
selectedImageIdx = 0
}
function navigateLeft() {
selectedImageIdx -= 1
}
function navigateRight() {
selectedImageIdx += 1
}
function handleFile(evt) {
processFiles(evt.target.files)
}
function handleDragOver(evt) {
evt.preventDefault()
fileDragged = true
}
function handleDragLeave(evt) {
evt.preventDefault()
fileDragged = false
}
function handleDrop(evt) {
evt.preventDefault()
processFiles(evt.dataTransfer.files)
fileDragged = false
}
</script>
<div
class="dropzone"
on:dragover={handleDragOver}
on:dragleave={handleDragLeave}
on:dragenter={handleDragOver}
on:drop={handleDrop}
class:fileDragged>
<ul>
{#if selectedImage}
<li>
<header>
<div>
<i
class={`file-icon ${determineFileIcon(selectedImage.extension)}`} />
<span class="filename">{selectedImage.name}</span>
</div>
<p>
{#if selectedImage.size <= BYTES_IN_MB}
{selectedImage.size / BYTES_IN_KB}KB
{:else}{selectedImage.size / BYTES_IN_MB}MB{/if}
</p>
</header>
<div class="delete-button" on:click={removeFile}>
<i class="ri-close-line" />
</div>
{#if selectedImageIdx !== 0}
<div class="nav left" on:click={navigateLeft}>
<i class="ri-arrow-left-line" />
</div>
{/if}
<img src={selectedImage.url} />
{#if selectedImageIdx !== files.length - 1}
<div class="nav right" on:click={navigateRight}>
<i class="ri-arrow-right-line" />
</div>
{/if}
</li>
{/if}
</ul>
<i class="ri-folder-upload-line" />
<input id="file-upload" type="file" multiple on:change={handleFile} />
<label for="file-upload">Upload</label>
</div>
<style>
.dropzone {
padding: var(--spacing-l);
border: 2px dashed var(--grey-7);
text-align: center;
display: flex;
align-items: center;
flex-direction: column;
border-radius: 10px;
transition: all 0.3s;
}
.fileDragged {
border: 2px dashed var(--grey-7);
transform: scale(1.03);
background: var(--blue-light);
}
input[type="file"] {
display: none;
}
label {
font-family: var(--font-sans);
cursor: pointer;
font-weight: 600;
box-sizing: border-box;
overflow: hidden;
border-radius: var(--border-radius-s);
color: var(--white);
padding: var(--spacing-s) var(--spacing-l);
transition: all 0.2s ease 0s;
display: inline-flex;
text-rendering: optimizeLegibility;
min-width: auto;
outline: none;
font-feature-settings: "case" 1, "rlig" 1, "calt" 0;
-webkit-box-align: center;
user-select: none;
flex-shrink: 0;
align-items: center;
justify-content: center;
margin-top: 10px;
width: 100%;
border: solid 1.5px var(--ink);
background-color: var(--ink);
}
div.nav {
position: absolute;
background: black;
color: var(--white);
display: flex;
align-items: center;
bottom: var(--spacing-s);
border-radius: 10px;
transition: 0.2s transform;
}
.nav:hover {
cursor: pointer;
transform: scale(1.1);
}
.left {
left: var(--spacing-s);
}
.right {
right: var(--spacing-s);
}
li {
position: relative;
height: 300px;
background: var(--grey-7);
display: flex;
justify-content: center;
border-radius: 10px;
}
img {
border-radius: 10px;
width: 100%;
box-shadow: 0 var(--spacing-s) 12px rgba(0, 0, 0, 0.15);
object-fit: contain;
}
i {
font-size: 3em;
}
.file-icon {
color: var(--white);
font-size: 2em;
margin-right: var(--spacing-s);
}
ul {
padding: 0;
display: grid;
grid-gap: var(--spacing-s);
list-style-type: none;
width: 100%;
}
header {
display: flex;
align-items: center;
justify-content: space-between;
position: absolute;
background: linear-gradient(
180deg,
rgba(12, 12, 12, 1),
rgba(60, 60, 60, 0)
);
width: 100%;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
height: 60px;
}
header > div {
color: var(--white);
display: flex;
align-items: center;
font-size: 15px;
margin-left: var(--spacing-m);
width: 60%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.filename {
overflow: hidden;
text-overflow: ellipsis;
}
header > p {
color: var(--grey-5);
margin-right: var(--spacing-m);
}
.delete-button {
position: absolute;
top: var(--spacing-s);
right: var(--spacing-s);
padding: var(--spacing-s);
border-radius: 10px;
opacity: 0;
transition: all 0.3s;
color: var(--white);
}
.delete-button i {
font-size: 2em;
}
.delete-button:hover {
opacity: 1;
cursor: pointer;
background: linear-gradient(
to top right,
rgba(60, 60, 60, 0),
rgba(255, 0, 0, 0.2)
);
}
</style>

View File

@ -1,5 +1,7 @@
<script>
import { Circle } from "svelte-loading-spinners"
export let size = "60"
</script>
<Circle size="60" color="#000000" unit="px" />
<Circle {size} color="#000000" unit="px" />

View File

@ -0,0 +1,64 @@
<script>
import { FILE_TYPES } from "constants/backend"
export let files
export let height = "70"
export let width = "70"
</script>
<div class="file-list">
{#each files as file}
<div class="file">
{#if FILE_TYPES.IMAGE.includes(file.extension.toLowerCase())}
<img {width} {height} src={file.url} />
{:else}
<i class="ri-file-line" />
<span class="extension">.{file.extension}</span>
<span>{file.name}</span>
{/if}
</div>
{/each}
</div>
<style>
.file-list {
display: grid;
grid-auto-flow: column;
grid-gap: var(--spacing-m);
grid-template-columns: repeat(10, 1fr);
}
img {
object-fit: contain;
}
i {
font-size: 36px;
margin-bottom: var(--spacing-m);
}
.file {
position: relative;
height: 75px;
width: 75px;
border: 2px dashed var(--grey-7);
padding: var(--spacing-m);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.extension {
position: absolute;
top: var(--spacing-s);
left: var(--spacing-s);
font-weight: 500;
}
span {
width: 75px;
overflow: hidden;
text-overflow: ellipsis;
}
</style>

View File

@ -6,6 +6,7 @@
import { Button, Icon } from "@budibase/bbui"
import ActionButton from "components/common/ActionButton.svelte"
import LinkedRecord from "./LinkedRecord.svelte"
import AttachmentList from "./AttachmentList.svelte"
import TablePagination from "./TablePagination.svelte"
import { DeleteRecordModal, CreateEditRecordModal } from "./modals"
import RowPopover from "./popovers/Row.svelte"
@ -90,6 +91,8 @@
<td>
{#if schema[header].type === 'link'}
<LinkedRecord field={schema[header]} ids={row[header]} />
{:else if schema[header].type === 'attachment'}
<AttachmentList files={row[header] || []} />
{:else}{getOr('', header, row)}{/if}
</td>
{/each}
@ -108,6 +111,7 @@
section {
margin-bottom: 20px;
}
.title {
font-size: 24px;
font-weight: 600;

View File

@ -6,7 +6,7 @@
import api from "builderStore/api"
import { Button, Icon } from "@budibase/bbui"
import ActionButton from "components/common/ActionButton.svelte"
import LinkedRecord from "./LinkedRecord.svelte"
import AttachmentList from "./AttachmentList.svelte"
import TablePagination from "./TablePagination.svelte"
import { DeleteRecordModal, CreateEditRecordModal } from "./modals"
import RowPopover from "./popovers/Row.svelte"
@ -59,7 +59,11 @@
{#each paginatedData as row}
<tr>
{#each columns as header}
<td>{getOr('', header, row)}</td>
<td>
{#if schema[header].type === 'attachment'}
<AttachmentList files={row[header] || []} />
{:else}{getOr('', header, row)}{/if}
</td>
{/each}
</tr>
{/each}

View File

@ -1,5 +1,5 @@
<script>
import { onMount } from "svelte"
import { onMount, tick } from "svelte"
import { store, backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import { compose, map, get, flatten } from "lodash/fp"
@ -34,12 +34,9 @@
return
}
backendUiStore.update(state => {
state.selectedView = state.selectedView
onClosed()
notifier.success("Record created successfully.")
return state
})
onClosed()
notifier.success("Record saved successfully.")
backendUiStore.actions.records.save(recordResponse)
}
</script>

View File

@ -1,9 +1,10 @@
<script>
import { Input, Select } from "@budibase/bbui"
import DatePicker from "components/common/DatePicker.svelte"
import { Input, Select, Label, DatePicker } from "@budibase/bbui"
import Dropzone from "components/common/Dropzone.svelte"
export let meta
export let value = meta.type === "boolean" ? false : ""
export let originalValue
let isSelect =
meta.type === "string" &&
@ -17,6 +18,7 @@
if (meta.type === "datetime") return "date"
if (meta.type === "number") return "number"
if (meta.type === "boolean") return "checkbox"
if (meta.type === "attachment") return "file"
if (isSelect) return "select"
return "text"
@ -45,7 +47,11 @@
{/each}
</Select>
{:else if type === 'date'}
<DatePicker label={meta.name} bind:value />
<Label small forAttr={'datepicker-label'}>{meta.name}</Label>
<DatePicker bind:value />
{:else if type === 'file'}
<Label small forAttr={'dropzone-label'}>{meta.name}</Label>
<Dropzone bind:files={value} />
{:else}
{#if type === 'checkbox'}
<label>{meta.name}</label>
@ -64,7 +70,6 @@
label {
font-weight: 500;
font-size: var(--font-size-s);
float: left;
margin-right: 8px;
margin-bottom: 12px;
}
</style>

View File

@ -1,7 +1,14 @@
<script>
import { getContext } from "svelte"
import { backendUiStore } from "builderStore"
import { DropdownMenu, Button, Icon, Input, Select } from "@budibase/bbui"
import {
DropdownMenu,
Button,
Icon,
Input,
Select,
Heading,
} from "@budibase/bbui"
import { FIELDS } from "constants/backend"
import CreateEditRecord from "../modals/CreateEditRecord.svelte"
import DeleteRecordModal from "../modals/DeleteRecord.svelte"
@ -48,11 +55,11 @@
<ul>
<li data-cy="edit-row" on:click={showEditor}>
<Icon name="edit" />
Edit
<span>Edit</span>
</li>
<li data-cy="delete-row" on:click={deleteRow}>
<Icon name="delete" />
Delete
<span>Delete</span>
</li>
</ul>
{/if}
@ -79,7 +86,6 @@
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 0px;

View File

@ -1,7 +1,7 @@
<script>
import { writable } from "svelte/store"
import { store, workflowStore, backendUiStore } from "builderStore"
import { store, automationStore, backendUiStore } from "builderStore"
import { string, object } from "yup"
import api, { get } from "builderStore/api"
import Form from "@svelteschool/svelte-forms"
@ -133,7 +133,7 @@
if (applicationPkg.ok) {
backendUiStore.actions.reset()
await store.setPackage(pkg)
workflowStore.actions.fetch()
automationStore.actions.fetch()
} else {
throw new Error(pkg)
}

View File

@ -35,8 +35,6 @@
c => c._component === componentInstance._component
) || {}
let panelDefinition = {}
$: panelDefinition =
componentPropDefinition.properties &&
componentPropDefinition.properties[selectedCategory.value]

View File

@ -3,7 +3,7 @@
import { find, map, keys, reduce, keyBy } from "lodash/fp"
import { pipe } from "components/common/core"
import { EVENT_TYPE_MEMBER_NAME } from "components/common/eventHandlers"
import { store, workflowStore } from "builderStore"
import { store, automationStore } from "builderStore"
import { ArrowDownIcon } from "components/common/Icons/"
import { createEventDispatcher } from "svelte"
@ -18,14 +18,14 @@
</script>
<div class="handler-option">
{#if parameter.name === 'workflow'}
{#if parameter.name === 'automation'}
<span>{parameter.name}</span>
{/if}
{#if parameter.name === 'workflow'}
{#if parameter.name === 'automation'}
<Select on:change bind:value={parameter.value}>
<option value="" />
{#each $workflowStore.workflows.filter(wf => wf.live) as workflow}
<option value={workflow._id}>{workflow.name}</option>
{#each $automationStore.automations.filter(wf => wf.live) as automation}
<option value={automation._id}>{automation.name}</option>
{/each}
</Select>
{:else if parameter.name === 'url'}

View File

@ -0,0 +1,293 @@
<script>
import { DropdownMenu, Button, Input } from "@budibase/bbui"
import { createEventDispatcher, tick } from "svelte"
import icons from "./icons.js"
const dispatch = createEventDispatcher()
export let value = ""
export let maxIconsPerPage = 30
let searchTerm = ""
let selectedLetter = "A"
let currentPage = 1
let filteredIcons = findIconByTerm(selectedLetter)
$: dispatch("change", value)
const alphabet = [
"A",
"B",
"C",
"D",
"E",
"F",
"G",
"H",
"I",
"J",
"K",
"L",
"M",
"N",
"O",
"P",
"Q",
"R",
"S",
"T",
"U",
"V",
"W",
"X",
"Y",
"Z",
]
let buttonAnchor, dropdown
let loading = false
function findIconByTerm(term) {
const r = new RegExp(`\^${term}`, "i")
return icons.filter(i => r.test(i.label))
}
async function switchLetter(letter) {
currentPage = 1
searchTerm = ""
loading = true
selectedLetter = letter
filteredIcons = findIconByTerm(letter)
await tick() //svg icons do not update without tick
loading = false
}
async function findIconOnPage() {
loading = true
const iconIdx = filteredIcons.findIndex(i => i.value === value)
if (iconIdx !== -1) {
currentPage = Math.ceil(iconIdx / maxIconsPerPage)
}
await tick() //svg icons do not update without tick
loading = false
}
async function setSelectedUI() {
if (value) {
const letter = displayValue.substring(0, 1)
await switchLetter(letter)
await findIconOnPage()
}
}
async function pageClick(next) {
loading = true
if (next && currentPage < totalPages) {
currentPage++
} else if (!next && currentPage > 1) {
currentPage--
}
await tick() //svg icons do not update without tick
loading = false
}
async function searchForIcon(e) {
currentPage = 1
loading = true
filteredIcons = findIconByTerm(searchTerm)
await tick() //svg icons do not update without tick
loading = false
}
$: displayValue = value ? value.substring(7) : "Pick Icon"
$: totalPages = Math.ceil(filteredIcons.length / maxIconsPerPage)
$: pageEndIdx = maxIconsPerPage * currentPage
$: pagedIcons = filteredIcons.slice(pageEndIdx - maxIconsPerPage, pageEndIdx)
$: pagerText = `Page ${currentPage} of ${totalPages}`
</script>
<div bind:this={buttonAnchor}>
<Button secondary on:click={dropdown.show}>{displayValue}</Button>
</div>
<DropdownMenu
bind:this={dropdown}
on:open={setSelectedUI}
anchor={buttonAnchor}>
<div class="container">
<div class="search-area">
<div class="alphabet-area">
{#each alphabet as letter, idx}
<span
class="letter"
class:letter-selected={letter === selectedLetter}
on:click={() => switchLetter(letter)}>
{letter}
</span>
{#if idx !== alphabet.length - 1}
<span>-</span>
{/if}
{/each}
</div>
<div class="search-input">
<div class="input-wrapper">
<Input bind:value={searchTerm} thin placeholder="Search Icon" />
</div>
<Button secondary on:click={searchForIcon}>Search</Button>
</div>
<div class="page-area">
<div class="pager">
<span on:click={() => pageClick(false)}>
<i class="page-btn fas fa-chevron-left" />
</span>
<span>{pagerText}</span>
<span on:click={() => pageClick(true)}>
<i class="page-btn fas fa-chevron-right" />
</span>
</div>
</div>
</div>
{#if pagedIcons.length > 0}
<div class="icon-area">
{#if !loading}
{#each pagedIcons as icon}
<div
class="icon-container"
class:selected={value === icon.value}
on:click={() => (value = icon.value)}>
<div class="icon-preview">
<i class={`${icon.value} fa-3x`} />
</div>
<div class="icon-label">{icon.label}</div>
</div>
{/each}
{/if}
</div>
{:else}
<div class="no-icons">
<h5>
{`There is no icons for this ${searchTerm ? 'search' : 'page'}`}
</h5>
</div>
{/if}
</div>
</DropdownMenu>
<style>
.container {
width: 610px;
height: 350px;
display: flex;
flex-direction: column;
padding: 10px 0px 10px 15px;
overflow-x: hidden;
}
.search-area {
flex: 0 0 80px;
display: flex;
flex-direction: column;
}
.icon-area {
flex: 1;
display: grid;
grid-template-columns: repeat(5, 1fr);
grid-gap: 5px;
justify-content: flex-start;
overflow-y: auto;
overflow-x: hidden;
padding-right: 10px;
}
.no-icons {
display: flex;
justify-content: center;
align-items: center;
}
.alphabet-area {
display: flex;
flex-flow: row wrap;
padding-bottom: 10px;
padding-right: 15px;
justify-content: space-around;
}
.loading-container {
display: flex;
justify-content: center;
align-items: center;
}
.search-input {
display: flex;
flex-flow: row nowrap;
width: 100%;
padding-right: 15px;
}
.input-wrapper {
width: 510px;
margin-right: 5px;
}
.page-area {
padding: 10px;
display: flex;
justify-content: center;
}
.letter {
color: var(--blue);
}
.letter:hover {
cursor: pointer;
text-decoration: underline;
}
.letter-selected {
text-decoration: underline;
}
.icon-container {
height: 100px;
display: flex;
justify-content: center;
flex-direction: column;
border: var(--border-dark);
}
.icon-container:hover {
cursor: pointer;
background: var(--grey-2);
}
.selected {
background: var(--grey-3);
}
.icon-preview {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
}
.icon-label {
flex: 0 0 20px;
text-align: center;
font-size: 12px;
}
.page-btn {
color: var(--blue);
}
.page-btn:hover {
cursor: pointer;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,3 @@
import "@fortawesome/fontawesome-free/js/all.js"
export { default as IconSelect } from "./IconSelect.svelte"

View File

@ -71,7 +71,7 @@
let temp = runtimeToReadableBinding(bindableProperties, value)
return value === undefined && props.defaultValue !== undefined
return !value && props.defaultValue !== undefined
? props.defaultValue
: temp
}

View File

@ -6,6 +6,7 @@ import ModelViewSelect from "components/userInterface/ModelViewSelect.svelte"
import ModelViewFieldSelect from "components/userInterface/ModelViewFieldSelect.svelte"
import Event from "components/userInterface/EventsEditor/EventPropertyControl.svelte"
import ScreenSelect from "components/userInterface/ScreenSelect.svelte"
import { IconSelect } from "components/userInterface/IconSelect"
import Colorpicker from "@budibase/colorpicker"
import { all } from "./propertyCategories.js"
@ -221,16 +222,41 @@ export default {
settings: [{ label: "URL", key: "url", control: Input }],
},
},
// {
// _component: "@budibase/standard-components/icon",
// name: "Icon",
// description: "A basic component for displaying icons",
// icon: "ri-sun-fill",
// children: [],
// properties: {
// design: { ...all },
// },
// },
{
_component: "@budibase/standard-components/icon",
name: "Icon",
description: "A basic component for displaying icons",
icon: "ri-sun-fill",
children: [],
properties: {
design: {},
settings: [
{ label: "Icon", key: "icon", control: IconSelect },
{
label: "Size",
key: "size",
control: OptionSelect,
defaultValue: "fa-lg",
options: [
{ value: "fa-xs", label: "xs" },
{ value: "fa-sm", label: "sm" },
{ value: "fa-lg", label: "lg" },
{ value: "fa-2x", label: "2x" },
{ value: "fa-3x", label: "3x" },
{ value: "fa-5x", label: "5x" },
{ value: "fa-7x", label: "7x" },
{ value: "fa-10x", label: "10x" },
],
},
{
label: "Color",
key: "color",
control: Colorpicker,
defaultValue: "#000",
},
],
},
},
{
_component: "@budibase/standard-components/link",
name: "Link",

View File

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

View File

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

View File

@ -51,24 +51,15 @@ export const FIELDS = {
},
},
},
// IMAGE: {
// name: "File",
// icon: "ri-image-line",
// type: "file",
// constraints: {
// type: "string",
// presence: { allowEmpty: true },
// },
// },
// FILE: {
// name: "Image",
// icon: "ri-file-line",
// type: "file",
// constraints: {
// type: "string",
// presence: { allowEmpty: true },
// },
// },
ATTACHMENT: {
name: "Attachment",
icon: "ri-file-line",
type: "attachment",
constraints: {
type: "array",
presence: { allowEmpty: true },
},
},
// LINKED_FIELDS: {
// name: "Linked Fields",
// icon: "ri-link",
@ -79,3 +70,9 @@ export const FIELDS = {
// },
// },
}
export const FILE_TYPES = {
IMAGE: ["png", "tiff", "gif", "raw", "jpg", "jpeg"],
CODE: ["js", "rs", "py", "java", "rb", "hs", "yml"],
DOCUMENT: ["odf", "docx", "doc", "pdf", "csv"],
}

View File

@ -1,6 +1,6 @@
<script>
import Modal from "svelte-simple-modal"
import { store, workflowStore, backendUiStore } from "builderStore"
import { store, automationStore, backendUiStore } from "builderStore"
import SettingsLink from "components/settings/Link.svelte"
import { get } from "builderStore/api"
@ -21,17 +21,17 @@
if (res.ok) {
backendUiStore.actions.reset()
await store.setPackage(pkg)
workflowStore.actions.fetch()
await automationStore.actions.fetch()
return pkg
} else {
throw new Error(pkg)
}
}
// handles navigation between frontend, backend, workflow.
// handles navigation between frontend, backend, automation.
// this remembers your last place on each of the sections
// e.g. if one of your screens is selected on front end, then
// you browse to backend, when you click fronend, you will be
// you browse to backend, when you click frontend, you will be
// brought back to the same screen
const topItemNavigate = path => () => {
const activeTopNav = $layout.children.find(c => $isActive(c.path))

View File

@ -1,18 +1,19 @@
<!-- routify:options index=3 -->
<script>
import { workflowStore } from "builderStore"
import { WorkflowPanel, SetupPanel } from "components/workflow"
import { automationStore } from "builderStore"
import { AutomationPanel, SetupPanel } from "components/automation"
</script>
<div class="root">
<div class="nav">
<div class="inner">
<WorkflowPanel />
<AutomationPanel />
</div>
</div>
<div class="content">
<slot />
</div>
{#if $workflowStore.selectedWorkflow}
{#if $automationStore.selectedAutomation}
<div class="nav">
<div class="inner">
<SetupPanel />

View File

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

View File

@ -1,3 +1,4 @@
<!-- routify:options index=1 -->
<script>
import { getContext } from "svelte"
import { store, backendUiStore } from "builderStore"

View File

@ -48,12 +48,14 @@
<Button secondary medium on:click={deployApp}>
Deploy App
{#if loading}
<Spinner ratio={'0.5'} />
<Spinner size="10" />
{/if}
</Button>
{/if}
</div>
<img src="/_builder/assets/deploy-rocket.jpg" />
<img
src="/_builder/assets/deploy-rocket.jpg"
alt="Rocket flying through sky" />
</section>
<style>

View File

@ -1,3 +1,4 @@
<!-- routify:options index=1 -->
<script>
import { store, backendUiStore } from "builderStore"
import { goto } from "@sveltech/routify"

View File

@ -1,5 +0,0 @@
<script>
import { WorkflowBuilder } from "components/workflow"
</script>
<WorkflowBuilder />

View File

@ -1,32 +0,0 @@
import { buildStateOrigins } from "../src/builderStore/buildStateOrigins"
it("builds the correct stateOrigins object from a screen definition with handlers", () => {
expect(
buildStateOrigins({
name: "screen1",
description: "",
props: {
_component: "@budibase/standard-components/container",
className: "",
type: "div",
onClick: [
{
"##eventHandlerType": "Set State",
parameters: {
path: "testKey",
value: "value",
},
},
],
},
})
).toEqual({
testKey: {
"##eventHandlerType": "Set State",
parameters: {
path: "testKey",
value: "value",
},
},
})
})

View File

@ -709,14 +709,23 @@
lodash "^4.17.13"
to-fast-properties "^2.0.0"
"@budibase/bbui@^1.33.0":
version "1.33.0"
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.33.0.tgz#216b24dd815f45880e9795e66b04848329b0390f"
integrity sha512-Rrt5eLbea014TIfAbT40kP0D0AWNUi8Q0kDr3UZO6Aq4UXgjc0f53ZuJ7Kb66YRDWrqiucjf1FtvOUs3/YaD6g==
"@budibase/bbui@^1.34.2":
version "1.34.2"
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.34.2.tgz#e4fcc728dc8d51a918f8ebd5c3f0b0afacfa4047"
integrity sha512-6RusGPZnEpHx1gtGcjk/lFLgMgFdDpSIxB8v2MiA+kp+uP1pFlzegbaDh+/JXyqFwK7HO91I0yXXBoPjibi7Aw==
dependencies:
sirv-cli "^0.4.6"
svelte-flatpickr "^2.4.0"
"@budibase/client@^0.1.21":
version "0.1.21"
resolved "https://registry.yarnpkg.com/@budibase/client/-/client-0.1.21.tgz#db414445c132b373f6c25e39d62628eb60cd8ac3"
integrity sha512-/ju0vYbWh9MUjmxkGNlOL4S/VQd4p5mbz5rHu0yt55ak9t/yyzI6PzBBxlucBeRbXYd9OFynFjy1pvYt1v+z9Q==
dependencies:
deep-equal "^2.0.1"
mustache "^4.0.1"
regexparam "^1.3.0"
"@budibase/colorpicker@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@budibase/colorpicker/-/colorpicker-1.0.1.tgz#940c180e7ebba0cb0756c4c8ef13f5dfab58e810"
@ -769,6 +778,11 @@
debug "^3.1.0"
lodash.once "^4.1.1"
"@fortawesome/fontawesome-free@^5.14.0":
version "5.14.0"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.14.0.tgz#a371e91029ebf265015e64f81bfbf7d228c9681f"
integrity sha512-OfdMsF+ZQgdKHP9jUbmDcRrP0eX90XXrsXIdyjLbkmSBzmMXPABB8eobUJtivaupucYaByz6WNe1PI1JuYm3qA==
"@hapi/address@^2.1.2":
version "2.1.4"
resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5"
@ -1388,6 +1402,11 @@ array-equal@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93"
array-filter@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-1.0.0.tgz#baf79e62e6ef4c2a4c0b831232daffec251f9d83"
integrity sha1-uveeYubvTCpMC4MSMtr/7CUfnYM=
array-union@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
@ -1442,6 +1461,13 @@ atob@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
available-typed-arrays@^1.0.0, available-typed-arrays@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz#6b098ca9d8039079ee3f77f7b783c4480ba513f5"
integrity sha512-XWX3OX8Onv97LMk/ftVyBibpGwY5a8SmuxZPzeOxqmuEqUCOM9ZE+uIaD1VNJ5QnvU2UQusvmKbuM1FR8QWGfQ==
dependencies:
array-filter "^1.0.0"
aws-sign2@~0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
@ -2391,6 +2417,26 @@ decode-uri-component@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
deep-equal@^2.0.1:
version "2.0.3"
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.0.3.tgz#cad1c15277ad78a5c01c49c2dee0f54de8a6a7b0"
integrity sha512-Spqdl4H+ky45I9ByyJtXteOm9CaIrPmnIPmOhrkKGNYWeDgCvJ8jNYVCTjChxW4FqGuZnLHADc8EKRMX6+CgvA==
dependencies:
es-abstract "^1.17.5"
es-get-iterator "^1.1.0"
is-arguments "^1.0.4"
is-date-object "^1.0.2"
is-regex "^1.0.5"
isarray "^2.0.5"
object-is "^1.1.2"
object-keys "^1.1.1"
object.assign "^4.1.0"
regexp.prototype.flags "^1.3.0"
side-channel "^1.0.2"
which-boxed-primitive "^1.0.1"
which-collection "^1.0.1"
which-typed-array "^1.1.2"
deep-is@~0.1.3:
version "0.1.3"
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
@ -2560,6 +2606,54 @@ es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.5:
string.prototype.trimleft "^2.1.1"
string.prototype.trimright "^2.1.1"
es-abstract@^1.17.4:
version "1.17.6"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.6.tgz#9142071707857b2cacc7b89ecb670316c3e2d52a"
integrity sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==
dependencies:
es-to-primitive "^1.2.1"
function-bind "^1.1.1"
has "^1.0.3"
has-symbols "^1.0.1"
is-callable "^1.2.0"
is-regex "^1.1.0"
object-inspect "^1.7.0"
object-keys "^1.1.1"
object.assign "^4.1.0"
string.prototype.trimend "^1.0.1"
string.prototype.trimstart "^1.0.1"
es-abstract@^1.18.0-next.0:
version "1.18.0-next.0"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.0.tgz#b302834927e624d8e5837ed48224291f2c66e6fc"
integrity sha512-elZXTZXKn51hUBdJjSZGYRujuzilgXo8vSPQzjGYXLvSlGiCo8VO8ZGV3kjo9a0WNJJ57hENagwbtlRuHuzkcQ==
dependencies:
es-to-primitive "^1.2.1"
function-bind "^1.1.1"
has "^1.0.3"
has-symbols "^1.0.1"
is-callable "^1.2.0"
is-negative-zero "^2.0.0"
is-regex "^1.1.1"
object-inspect "^1.8.0"
object-keys "^1.1.1"
object.assign "^4.1.0"
string.prototype.trimend "^1.0.1"
string.prototype.trimstart "^1.0.1"
es-get-iterator@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.0.tgz#bb98ad9d6d63b31aacdc8f89d5d0ee57bcb5b4c8"
integrity sha512-UfrmHuWQlNMTs35e1ypnvikg6jCz3SK8v8ImvmDsh36fCVUR1MqoFDiyn0/k52C8NqO3YsO8Oe0azeesNuqSsQ==
dependencies:
es-abstract "^1.17.4"
has-symbols "^1.0.1"
is-arguments "^1.0.4"
is-map "^2.0.1"
is-set "^2.0.1"
is-string "^1.0.5"
isarray "^2.0.5"
es-to-primitive@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a"
@ -2866,7 +2960,7 @@ for-in@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
foreach@~2.0.1:
foreach@^2.0.5, foreach@~2.0.1:
version "2.0.5"
resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99"
integrity sha1-C+4AUBiusmDQo6865ljdATbsG5k=
@ -3240,16 +3334,31 @@ is-accessor-descriptor@^1.0.0:
dependencies:
kind-of "^6.0.0"
is-arguments@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.0.4.tgz#3faf966c7cba0ff437fb31f6250082fcf0448cf3"
integrity sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==
is-arrayish@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
is-bigint@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.0.tgz#73da8c33208d00f130e9b5e15d23eac9215601c4"
integrity sha512-t5mGUXC/xRheCK431ylNiSkGGpBp8bHENBcENTkDT6ppwPzEVxNGZRvgvmOEfbWkFhA7D2GEuE2mmQTr78sl2g==
is-binary-path@~2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
dependencies:
binary-extensions "^2.0.0"
is-boolean-object@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.0.1.tgz#10edc0900dd127697a92f6f9807c7617d68ac48e"
integrity sha512-TqZuVwa/sppcrhUCAYkGBk7w0yxfQQnxq28fjkO53tnK9FQXmdwz2JS5+GjsWQ6RByES1K40nI+yDic5c9/aAQ==
is-buffer@^1.1.5:
version "1.1.6"
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
@ -3258,6 +3367,11 @@ is-callable@^1.1.4, is-callable@^1.1.5:
version "1.1.5"
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.5.tgz#f7e46b596890456db74e7f6e976cb3273d06faab"
is-callable@^1.2.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.1.tgz#4d1e21a4f437509d25ce55f8184350771421c96d"
integrity sha512-wliAfSzx6V+6WfMOmus1xy0XvSgf/dlStkvTfq7F0g4bOIW0PSUbnyse3NhDwdyYS1ozfUtAAySqTws3z9Eqgg==
is-ci@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c"
@ -3276,7 +3390,7 @@ is-data-descriptor@^1.0.0:
dependencies:
kind-of "^6.0.0"
is-date-object@^1.0.1:
is-date-object@^1.0.1, is-date-object@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e"
@ -3341,10 +3455,25 @@ is-installed-globally@^0.3.2:
global-dirs "^2.0.1"
is-path-inside "^3.0.1"
is-map@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.1.tgz#520dafc4307bb8ebc33b813de5ce7c9400d644a1"
integrity sha512-T/S49scO8plUiAOA2DBTBG3JHpn1yiw0kRp6dgiZ0v2/6twi5eiB0rHtHFH9ZIrvlWc6+4O+m4zg5+Z833aXgw==
is-module@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591"
is-negative-zero@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.0.tgz#9553b121b0fac28869da9ed459e20c7543788461"
integrity sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE=
is-number-object@^1.0.3:
version "1.0.4"
resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.4.tgz#36ac95e741cf18b283fc1ddf5e83da798e3ec197"
integrity sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw==
is-number@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195"
@ -3401,6 +3530,18 @@ is-regex@^1.0.5:
dependencies:
has "^1.0.3"
is-regex@^1.1.0, is-regex@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9"
integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==
dependencies:
has-symbols "^1.0.1"
is-set@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.1.tgz#d1604afdab1724986d30091575f54945da7e5f43"
integrity sha512-eJEzOtVyenDs1TMzSQ3kU3K+E0GUS9sno+F0OBT97xsgcJsF9nXMBtkT9/kut5JEpM7oL7X/0qxR17K3mcwIAA==
is-stream@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
@ -3409,16 +3550,41 @@ is-stream@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3"
is-string@^1.0.4, is-string@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6"
integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==
is-symbol@^1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937"
dependencies:
has-symbols "^1.0.1"
is-typed-array@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.3.tgz#a4ff5a5e672e1a55f99c7f54e59597af5c1df04d"
integrity sha512-BSYUBOK/HJibQ30wWkWold5txYwMUXQct9YHAQJr8fSwvZoiglcqB0pd7vEN23+Tsi9IUEjztdOSzl4qLVYGTQ==
dependencies:
available-typed-arrays "^1.0.0"
es-abstract "^1.17.4"
foreach "^2.0.5"
has-symbols "^1.0.1"
is-typedarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
is-weakmap@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2"
integrity sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==
is-weakset@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.1.tgz#e9a0af88dbd751589f5e50d80f4c98b780884f83"
integrity sha512-pi4vhbhVHGLxohUw7PhGsueT4vRGFoXhP7+RGN0jKIv9+8PWYCQTqtADngrxOm2g46hoH0+g8uZZBzMrvVGDmw==
is-windows@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
@ -3439,6 +3605,11 @@ isarray@1.0.0, isarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
isarray@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==
isbuffer@~0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/isbuffer/-/isbuffer-0.0.0.tgz#38c146d9df528b8bf9b0701c3d43cf12df3fc39b"
@ -4558,6 +4729,19 @@ object-inspect@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67"
object-inspect@^1.8.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0"
integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==
object-is@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.2.tgz#c5d2e87ff9e119f78b7a088441519e2eec1573b6"
integrity sha512-5lHCz+0uufF6wZ7CRFWJN3hp8Jqblpgve06U5CMQ3f//6iDjPr2PEo9MWCjEssDsa+UZEL4PkFpr+BMop6aKzQ==
dependencies:
define-properties "^1.1.3"
es-abstract "^1.17.5"
object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
@ -5068,6 +5252,19 @@ regex-not@^1.0.0, regex-not@^1.0.2:
extend-shallow "^3.0.2"
safe-regex "^1.1.0"
regexp.prototype.flags@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz#7aba89b3c13a64509dabcf3ca8d9fbb9bdf5cb75"
integrity sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==
dependencies:
define-properties "^1.1.3"
es-abstract "^1.17.0-next.1"
regexparam@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/regexparam/-/regexparam-1.3.0.tgz#2fe42c93e32a40eff6235d635e0ffa344b92965f"
integrity sha512-6IQpFBv6e5vz1QAqI+V4k8P2e/3gRrqfCJ9FI+O1FLQTO+Uz6RXZEZOPmTJ6hlGj7gkERzY5BRCv09whKP96/g==
regexpu-core@^4.7.0:
version "4.7.0"
resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.7.0.tgz#fcbf458c50431b0bb7b45d6967b8192d91f3d938"
@ -5475,6 +5672,14 @@ shortid@^2.2.15:
dependencies:
nanoid "^2.1.0"
side-channel@^1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.3.tgz#cdc46b057550bbab63706210838df5d4c19519c3"
integrity sha512-A6+ByhlLkksFoUepsGxfj5x1gTSrs+OydsRptUxeNCabQpCFUvcwIczgOigI8vhY/OJCnPnyE9rGiwgvr9cS1g==
dependencies:
es-abstract "^1.18.0-next.0"
object-inspect "^1.8.0"
signal-exit@^3.0.0, signal-exit@^3.0.2:
version "3.0.3"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
@ -5701,7 +5906,7 @@ string-width@^4.2.0:
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.0"
string.prototype.trimend@^1.0.0:
string.prototype.trimend@^1.0.0, string.prototype.trimend@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913"
dependencies:
@ -5724,7 +5929,7 @@ string.prototype.trimright@^2.1.1:
es-abstract "^1.17.5"
string.prototype.trimend "^1.0.0"
string.prototype.trimstart@^1.0.0:
string.prototype.trimstart@^1.0.0, string.prototype.trimstart@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz#14af6d9f34b053f7cfc89b72f8f2ee14b9039a54"
dependencies:
@ -6179,10 +6384,43 @@ whatwg-url@^8.0.0:
tr46 "^2.0.2"
webidl-conversions "^5.0.0"
which-boxed-primitive@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.1.tgz#cbe8f838ebe91ba2471bb69e9edbda67ab5a5ec1"
integrity sha512-7BT4TwISdDGBgaemWU0N0OU7FeAEJ9Oo2P1PHRm/FCWoEi2VLWC9b6xvxAA3C/NMpxg3HXVgi0sMmGbNUbNepQ==
dependencies:
is-bigint "^1.0.0"
is-boolean-object "^1.0.0"
is-number-object "^1.0.3"
is-string "^1.0.4"
is-symbol "^1.0.2"
which-collection@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.1.tgz#70eab71ebbbd2aefaf32f917082fc62cdcb70906"
integrity sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==
dependencies:
is-map "^2.0.1"
is-set "^2.0.1"
is-weakmap "^2.0.1"
is-weakset "^2.0.1"
which-module@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
which-typed-array@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.2.tgz#e5f98e56bda93e3dac196b01d47c1156679c00b2"
integrity sha512-KT6okrd1tE6JdZAy3o2VhMoYPh3+J6EMZLyrxBQsZflI1QCZIxMrIYLkosd8Twf+YfknVIHmYQPgJt238p8dnQ==
dependencies:
available-typed-arrays "^1.0.2"
es-abstract "^1.17.5"
foreach "^2.0.5"
function-bind "^1.1.1"
has-symbols "^1.0.1"
is-typed-array "^1.1.3"
which@^1.2.9, which@^1.3.0:
version "1.3.1"
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"

View File

@ -71,6 +71,7 @@
"pino-pretty": "^4.0.0",
"pouchdb": "^7.2.1",
"pouchdb-all-dbs": "^1.0.2",
"sharp": "^0.26.0",
"squirrelly": "^7.5.0",
"tar-fs": "^2.1.0",
"uuid": "^3.3.2",

View File

@ -1,8 +1,8 @@
const CouchDB = require("../../db")
const newid = require("../../db/newid")
const actions = require("../../workflows/actions")
const logic = require("../../workflows/logic")
const triggers = require("../../workflows/triggers")
const actions = require("../../automations/actions")
const logic = require("../../automations/logic")
const triggers = require("../../automations/triggers")
/*************************
* *
@ -10,12 +10,12 @@ const triggers = require("../../workflows/triggers")
* *
*************************/
function cleanWorkflowInputs(workflow) {
if (workflow == null) {
return workflow
function cleanAutomationInputs(automation) {
if (automation == null) {
return automation
}
let steps = workflow.definition.steps
let trigger = workflow.definition.trigger
let steps = automation.definition.steps
let trigger = automation.definition.trigger
let allSteps = [...steps, trigger]
for (let step of allSteps) {
if (step == null) {
@ -27,25 +27,25 @@ function cleanWorkflowInputs(workflow) {
}
}
}
return workflow
return automation
}
exports.create = async function(ctx) {
const db = new CouchDB(ctx.user.instanceId)
let workflow = ctx.request.body
let automation = ctx.request.body
workflow._id = newid()
automation._id = newid()
workflow.type = "workflow"
workflow = cleanWorkflowInputs(workflow)
const response = await db.post(workflow)
workflow._rev = response.rev
automation.type = "automation"
automation = cleanAutomationInputs(automation)
const response = await db.post(automation)
automation._rev = response.rev
ctx.status = 200
ctx.body = {
message: "Workflow created successfully",
workflow: {
...workflow,
message: "Automation created successfully",
automation: {
...automation,
...response,
},
}
@ -53,17 +53,17 @@ exports.create = async function(ctx) {
exports.update = async function(ctx) {
const db = new CouchDB(ctx.user.instanceId)
let workflow = ctx.request.body
let automation = ctx.request.body
workflow = cleanWorkflowInputs(workflow)
const response = await db.put(workflow)
workflow._rev = response.rev
automation = cleanAutomationInputs(automation)
const response = await db.put(automation)
automation._rev = response.rev
ctx.status = 200
ctx.body = {
message: `Workflow ${workflow._id} updated successfully.`,
workflow: {
...workflow,
message: `Automation ${automation._id} updated successfully.`,
automation: {
...automation,
_rev: response.rev,
_id: response.id,
},
@ -73,7 +73,7 @@ exports.update = async function(ctx) {
exports.fetch = async function(ctx) {
const db = new CouchDB(ctx.user.instanceId)
const response = await db.query(`database/by_type`, {
key: ["workflow"],
key: ["automation"],
include_docs: true,
})
ctx.body = response.rows.map(row => row.doc)
@ -117,14 +117,14 @@ module.exports.getDefinitionList = async function(ctx) {
exports.trigger = async function(ctx) {
const db = new CouchDB(ctx.user.instanceId)
let workflow = await db.get(ctx.params.id)
await triggers.externalTrigger(workflow, {
let automation = await db.get(ctx.params.id)
await triggers.externalTrigger(automation, {
...ctx.request.body,
instanceId: ctx.user.instanceId,
})
ctx.status = 200
ctx.body = {
message: `Workflow ${workflow._id} has been triggered.`,
workflow,
message: `Automation ${automation._id} has been triggered.`,
automation,
}
}

View File

@ -2,6 +2,7 @@ const fs = require("fs")
const AWS = require("aws-sdk")
const fetch = require("node-fetch")
const { budibaseAppsDir } = require("../../../utilities/budibaseDir")
const PouchDB = require("../../../db")
async function invalidateCDN(cfDistribution, appId) {
const cf = new AWS.CloudFront({})
@ -63,8 +64,22 @@ function walkDir(dirPath, callback) {
}
}
function prepareUploadForS3({ filePath, s3Key, metadata, s3 }) {
const fileExtension = [...filePath.split(".")].pop()
const fileBytes = fs.readFileSync(filePath)
return s3
.upload({
Key: s3Key,
Body: fileBytes,
ContentType: CONTENT_TYPE_MAP[fileExtension.toLowerCase()],
Metadata: metadata,
})
.promise()
}
exports.uploadAppAssets = async function({
appId,
instanceId,
credentials,
bucket,
cfDistribution,
@ -86,30 +101,47 @@ exports.uploadAppAssets = async function({
const appPages = fs.readdirSync(appAssetsPath)
const uploads = []
let uploads = []
for (let page of appPages) {
walkDir(`${appAssetsPath}/${page}`, function prepareUploadsForS3(filePath) {
const fileExtension = [...filePath.split(".")].pop()
const fileBytes = fs.readFileSync(filePath)
const upload = s3
.upload({
Key: filePath.replace(appAssetsPath, `assets/${appId}`),
Body: fileBytes,
ContentType: CONTENT_TYPE_MAP[fileExtension],
Metadata: {
accountId,
},
})
.promise()
uploads.push(upload)
// Upload HTML, CSS and JS for each page of the web app
walkDir(`${appAssetsPath}/${page}`, function(filePath) {
const appAssetUpload = prepareUploadForS3({
filePath,
s3Key: filePath.replace(appAssetsPath, `assets/${appId}`),
s3,
metadata: { accountId },
})
uploads.push(appAssetUpload)
})
}
// Upload file attachments
const db = new PouchDB(instanceId)
const fileUploads = await db.get("_local/fileuploads")
if (fileUploads) {
for (let file of fileUploads.uploads) {
if (file.uploaded) continue
const attachmentUpload = prepareUploadForS3({
filePath: file.path,
s3Key: `assets/${appId}/attachments/${file.name}`,
s3,
metadata: { accountId },
})
uploads.push(attachmentUpload)
// mark file as uploaded
file.uploaded = true
}
db.put(fileUploads)
}
try {
await Promise.all(uploads)
// TODO: update dynamoDB with a synopsis of the app deployment for historical purposes
await invalidateCDN(cfDistribution, appId)
} catch (err) {
console.error("Error uploading budibase app assets to s3", err)

View File

@ -42,6 +42,7 @@ exports.deployApp = async function(ctx) {
await uploadAppAssets({
clientId,
appId: ctx.user.appId,
instanceId: ctx.user.instanceId,
...credentials,
})

View File

@ -31,9 +31,9 @@ exports.create = async function(ctx) {
emit([doc.type], doc._id)
}.toString(),
},
by_workflow_trigger: {
by_automation_trigger: {
map: function(doc) {
if (doc.type === "workflow") {
if (doc.type === "automation") {
const trigger = doc.definition.trigger
if (trigger) {
emit([trigger.event], trigger)

View File

@ -188,7 +188,7 @@ exports.destroy = async function(ctx) {
}
ctx.body = await db.remove(ctx.params.recordId, ctx.params.revId)
ctx.status = 200
// for workflows
// for automations
ctx.record = record
emitEvent(`record:delete`, ctx, record)
}

View File

@ -1,13 +1,18 @@
const send = require("koa-send")
const { resolve, join } = require("path")
const jwt = require("jsonwebtoken")
const fetch = require("node-fetch")
const fs = require("fs")
const uuid = require("uuid")
const {
budibaseAppsDir,
budibaseTempDir,
} = require("../../utilities/budibaseDir")
const CouchDB = require("../../db")
const setBuilderToken = require("../../utilities/builder/setBuilderToken")
const { ANON_LEVEL_ID } = require("../../utilities/accessLevels")
const jwt = require("jsonwebtoken")
const fetch = require("node-fetch")
const fileProcessor = require("../../utilities/fileProcessor")
exports.serveBuilder = async function(ctx) {
let builderPath = resolve(__dirname, "../../../builder")
@ -17,6 +22,66 @@ exports.serveBuilder = async function(ctx) {
await send(ctx, ctx.file, { root: ctx.devPath || builderPath })
}
exports.processLocalFileUpload = async function(ctx) {
const { files } = ctx.request.body
const attachmentsPath = resolve(
budibaseAppsDir(),
ctx.user.appId,
"attachments"
)
// create attachments dir if it doesnt exist
!fs.existsSync(attachmentsPath) &&
fs.mkdirSync(attachmentsPath, { recursive: true })
const filesToProcess = files.map(file => {
const fileExtension = [...file.path.split(".")].pop()
// filenames converted to UUIDs so they are unique
const fileName = `${uuid.v4()}.${fileExtension}`
return {
...file,
fileName,
extension: fileExtension,
outputPath: join(attachmentsPath, fileName),
url: join("/attachments", fileName),
}
})
const fileProcessOperations = filesToProcess.map(file =>
fileProcessor.process(file)
)
try {
const processedFiles = await Promise.all(fileProcessOperations)
let pendingFileUploads
// local document used to track which files need to be uploaded
// db.get throws an error if the document doesn't exist
// need to use a promise to default
const db = new CouchDB(ctx.user.instanceId)
await db
.get("_local/fileuploads")
.then(data => {
pendingFileUploads = data
})
.catch(() => {
pendingFileUploads = { _id: "_local/fileuploads", uploads: [] }
})
pendingFileUploads.uploads = [
...processedFiles,
...pendingFileUploads.uploads,
]
await db.put(pendingFileUploads)
ctx.body = processedFiles
} catch (err) {
ctx.throw(500, err)
}
}
exports.serveApp = async function(ctx) {
const mainOrAuth = ctx.isAuthenticated ? "main" : "unauthenticated"
@ -62,6 +127,24 @@ exports.serveApp = async function(ctx) {
await send(ctx, ctx.file || "index.html", { root: ctx.devPath || appPath })
}
exports.serveAttachment = async function(ctx) {
const appId = ctx.user.appId
const attachmentsPath = resolve(budibaseAppsDir(), appId, "attachments")
// Serve from CloudFront
if (process.env.CLOUD) {
const S3_URL = `https://cdn.app.budi.live/assets/${appId}/attachments/${ctx.file}`
const response = await fetch(S3_URL)
const body = await response.text()
ctx.body = body
return
}
await send(ctx, ctx.file, { root: attachmentsPath })
}
exports.serveAppAsset = async function(ctx) {
// default to homedir
const mainOrAuth = ctx.isAuthenticated ? "main" : "unauthenticated"

View File

@ -12,7 +12,7 @@ const controller = {
!name.startsWith("all") &&
name !== "by_type" &&
name !== "by_username" &&
name !== "by_workflow_trigger"
name !== "by_automation_trigger"
) {
response.push({
name,

View File

@ -16,7 +16,7 @@ const {
viewRoutes,
staticRoutes,
componentRoutes,
workflowRoutes,
automationRoutes,
accesslevelRoutes,
apiKeysRoutes,
} = require("./routes")
@ -84,8 +84,8 @@ router.use(userRoutes.allowedMethods())
router.use(instanceRoutes.routes())
router.use(instanceRoutes.allowedMethods())
router.use(workflowRoutes.routes())
router.use(workflowRoutes.allowedMethods())
router.use(automationRoutes.routes())
router.use(automationRoutes.allowedMethods())
router.use(deployRoutes.routes())
router.use(deployRoutes.allowedMethods())

View File

@ -1,5 +1,5 @@
const Router = require("@koa/router")
const controller = require("../controllers/workflow")
const controller = require("../controllers/automation")
const authorized = require("../../middleware/authorized")
const joiValidator = require("../../middleware/joi-validator")
const { BUILDER } = require("../../utilities/accessLevels")
@ -30,7 +30,7 @@ function generateValidator(existing = false) {
_id: existing ? Joi.string().required() : Joi.string(),
_rev: existing ? Joi.string().required() : Joi.string(),
name: Joi.string().required(),
type: Joi.string().valid("workflow").required(),
type: Joi.string().valid("automation").required(),
definition: Joi.object({
steps: Joi.array().required().items(generateStepSchema(["ACTION", "LOGIC"])),
trigger: generateStepSchema(["TRIGGER"]),
@ -40,40 +40,40 @@ function generateValidator(existing = false) {
router
.get(
"/api/workflows/trigger/list",
"/api/automations/trigger/list",
authorized(BUILDER),
controller.getTriggerList
)
.get(
"/api/workflows/action/list",
"/api/automations/action/list",
authorized(BUILDER),
controller.getActionList
)
.get(
"/api/workflows/logic/list",
"/api/automations/logic/list",
authorized(BUILDER),
controller.getLogicList
)
.get(
"/api/workflows/definitions/list",
"/api/automations/definitions/list",
authorized(BUILDER),
controller.getDefinitionList
)
.get("/api/workflows", authorized(BUILDER), controller.fetch)
.get("/api/workflows/:id", authorized(BUILDER), controller.find)
.get("/api/automations", authorized(BUILDER), controller.fetch)
.get("/api/automations/:id", authorized(BUILDER), controller.find)
.put(
"/api/workflows",
"/api/automations",
authorized(BUILDER),
generateValidator(true),
controller.update
)
.post(
"/api/workflows",
"/api/automations",
authorized(BUILDER),
generateValidator(false),
controller.create
)
.post("/api/workflows/:id/trigger", controller.trigger)
.delete("/api/workflows/:id/:rev", authorized(BUILDER), controller.destroy)
.post("/api/automations/:id/trigger", controller.trigger)
.delete("/api/automations/:id/:rev", authorized(BUILDER), controller.destroy)
module.exports = router

View File

@ -9,7 +9,7 @@ const recordRoutes = require("./record")
const viewRoutes = require("./view")
const staticRoutes = require("./static")
const componentRoutes = require("./component")
const workflowRoutes = require("./workflow")
const automationRoutes = require("./automation")
const accesslevelRoutes = require("./accesslevel")
const deployRoutes = require("./deploy")
const apiKeysRoutes = require("./apikeys")
@ -27,7 +27,7 @@ module.exports = {
viewRoutes,
staticRoutes,
componentRoutes,
workflowRoutes,
automationRoutes,
accesslevelRoutes,
apiKeysRoutes,
}

View File

@ -2,6 +2,8 @@ const Router = require("@koa/router")
const controller = require("../controllers/static")
const { budibaseTempDir } = require("../../utilities/budibaseDir")
const env = require("../../environment")
const authorized = require("../../middleware/authorized")
const { BUILDER } = require("../../utilities/accessLevels")
const router = Router()
@ -21,8 +23,14 @@ if (env.NODE_ENV !== "production") {
}
router
.post(
"/api/attachments/process",
authorized(BUILDER),
controller.processLocalFileUpload
)
.get("/componentlibrary", controller.serveComponentLibrary)
.get("/assets/:file*", controller.serveAppAsset)
.get("/attachments/:file*", controller.serveAttachment)
.get("/:appId/:path*", controller.serveApp)
module.exports = router

View File

@ -13,9 +13,9 @@ const {
const { delay } = require("./testUtils")
const TEST_WORKFLOW = {
_id: "Test Workflow",
name: "My Workflow",
const TEST_AUTOMATION = {
_id: "Test Automation",
name: "My Automation",
pageId: "123123123",
screenId: "kasdkfldsafkl",
live: true,
@ -27,20 +27,20 @@ const TEST_WORKFLOW = {
steps: [
],
},
type: "workflow",
type: "automation",
}
let ACTION_DEFINITIONS = {}
let TRIGGER_DEFINITIONS = {}
let LOGIC_DEFINITIONS = {}
describe("/workflows", () => {
describe("/automations", () => {
let request
let server
let app
let instance
let workflow
let workflowId
let automation
let automationId
beforeAll(async () => {
({ request, server } = await supertest())
@ -49,7 +49,7 @@ describe("/workflows", () => {
})
beforeEach(async () => {
if (workflow) await destroyDocument(workflow.id)
if (automation) await destroyDocument(automation.id)
instance = await createInstance(request, app._id)
})
@ -57,18 +57,18 @@ describe("/workflows", () => {
server.close()
})
const createWorkflow = async () => {
workflow = await insertDocument(instance._id, {
type: "workflow",
...TEST_WORKFLOW
const createAutomation = async () => {
automation = await insertDocument(instance._id, {
type: "automation",
...TEST_AUTOMATION
})
workflow = { ...workflow, ...TEST_WORKFLOW }
automation = { ...automation, ...TEST_AUTOMATION }
}
describe("get definitions", () => {
it("returns a list of definitions for actions", async () => {
const res = await request
.get(`/api/workflows/action/list`)
.get(`/api/automations/action/list`)
.set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/)
.expect(200)
@ -79,7 +79,7 @@ describe("/workflows", () => {
it("returns a list of definitions for triggers", async () => {
const res = await request
.get(`/api/workflows/trigger/list`)
.get(`/api/automations/trigger/list`)
.set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/)
.expect(200)
@ -90,7 +90,7 @@ describe("/workflows", () => {
it("returns a list of definitions for actions", async () => {
const res = await request
.get(`/api/workflows/logic/list`)
.get(`/api/automations/logic/list`)
.set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/)
.expect(200)
@ -101,7 +101,7 @@ describe("/workflows", () => {
it("returns all of the definitions in one", async () => {
const res = await request
.get(`/api/workflows/definitions/list`)
.get(`/api/automations/definitions/list`)
.set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/)
.expect(200)
@ -113,7 +113,7 @@ describe("/workflows", () => {
})
describe("create", () => {
it("should setup the workflow fully", () => {
it("should setup the automation fully", () => {
let trigger = TRIGGER_DEFINITIONS["RECORD_SAVED"]
trigger.id = "wadiawdo34"
let saveAction = ACTION_DEFINITIONS["SAVE_RECORD"]
@ -123,51 +123,51 @@ describe("/workflows", () => {
}
saveAction.id = "awde444wk"
TEST_WORKFLOW.definition.steps.push(saveAction)
TEST_WORKFLOW.definition.trigger = trigger
TEST_AUTOMATION.definition.steps.push(saveAction)
TEST_AUTOMATION.definition.trigger = trigger
})
it("returns a success message when the workflow is successfully created", async () => {
it("returns a success message when the automation is successfully created", async () => {
const res = await request
.post(`/api/workflows`)
.post(`/api/automations`)
.set(defaultHeaders(app._id, instance._id))
.send(TEST_WORKFLOW)
.send(TEST_AUTOMATION)
.expect('Content-Type', /json/)
.expect(200)
expect(res.body.message).toEqual("Workflow created successfully")
expect(res.body.workflow.name).toEqual("My Workflow")
expect(res.body.workflow._id).not.toEqual(null)
workflowId = res.body.workflow._id
expect(res.body.message).toEqual("Automation created successfully")
expect(res.body.automation.name).toEqual("My Automation")
expect(res.body.automation._id).not.toEqual(null)
automationId = res.body.automation._id
})
it("should apply authorization to endpoint", async () => {
await builderEndpointShouldBlockNormalUsers({
request,
method: "POST",
url: `/api/workflows`,
url: `/api/automations`,
instanceId: instance._id,
appId: app._id,
body: TEST_WORKFLOW
body: TEST_AUTOMATION
})
})
})
describe("trigger", () => {
it("trigger the workflow successfully", async () => {
it("trigger the automation successfully", async () => {
let model = await createModel(request, app._id, instance._id)
TEST_WORKFLOW.definition.trigger.inputs.modelId = model._id
TEST_WORKFLOW.definition.steps[0].inputs.record.modelId = model._id
await createWorkflow()
TEST_AUTOMATION.definition.trigger.inputs.modelId = model._id
TEST_AUTOMATION.definition.steps[0].inputs.record.modelId = model._id
await createAutomation()
const res = await request
.post(`/api/workflows/${workflow._id}/trigger`)
.post(`/api/automations/${automation._id}/trigger`)
.send({ name: "Test" })
.set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/)
.expect(200)
expect(res.body.message).toEqual(`Workflow ${workflow._id} has been triggered.`)
expect(res.body.workflow.name).toEqual(TEST_WORKFLOW.name)
// wait for workflow to complete in background
expect(res.body.message).toEqual(`Automation ${automation._id} has been triggered.`)
expect(res.body.automation.name).toEqual(TEST_AUTOMATION.name)
// wait for automation to complete in background
await delay(500)
let elements = await getAllFromModel(request, app._id, instance._id, model._id)
expect(elements.length).toEqual(1)
@ -177,42 +177,42 @@ describe("/workflows", () => {
})
describe("update", () => {
it("updates a workflows data", async () => {
await createWorkflow()
workflow._id = workflow.id
workflow._rev = workflow.rev
workflow.name = "Updated Name"
workflow.type = "workflow"
it("updates a automations data", async () => {
await createAutomation()
automation._id = automation.id
automation._rev = automation.rev
automation.name = "Updated Name"
automation.type = "automation"
const res = await request
.put(`/api/workflows`)
.put(`/api/automations`)
.set(defaultHeaders(app._id, instance._id))
.send(workflow)
.send(automation)
.expect('Content-Type', /json/)
.expect(200)
expect(res.body.message).toEqual("Workflow Test Workflow updated successfully.")
expect(res.body.workflow.name).toEqual("Updated Name")
expect(res.body.message).toEqual("Automation Test Automation updated successfully.")
expect(res.body.automation.name).toEqual("Updated Name")
})
})
describe("fetch", () => {
it("return all the workflows for an instance", async () => {
await createWorkflow()
it("return all the automations for an instance", async () => {
await createAutomation()
const res = await request
.get(`/api/workflows`)
.get(`/api/automations`)
.set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/)
.expect(200)
expect(res.body[0]).toEqual(expect.objectContaining(TEST_WORKFLOW))
expect(res.body[0]).toEqual(expect.objectContaining(TEST_AUTOMATION))
})
it("should apply authorization to endpoint", async () => {
await builderEndpointShouldBlockNormalUsers({
request,
method: "GET",
url: `/api/workflows`,
url: `/api/automations`,
instanceId: instance._id,
appId: app._id,
})
@ -220,23 +220,23 @@ describe("/workflows", () => {
})
describe("destroy", () => {
it("deletes a workflow by its ID", async () => {
await createWorkflow()
it("deletes a automation by its ID", async () => {
await createAutomation()
const res = await request
.delete(`/api/workflows/${workflow.id}/${workflow.rev}`)
.delete(`/api/automations/${automation.id}/${automation.rev}`)
.set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/)
.expect(200)
expect(res.body.id).toEqual(TEST_WORKFLOW._id)
expect(res.body.id).toEqual(TEST_AUTOMATION._id)
})
it("should apply authorization to endpoint", async () => {
await createWorkflow()
await createAutomation()
await builderEndpointShouldBlockNormalUsers({
request,
method: "DELETE",
url: `/api/workflows/${workflow.id}/${workflow._rev}`,
url: `/api/automations/${automation.id}/${automation._rev}`,
instanceId: instance._id,
appId: app._id,
})

View File

@ -6,7 +6,7 @@ const http = require("http")
const api = require("./api")
const env = require("./environment")
const eventEmitter = require("./events")
const workflows = require("./workflows/index")
const automations = require("./automations/index")
const Sentry = require("@sentry/node")
const app = new Koa()
@ -50,5 +50,5 @@ process.on("SIGINT", () => process.exit(1))
module.exports = server.listen(env.PORT || 4001, () => {
console.log(`Budibase running on ${JSON.stringify(server.address())}`)
workflows.init()
automations.init()
})

View File

@ -23,7 +23,7 @@ function runWorker(job) {
*/
module.exports.init = function() {
actions.init().then(() => {
triggers.workflowQueue.process(async job => {
triggers.automationQueue.process(async job => {
if (environment.BUDIBASE_ENVIRONMENT === "PRODUCTION") {
await runWorker(job)
} else {

View File

@ -82,4 +82,4 @@ module.exports.run = async function({ inputs, instanceId }) {
response: err,
}
}
}
}

View File

@ -4,7 +4,7 @@ module.exports.definition = {
name: "Delay",
icon: "ri-time-fill",
tagline: "Delay for {{inputs.time}} milliseconds",
description: "Delay the workflow until an amount of time has passed",
description: "Delay the automation until an amount of time has passed",
stepId: "DELAY",
inputs: {},
schema: {

View File

@ -16,7 +16,7 @@ module.exports.definition = {
name: "Filter",
tagline: "{{inputs.field}} {{inputs.condition}} {{inputs.value}}",
icon: "ri-git-branch-line",
description: "Filter any workflows which do not meet certain conditions",
description: "Filter any automations which do not meet certain conditions",
type: "LOGIC",
stepId: "FILTER",
inputs: {

View File

@ -28,6 +28,11 @@ module.exports.definition = {
},
outputs: {
properties: {
record: {
type: "object",
customType: "record",
description: "The new record",
},
response: {
type: "object",
description: "The response from the table",
@ -69,6 +74,7 @@ module.exports.run = async function({ inputs, instanceId }) {
try {
await recordController.save(ctx)
return {
record: inputs.record,
response: ctx.body,
id: ctx.body._id,
revision: ctx.body._rev,

View File

@ -42,18 +42,18 @@ function recurseMustache(inputs, context) {
}
/**
* The workflow orchestrator is a class responsible for executing workflows.
* It handles the context of the workflow and makes sure each step gets the correct
* The automation orchestrator is a class responsible for executing automations.
* It handles the context of the automation and makes sure each step gets the correct
* inputs and handles any outputs.
*/
class Orchestrator {
constructor(workflow, triggerOutput) {
constructor(automation, triggerOutput) {
this._instanceId = triggerOutput.instanceId
// remove from context
delete triggerOutput.instanceId
// step zero is never used as the mustache is zero indexed for customer facing
this._context = { steps: [{}], trigger: triggerOutput }
this._workflow = workflow
this._automation = automation
}
async getStepFunctionality(type, stepId) {
@ -64,14 +64,14 @@ class Orchestrator {
step = logic.getLogic(stepId)
}
if (step == null) {
throw `Cannot find workflow step by name ${stepId}`
throw `Cannot find automation step by name ${stepId}`
}
return step
}
async execute() {
let workflow = this._workflow
for (let step of workflow.definition.steps) {
let automation = this._automation
for (let step of automation.definition.steps) {
let stepFn = await this.getStepFunctionality(step.type, step.stepId)
step.inputs = recurseMustache(step.inputs, this._context)
// instanceId is always passed
@ -90,11 +90,11 @@ class Orchestrator {
// callback is required for worker-farm to state that the worker thread has completed
module.exports = async (job, cb = null) => {
try {
const workflowOrchestrator = new Orchestrator(
job.data.workflow,
const automationOrchestrator = new Orchestrator(
job.data.automation,
job.data.event
)
await workflowOrchestrator.execute()
await automationOrchestrator.execute()
if (cb) {
cb()
}

View File

@ -2,7 +2,7 @@ const CouchDB = require("../db")
const emitter = require("../events/index")
const InMemoryQueue = require("./queue/inMemoryQueue")
let workflowQueue = new InMemoryQueue()
let automationQueue = new InMemoryQueue()
const FAKE_STRING = "TEST"
const FAKE_BOOL = false
@ -76,28 +76,31 @@ const BUILTIN_DEFINITIONS = {
},
}
async function queueRelevantRecordWorkflows(event, eventType) {
async function queueRelevantRecordAutomations(event, eventType) {
if (event.instanceId == null) {
throw `No instanceId specified for ${eventType} - check event emitters.`
}
const db = new CouchDB(event.instanceId)
const workflowsToTrigger = await db.query("database/by_workflow_trigger", {
key: [eventType],
include_docs: true,
})
const automationsToTrigger = await db.query(
"database/by_automation_trigger",
{
key: [eventType],
include_docs: true,
}
)
const workflows = workflowsToTrigger.rows.map(wf => wf.doc)
for (let workflow of workflows) {
let workflowDef = workflow.definition
let workflowTrigger = workflowDef ? workflowDef.trigger : {}
const automations = automationsToTrigger.rows.map(wf => wf.doc)
for (let automation of automations) {
let automationDef = automation.definition
let automationTrigger = automationDef ? automationDef.trigger : {}
if (
!workflow.live ||
!workflowTrigger.inputs ||
workflowTrigger.inputs.modelId !== event.record.modelId
!automation.live ||
!automationTrigger.inputs ||
automationTrigger.inputs.modelId !== event.record.modelId
) {
continue
}
workflowQueue.add({ workflow, event })
automationQueue.add({ automation, event })
}
}
@ -105,18 +108,18 @@ emitter.on("record:save", async function(event) {
if (!event || !event.record || !event.record.modelId) {
return
}
await queueRelevantRecordWorkflows(event, "record:save")
await queueRelevantRecordAutomations(event, "record:save")
})
emitter.on("record:delete", async function(event) {
if (!event || !event.record || !event.record.modelId) {
return
}
await queueRelevantRecordWorkflows(event, "record:delete")
await queueRelevantRecordAutomations(event, "record:delete")
})
async function fillRecordOutput(workflow, params) {
let triggerSchema = workflow.definition.trigger
async function fillRecordOutput(automation, params) {
let triggerSchema = automation.definition.trigger
let modelId = triggerSchema.inputs.modelId
const db = new CouchDB(params.instanceId)
try {
@ -147,19 +150,19 @@ async function fillRecordOutput(workflow, params) {
return params
}
module.exports.externalTrigger = async function(workflow, params) {
module.exports.externalTrigger = async function(automation, params) {
// TODO: replace this with allowing user in builder to input values in future
if (
workflow.definition != null &&
workflow.definition.trigger != null &&
workflow.definition.trigger.inputs.modelId != null
automation.definition != null &&
automation.definition.trigger != null &&
automation.definition.trigger.inputs.modelId != null
) {
params = await fillRecordOutput(workflow, params)
params = await fillRecordOutput(automation, params)
}
workflowQueue.add({ workflow, event: params })
automationQueue.add({ automation, event: params })
}
module.exports.workflowQueue = workflowQueue
module.exports.automationQueue = automationQueue
module.exports.BUILTIN_DEFINITIONS = BUILTIN_DEFINITIONS

View File

@ -2,7 +2,7 @@ const EventEmitter = require("events").EventEmitter
/**
* keeping event emitter in one central location as it might be used for things other than
* workflows (what it was for originally) - having a central emitter will be useful in the
* automations (what it was for originally) - having a central emitter will be useful in the
* future.
*/

View File

@ -2,7 +2,7 @@
module.exports.READ_MODEL = "read-model"
module.exports.WRITE_MODEL = "write-model"
module.exports.READ_VIEW = "read-view"
module.exports.EXECUTE_WORKFLOW = "execute-workflow"
module.exports.EXECUTE_AUTOMATION = "execute-automation"
module.exports.USER_MANAGEMENT = "user-management"
module.exports.BUILDER = "builder"
module.exports.LIST_USERS = "list-users"
@ -32,4 +32,5 @@ module.exports.adminPermissions = [
// to avoid circular dependencies this is included later, after exporting all enums
const permissions = require("./permissions")
module.exports.generateAdminPermissions = permissions.generateAdminPermissions
module.exports.generatePowerUserPermissions = permissions.generatePowerUserPermissions
module.exports.generatePowerUserPermissions =
permissions.generatePowerUserPermissions

View File

@ -0,0 +1,30 @@
const fs = require("fs")
const sharp = require("sharp")
const fsPromises = fs.promises
const FORMATS = {
IMAGES: ["png", "jpg", "jpeg", "gif", "svg", "tiff", "raw"],
}
async function processImage(file) {
const imgMeta = await sharp(file.path)
.resize(300)
.toFile(file.outputPath)
return {
...file,
...imgMeta,
}
}
async function process(file) {
if (FORMATS.IMAGES.includes(file.extension.toLowerCase())) {
return await processImage(file)
}
// No processing required
await fsPromises.copyFile(file.path, file.outputPath)
return file
}
exports.process = process

Some files were not shown because too many files have changed in this diff Show More