merge with master

This commit is contained in:
Martin McKeaveney 2020-09-17 16:40:09 +01:00
commit e609414f57
114 changed files with 8911 additions and 1630 deletions

View File

@ -4,8 +4,6 @@ on:
# Trigger the workflow on push with tags, # Trigger the workflow on push with tags,
# but only for the master branch # but only for the master branch
push: push:
branches:
- master
tags: tags:
- 'v*' - 'v*'

View File

@ -1,5 +1,5 @@
{ {
"version": "0.1.19", "version": "0.1.21",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*" "packages/*"

View File

@ -21,12 +21,15 @@
"publishdev": "lerna run publishdev", "publishdev": "lerna run publishdev",
"publishnpm": "yarn build && lerna publish --force-publish", "publishnpm": "yarn build && lerna publish --force-publish",
"clean": "lerna clean", "clean": "lerna clean",
"dev": "node ./scripts/symlinkDev.js && lerna run --parallel --stream dev:builder", "dev": "node ./scripts/symlinkDev.js && lerna run --parallel dev:builder",
"test": "lerna run test", "test": "lerna run test",
"lint": "eslint packages", "lint": "eslint packages",
"lint:fix": "eslint --fix packages", "lint:fix": "eslint --fix packages",
"format": "prettier --write \"{,!(node_modules)/**/}*.{js,jsx,svelte}\"", "format": "prettier --write \"{,!(node_modules)/**/}*.{js,jsx,svelte}\"",
"test:e2e": "lerna run cy:test", "test:e2e": "lerna run cy:test",
"test:e2e:ci": "lerna run cy:ci" "test:e2e:ci": "lerna run cy:ci"
},
"dependencies": {
"@fortawesome/fontawesome": "^1.1.8"
} }
} }

View File

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

View File

@ -1,46 +1,52 @@
context('Create a workflow', () => { context("Create a workflow", () => {
before(() => { before(() => {
cy.server() cy.server()
cy.visit('localhost:4001/_builder') cy.visit("localhost:4001/_builder")
cy.createApp('Workflow Test App', 'This app is used to test that workflows do in fact work!') cy.createApp(
"Workflow Test App",
"This app is used to test that workflows do in fact work!"
)
}) })
// https://on.cypress.io/interacting-with-elements // https://on.cypress.io/interacting-with-elements
it('should create a workflow', () => { it("should create a workflow", () => {
cy.createTestTableWithData() cy.createTestTableWithData()
cy.contains('workflow').click() cy.contains("workflow").click()
cy.contains('Create New Workflow').click() cy.contains("Create New Workflow").click()
cy.get('input').type('Add Record') cy.get("input").type("Add Record")
cy.contains('Save').click() cy.contains("Save").click()
// Add trigger // Add trigger
cy.get('[data-cy=add-workflow-component]').click() cy.get("[data-cy=add-workflow-component]").click()
cy.get('[data-cy=RECORD_SAVED]').click() cy.get("[data-cy=RECORD_SAVED]").click()
cy.get('.budibase__input').select('dog') cy.get(".budibase__input").select("dog")
// Create action // Create action
cy.get('[data-cy=SAVE_RECORD]').click() cy.get("[data-cy=SAVE_RECORD]").click()
cy.get('.container input').first().type('goodboy') cy.get(".budibase__input").select("dog")
cy.get('.container input').eq(1).type('11') cy.get(".container input")
.first()
.type("goodboy")
cy.get(".container input")
.eq(1)
.type("11")
// Save // Save
cy.contains('Save Workflow').click() cy.contains("Save Workflow").click()
// Activate Workflow // Activate Workflow
cy.get('[data-cy=activate-workflow]').click() cy.get("[data-cy=activate-workflow]").click()
cy.contains("Add Record").should("be.visible") cy.contains("Add Record").should("be.visible")
cy.get(".stop-button.highlighted").should("be.visible") cy.get(".stop-button.highlighted").should("be.visible")
}) })
it('should add record when a new record is added', () => { it("should add record when a new record is added", () => {
cy.contains('backend').click() cy.contains("backend").click()
cy.addRecord(["Rover", 15]) cy.addRecord(["Rover", 15])
cy.reload() cy.reload()
cy.contains('goodboy').should('have.text', 'goodboy') cy.contains("goodboy").should("have.text", "goodboy")
}) })
}) })

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/builder", "name": "@budibase/builder",
"version": "0.1.19", "version": "0.1.21",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"private": true, "private": true,
"scripts": { "scripts": {
@ -63,16 +63,16 @@
} }
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "^1.32.0", "@budibase/bbui": "^1.33.0",
"@budibase/client": "^0.1.19", "@budibase/client": "^0.1.21",
"@budibase/colorpicker": "^1.0.1", "@budibase/colorpicker": "^1.0.1",
"@fortawesome/fontawesome-free": "^5.14.0",
"@sentry/browser": "5.19.1", "@sentry/browser": "5.19.1",
"@svelteschool/svelte-forms": "^0.7.0", "@svelteschool/svelte-forms": "^0.7.0",
"britecharts": "^2.16.0", "britecharts": "^2.16.0",
"d3-selection": "^1.4.1", "d3-selection": "^1.4.1",
"deepmerge": "^4.2.2", "deepmerge": "^4.2.2",
"fast-sort": "^2.2.0", "fast-sort": "^2.2.0",
"feather-icons": "^4.21.0",
"lodash": "^4.17.13", "lodash": "^4.17.13",
"mustache": "^4.0.1", "mustache": "^4.0.1",
"posthog-js": "1.3.1", "posthog-js": "1.3.1",

View File

@ -234,7 +234,7 @@ export default {
// Watch the `dist` directory and refresh the // Watch the `dist` directory and refresh the
// browser on changes when not in production // browser on changes when not in production
!production && livereload(outputpath), !production && livereload({ watch: outputpath, delay: 500 }),
// If we're building for production (npm run build // If we're building for production (npm run build
// instead of npm run dev), minify // instead of npm run dev), minify

View File

@ -1,3 +1,4 @@
// Array.flat needs polyfilled in < Node 11
if (!Array.prototype.flat) { if (!Array.prototype.flat) {
Object.defineProperty(Array.prototype, "flat", { Object.defineProperty(Array.prototype, "flat", {
configurable: true, configurable: true,

View File

@ -37,7 +37,9 @@ export default function({ componentInstanceId, screen, components, models }) {
.filter(isInstanceInSharedContext(walkResult)) .filter(isInstanceInSharedContext(walkResult))
.map(componentInstanceToBindable(walkResult)), .map(componentInstanceToBindable(walkResult)),
...walkResult.target._contexts.map(contextToBindables(walkResult)).flat(), ...walkResult.target._contexts
.map(contextToBindables(models, walkResult))
.flat(),
] ]
} }
@ -69,17 +71,31 @@ const componentInstanceToBindable = walkResult => i => {
} }
} }
const contextToBindables = walkResult => context => { const contextToBindables = (models, walkResult) => context => {
const contextParentPath = getParentPath(walkResult, context) const contextParentPath = getParentPath(walkResult, context)
return Object.keys(context.model.schema).map(k => ({ const newBindable = key => ({
type: "context", type: "context",
instance: context.instance, instance: context.instance,
// how the binding expression persists, and is used in the app at runtime // how the binding expression persists, and is used in the app at runtime
runtimeBinding: `${contextParentPath}data.${k}`, runtimeBinding: `${contextParentPath}data.${key}`,
// how the binding exressions looks to the user of the builder // how the binding exressions looks to the user of the builder
readableBinding: `${context.instance._instanceName}.${context.model.name}.${k}`, readableBinding: `${context.instance._instanceName}.${context.model.label}.${key}`,
})) })
// see ModelViewSelect.svelte for the format of context.model
// ... this allows us to bind to Model scheams, or View schemas
const model = models.find(m => m._id === context.model.modelId)
const schema = context.model.isModel
? model.schema
: model.views[context.model.name].schema
return (
Object.keys(schema)
.map(newBindable)
// add _id and _rev fields - not part of schema, but always valid
.concat([newBindable("_id"), newBindable("_rev")])
)
} }
const getParentPath = (walkResult, context) => { const getParentPath = (walkResult, context) => {
@ -135,7 +151,7 @@ const walk = ({ instance, targetId, components, models, result }) => {
if (contextualInstance) { if (contextualInstance) {
// add to currentContexts (ancestory of context) // add to currentContexts (ancestory of context)
// before walking children // before walking children
const model = models.find(m => m._id === instance[component.context]) const model = instance[component.context]
result.currentContexts.push({ instance, model }) result.currentContexts.push({ instance, model })
} }

View File

@ -0,0 +1,42 @@
export const CAPTURE_VAR_INSIDE_MUSTACHE = /{{([^}]+)}}/g
export function readableToRuntimeBinding(bindableProperties, textWithBindings) {
// Find all instances of mustasche
const boundValues = textWithBindings.match(CAPTURE_VAR_INSIDE_MUSTACHE)
let result = textWithBindings
// Replace readableBindings with runtimeBindings
boundValues &&
boundValues.forEach(boundValue => {
const binding = bindableProperties.find(({ readableBinding }) => {
return boundValue === `{{ ${readableBinding} }}`
})
if (binding) {
result = textWithBindings.replace(
boundValue,
`{{ ${binding.runtimeBinding} }}`
)
}
})
return result
}
export function runtimeToReadableBinding(bindableProperties, textWithBindings) {
let temp = textWithBindings
const boundValues =
(typeof textWithBindings === "string" &&
textWithBindings.match(CAPTURE_VAR_INSIDE_MUSTACHE)) ||
[]
// Replace runtimeBindings with readableBindings:
boundValues.forEach(v => {
const binding = bindableProperties.find(({ runtimeBinding }) => {
return v === `{{ ${runtimeBinding} }}`
})
if (binding) {
temp = temp.replace(v, `{{ ${binding.readableBinding} }}`)
}
})
return temp
}

View File

@ -1,5 +1,3 @@
import mustache from "mustache"
import blockDefinitions from "components/workflow/WorkflowPanel/blockDefinitions"
import { generate } from "shortid" import { generate } from "shortid"
/** /**
@ -18,27 +16,31 @@ export default class Workflow {
addBlock(block) { addBlock(block) {
// Make sure to add trigger if doesn't exist // Make sure to add trigger if doesn't exist
if (!this.hasTrigger() && block.type === "TRIGGER") { if (!this.hasTrigger() && block.type === "TRIGGER") {
this.workflow.definition.trigger = { id: generate(), ...block } const trigger = { id: generate(), ...block }
return this.workflow.definition.trigger = trigger
return trigger
} }
this.workflow.definition.steps.push({ const newBlock = { id: generate(), ...block }
id: generate(), this.workflow.definition.steps = [
...block, ...this.workflow.definition.steps,
}) newBlock,
]
return newBlock
} }
updateBlock(updatedBlock, id) { updateBlock(updatedBlock, id) {
const { steps, trigger } = this.workflow.definition const { steps, trigger } = this.workflow.definition
if (trigger && trigger.id === id) { if (trigger && trigger.id === id) {
this.workflow.definition.trigger = null this.workflow.definition.trigger = updatedBlock
return return
} }
const stepIdx = steps.findIndex(step => step.id === id) const stepIdx = steps.findIndex(step => step.id === id)
if (stepIdx < 0) throw new Error("Block not found.") if (stepIdx < 0) throw new Error("Block not found.")
steps.splice(stepIdx, 1, updatedBlock) steps.splice(stepIdx, 1, updatedBlock)
this.workflow.definition.steps = steps
} }
deleteBlock(id) { deleteBlock(id) {
@ -52,44 +54,6 @@ export default class Workflow {
const stepIdx = steps.findIndex(step => step.id === id) const stepIdx = steps.findIndex(step => step.id === id)
if (stepIdx < 0) throw new Error("Block not found.") if (stepIdx < 0) throw new Error("Block not found.")
steps.splice(stepIdx, 1) steps.splice(stepIdx, 1)
} this.workflow.definition.steps = steps
createUiTree() {
if (!this.workflow.definition) return []
return Workflow.buildUiTree(this.workflow.definition)
}
static buildUiTree(definition) {
const steps = []
if (definition.trigger) steps.push(definition.trigger)
return [...steps, ...definition.steps].map(step => {
// The client side display definition for the block
const definition = blockDefinitions[step.type][step.actionId]
if (!definition) {
throw new Error(
`No block definition exists for the chosen block. Check there's an entry in the block definitions for ${step.actionId}`
)
}
if (!definition.params) {
throw new Error(
`Blocks should always have parameters. Ensure that the block definition is correct for ${step.actionId}`
)
}
const tagline = definition.tagline || ""
const args = step.args || {}
return {
id: step.id,
type: step.type,
params: step.params,
args,
heading: step.actionId,
body: mustache.render(tagline, args),
name: definition.name,
}
})
} }
} }

View File

@ -1,14 +1,22 @@
import { writable } from "svelte/store" import { writable } from "svelte/store"
import api from "../../api" import api from "../../api"
import Workflow from "./Workflow" import Workflow from "./Workflow"
import { cloneDeep } from "lodash/fp"
const workflowActions = store => ({ const workflowActions = store => ({
fetch: async () => { fetch: async () => {
const WORKFLOWS_URL = `/api/workflows` const responses = await Promise.all([
const workflowResponse = await api.get(WORKFLOWS_URL) api.get(`/api/workflows`),
const json = await workflowResponse.json() api.get(`/api/workflows/definitions/list`),
])
const jsonResponses = await Promise.all(responses.map(x => x.json()))
store.update(state => { store.update(state => {
state.workflows = json state.workflows = jsonResponses[0]
state.blockDefinitions = {
TRIGGER: jsonResponses[1].trigger,
ACTION: jsonResponses[1].action,
LOGIC: jsonResponses[1].logic,
}
return state return state
}) })
}, },
@ -23,8 +31,8 @@ const workflowActions = store => ({
const response = await api.post(CREATE_WORKFLOW_URL, workflow) const response = await api.post(CREATE_WORKFLOW_URL, workflow)
const json = await response.json() const json = await response.json()
store.update(state => { store.update(state => {
state.workflows = state.workflows.concat(json.workflow) state.workflows = [...state.workflows, json.workflow]
state.currentWorkflow = new Workflow(json.workflow) store.actions.select(json.workflow)
return state return state
}) })
}, },
@ -38,20 +46,7 @@ const workflowActions = store => ({
) )
state.workflows.splice(existingIdx, 1, json.workflow) state.workflows.splice(existingIdx, 1, json.workflow)
state.workflows = state.workflows state.workflows = state.workflows
state.currentWorkflow = new Workflow(json.workflow) store.actions.select(json.workflow)
return state
})
},
update: 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
return state return state
}) })
}, },
@ -66,28 +61,49 @@ const workflowActions = store => ({
) )
state.workflows.splice(existingIdx, 1) state.workflows.splice(existingIdx, 1)
state.workflows = state.workflows state.workflows = state.workflows
state.currentWorkflow = null state.selectedWorkflow = null
state.selectedBlock = null
return state 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 => { select: workflow => {
store.update(state => { store.update(state => {
state.currentWorkflow = new Workflow(workflow) state.selectedWorkflow = new Workflow(cloneDeep(workflow))
state.selectedWorkflowBlock = null state.selectedBlock = null
return state return state
}) })
}, },
addBlockToWorkflow: block => { addBlockToWorkflow: block => {
store.update(state => { store.update(state => {
state.currentWorkflow.addBlock(block) const newBlock = state.selectedWorkflow.addBlock(cloneDeep(block))
state.selectedWorkflowBlock = block state.selectedBlock = newBlock
return state return state
}) })
}, },
deleteWorkflowBlock: block => { deleteWorkflowBlock: block => {
store.update(state => { store.update(state => {
state.currentWorkflow.deleteBlock(block.id) const idx = state.selectedWorkflow.workflow.definition.steps.findIndex(
state.selectedWorkflowBlock = null 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 return state
}) })
}, },
@ -96,11 +112,14 @@ const workflowActions = store => ({
export const getWorkflowStore = () => { export const getWorkflowStore = () => {
const INITIAL_WORKFLOW_STATE = { const INITIAL_WORKFLOW_STATE = {
workflows: [], workflows: [],
blockDefinitions: {
TRIGGER: [],
ACTION: [],
LOGIC: [],
},
selectedWorkflow: null,
} }
const store = writable(INITIAL_WORKFLOW_STATE) const store = writable(INITIAL_WORKFLOW_STATE)
store.actions = workflowActions(store) store.actions = workflowActions(store)
return store return store
} }

View File

@ -1,44 +1,37 @@
import Workflow from "../Workflow"; import Workflow from "../Workflow"
import TEST_WORKFLOW from "./testWorkflow"; import TEST_WORKFLOW from "./testWorkflow"
const TEST_BLOCK = { const TEST_BLOCK = {
id: "VFWeZcIPx", id: "AUXJQGZY7",
name: "Update UI State", name: "Delay",
tagline: "Update <b>{{path}}</b> to <b>{{value}}</b>", icon: "ri-time-fill",
icon: "ri-refresh-line", tagline: "Delay for <b>{{time}}</b> milliseconds",
description: "Update your User Interface with some data.", description: "Delay the workflow until an amount of time has passed.",
environment: "CLIENT", params: { time: "number" },
params: { type: "LOGIC",
path: "string", args: { time: "5000" },
value: "longText", stepId: "DELAY",
},
args: {
path: "foo",
value: "started...",
},
actionId: "SET_STATE",
type: "ACTION",
} }
describe("Workflow Data Object", () => { describe("Workflow Data Object", () => {
let workflow let workflow
beforeEach(() => { beforeEach(() => {
workflow = new Workflow({ ...TEST_WORKFLOW }); workflow = new Workflow({ ...TEST_WORKFLOW })
}); })
it("adds a workflow block to the workflow", () => { it("adds a workflow block to the workflow", () => {
workflow.addBlock(TEST_BLOCK); workflow.addBlock(TEST_BLOCK)
expect(workflow.workflow.definition) expect(workflow.workflow.definition)
}) })
it("updates a workflow block with new attributes", () => { it("updates a workflow block with new attributes", () => {
const firstBlock = workflow.workflow.definition.steps[0]; const firstBlock = workflow.workflow.definition.steps[0]
const updatedBlock = { const updatedBlock = {
...firstBlock, ...firstBlock,
name: "UPDATED" name: "UPDATED",
}; }
workflow.updateBlock(updatedBlock, firstBlock.id); workflow.updateBlock(updatedBlock, firstBlock.id)
expect(workflow.workflow.definition.steps[0]).toEqual(updatedBlock) expect(workflow.workflow.definition.steps[0]).toEqual(updatedBlock)
}) })
@ -46,12 +39,10 @@ describe("Workflow Data Object", () => {
const { steps } = workflow.workflow.definition const { steps } = workflow.workflow.definition
const originalLength = steps.length const originalLength = steps.length
const lastBlock = steps[steps.length - 1]; const lastBlock = steps[steps.length - 1]
workflow.deleteBlock(lastBlock.id); workflow.deleteBlock(lastBlock.id)
expect(workflow.workflow.definition.steps.length).toBeLessThan(originalLength); expect(workflow.workflow.definition.steps.length).toBeLessThan(
}) originalLength
)
it("builds a tree that gets rendered in the flowchart builder", () => {
expect(Workflow.buildUiTree(TEST_WORKFLOW.definition)).toMatchSnapshot();
}) })
}) })

View File

@ -1,49 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Workflow Data Object builds a tree that gets rendered in the flowchart builder 1`] = `
Array [
Object {
"args": Object {
"time": 3000,
},
"body": "Delay for <b>3000</b> milliseconds",
"heading": "DELAY",
"id": "zJQcZUgDS",
"name": "Delay",
"params": Object {
"time": "number",
},
"type": "LOGIC",
},
Object {
"args": Object {
"path": "foo",
"value": "finished",
},
"body": "Update <b>foo</b> to <b>finished</b>",
"heading": "SET_STATE",
"id": "3RSTO7BMB",
"name": "Update UI State",
"params": Object {
"path": "string",
"value": "longText",
},
"type": "ACTION",
},
Object {
"args": Object {
"path": "foo",
"value": "started...",
},
"body": "Update <b>foo</b> to <b>started...</b>",
"heading": "SET_STATE",
"id": "VFWeZcIPx",
"name": "Update UI State",
"params": Object {
"path": "string",
"value": "longText",
},
"type": "ACTION",
},
]
`;

View File

@ -1,63 +1,78 @@
export default { export default {
_id: "53b6148c65d1429c987e046852d11611", name: "Test workflow",
_rev: "4-02c6659734934895812fa7be0215ee59",
name: "Test Workflow",
definition: { definition: {
steps: [ steps: [
{ {
id: "VFWeZcIPx", id: "ANBDINAPS",
name: "Update UI State", description: "Send an email.",
tagline: "Update <b>{{path}}</b> to <b>{{value}}</b>", tagline: "Send email to <b>{{to}}</b>",
icon: "ri-refresh-line", icon: "ri-mail-open-fill",
description: "Update your User Interface with some data.", name: "Send Email",
environment: "CLIENT",
params: { params: {
path: "string", to: "string",
value: "longText", from: "string",
subject: "longText",
text: "longText",
}, },
args: {
path: "foo",
value: "started...",
},
actionId: "SET_STATE",
type: "ACTION", type: "ACTION",
},
{
id: "zJQcZUgDS",
name: "Delay",
icon: "ri-time-fill",
tagline: "Delay for <b>{{time}}</b> milliseconds",
description: "Delay the workflow until an amount of time has passed.",
environment: "CLIENT",
params: {
time: "number",
},
args: { args: {
time: 3000, text: "A user was created!",
subject: "New Budibase User",
from: "budimaster@budibase.com",
to: "test@test.com",
}, },
actionId: "DELAY", stepId: "SEND_EMAIL",
type: "LOGIC",
},
{
id: "3RSTO7BMB",
name: "Update UI State",
tagline: "Update <b>{{path}}</b> to <b>{{value}}</b>",
icon: "ri-refresh-line",
description: "Update your User Interface with some data.",
environment: "CLIENT",
params: {
path: "string",
value: "longText",
},
args: {
path: "foo",
value: "finished",
},
actionId: "SET_STATE",
type: "ACTION",
}, },
], ],
trigger: {
id: "iRzYMOqND",
name: "Record Saved",
event: "record:save",
icon: "ri-save-line",
tagline: "Record is added to <b>{{model.name}}</b>",
description: "Fired when a record is saved to your database.",
params: { model: "model" },
type: "TRIGGER",
args: {
model: {
type: "model",
views: {},
name: "users",
schema: {
name: {
type: "string",
constraints: {
type: "string",
length: { maximum: 123 },
presence: { allowEmpty: false },
},
name: "name",
},
age: {
type: "number",
constraints: {
type: "number",
presence: { allowEmpty: false },
numericality: {
greaterThanOrEqualTo: "",
lessThanOrEqualTo: "",
},
},
name: "age",
},
},
_id: "c6b4e610cd984b588837bca27188a451",
_rev: "7-b8aa1ce0b53e88928bb88fc11bdc0aff",
},
},
stepId: "RECORD_SAVED",
},
}, },
type: "workflow", type: "workflow",
live: true, ok: true,
id: "b384f861f4754e1693835324a7fcca62",
rev: "1-aa1c2cbd868ef02e26f8fad531dd7e37",
live: false,
_id: "b384f861f4754e1693835324a7fcca62",
_rev: "108-4116829ec375e0481d0ecab9e83a2caf",
} }

View File

@ -12,6 +12,5 @@
</script> </script>
<div class="bb-margin-m"> <div class="bb-margin-m">
<Label small forAttr={'datepicker-label'}>{label}</Label>
<DatePicker placeholder={label} on:change={onChange} {value} /> <DatePicker placeholder={label} on:change={onChange} {value} />
</div> </div>

View File

@ -0,0 +1,12 @@
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24">
<path fill="none" d="M0 0h24v24H0z" />
<path
fill="currentColor"
d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10
10zm0-11.414L9.172 7.757 7.757 9.172 10.586 12l-2.829 2.828 1.415 1.415L12
13.414l2.828 2.829 1.415-1.415L13.414 12l2.829-2.828-1.415-1.415L12 10.586z" />
</svg>

After

Width:  |  Height:  |  Size: 412 B

View File

@ -32,3 +32,4 @@ export { default as TwitterIcon } from "./Twitter.svelte"
export { default as InfoIcon } from "./Info.svelte" export { default as InfoIcon } from "./Info.svelte"
export { default as CloseIcon } from "./Close.svelte" export { default as CloseIcon } from "./Close.svelte"
export { default as MoreIcon } from "./More.svelte" export { default as MoreIcon } from "./More.svelte"
export { default as CloseCircleIcon } from "./CloseCircle.svelte"

View File

@ -1,4 +0,0 @@
import feather from "feather-icons"
const getIcon = (icon, size) =>
feather.icons[icon].toSvg({ height: size || "16", width: size || "16" })
export default getIcon

View File

@ -37,14 +37,14 @@
} }
} }
$: paginatedData = data $: sort = $backendUiStore.sort
? data.slice( $: sorted = sort ? fsort(data)[sort.direction](sort.column) : data
$: paginatedData = sorted
? sorted.slice(
currentPage * ITEMS_PER_PAGE, currentPage * ITEMS_PER_PAGE,
currentPage * ITEMS_PER_PAGE + ITEMS_PER_PAGE currentPage * ITEMS_PER_PAGE + ITEMS_PER_PAGE
) )
: [] : []
$: sort = $backendUiStore.sort
$: sorted = sort ? fsort(data)[sort.direction](sort.column) : data
$: headers = Object.keys($backendUiStore.selectedModel.schema) $: headers = Object.keys($backendUiStore.selectedModel.schema)
.sort() .sort()
@ -79,10 +79,10 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#if sorted.length === 0} {#if paginatedData.length === 0}
<div class="no-data">No Data.</div> <div class="no-data">No Data.</div>
{/if} {/if}
{#each sorted as row} {#each paginatedData as row}
<tr> <tr>
<td> <td>
<EditRowPopover {row} /> <EditRowPopover {row} />
@ -103,7 +103,7 @@
<TablePagination <TablePagination
{data} {data}
bind:currentPage bind:currentPage
pageItemCount={data.length} pageItemCount={paginatedData.length}
{ITEMS_PER_PAGE} /> {ITEMS_PER_PAGE} />
</section> </section>

View File

@ -26,15 +26,15 @@
$: columns = schema ? Object.keys(schema) : [] $: columns = schema ? Object.keys(schema) : []
$: sort = $backendUiStore.sort
$: sorted = sort ? fsort(data)[sort.direction](sort.column) : data
$: paginatedData = $: paginatedData =
data && data.length sorted && sorted.length
? data.slice( ? sorted.slice(
currentPage * ITEMS_PER_PAGE, currentPage * ITEMS_PER_PAGE,
currentPage * ITEMS_PER_PAGE + ITEMS_PER_PAGE currentPage * ITEMS_PER_PAGE + ITEMS_PER_PAGE
) )
: [] : []
$: sort = $backendUiStore.sort
$: sorted = sort ? fsort(data)[sort.direction](sort.column) : data
</script> </script>
<section> <section>
@ -72,7 +72,7 @@
<TablePagination <TablePagination
{data} {data}
bind:currentPage bind:currentPage
pageItemCount={data.length} pageItemCount={paginatedData.length}
{ITEMS_PER_PAGE} /> {ITEMS_PER_PAGE} />
</section> </section>

View File

@ -71,7 +71,16 @@
} }
function addFilter() { function addFilter() {
view.filters = [...view.filters, {}] view.filters.push({})
view.filters = view.filters
}
function isMultipleChoice(field) {
return (
viewModel.schema[field].constraints &&
viewModel.schema[field].constraints.inclusion &&
viewModel.schema[field].constraints.inclusion.length
)
} }
</script> </script>
@ -108,10 +117,18 @@
<option value={condition.key}>{condition.name}</option> <option value={condition.key}>{condition.name}</option>
{/each} {/each}
</Select> </Select>
{#if filter.key && isMultipleChoice(filter.key)}
<Select secondary thin bind:value={filter.value}>
{#each viewModel.schema[filter.key].constraints.inclusion as option}
<option value={option}>{option}</option>
{/each}
</Select>
{:else}
<Input <Input
thin thin
placeholder={filter.key || fields[0]} placeholder={filter.key || fields[0]}
bind:value={filter.value} /> bind:value={filter.value} />
{/if}
<i class="ri-close-circle-fill" on:click={() => removeFilter(idx)} /> <i class="ri-close-circle-fill" on:click={() => removeFilter(idx)} />
{/each} {/each}
</div> </div>

View File

@ -9,7 +9,6 @@
CircleIndicator, CircleIndicator,
EventsIcon, EventsIcon,
} from "components/common/Icons/" } from "components/common/Icons/"
import EventsEditor from "./EventsEditor"
import panelStructure from "./temporaryPanelStructure.js" import panelStructure from "./temporaryPanelStructure.js"
import CategoryTab from "./CategoryTab.svelte" import CategoryTab from "./CategoryTab.svelte"
import DesignView from "./DesignView.svelte" import DesignView from "./DesignView.svelte"
@ -21,7 +20,6 @@
let categories = [ let categories = [
{ value: "settings", name: "Settings" }, { value: "settings", name: "Settings" },
{ value: "design", name: "Design" }, { value: "design", name: "Design" },
{ value: "events", name: "Events" },
] ]
let selectedCategory = categories[0] let selectedCategory = categories[0]
@ -37,8 +35,6 @@
c => c._component === componentInstance._component c => c._component === componentInstance._component
) || {} ) || {}
let panelDefinition = {}
$: panelDefinition = $: panelDefinition =
componentPropDefinition.properties && componentPropDefinition.properties &&
componentPropDefinition.properties[selectedCategory.value] componentPropDefinition.properties[selectedCategory.value]
@ -109,8 +105,6 @@
displayNameField={displayName} displayNameField={displayName}
onChange={onPropChanged} onChange={onPropChanged}
screenOrPageInstance={$store.currentView !== 'component' && $store.currentPreviewItem} /> screenOrPageInstance={$store.currentView !== 'component' && $store.currentPreviewItem} />
{:else if selectedCategory.value === 'events'}
<EventsEditor component={componentInstance} />
{/if} {/if}
</div> </div>

View File

@ -1,168 +1,220 @@
<script> <script>
import { store } from "builderStore" import { store } from "builderStore"
import { Button, Select } from "@budibase/bbui" import { TextButton, Button, Heading, DropdownMenu } from "@budibase/bbui"
import HandlerSelector from "./HandlerSelector.svelte" import { AddIcon, ArrowDownIcon } from "components/common/Icons/"
import ActionButton from "../../common/ActionButton.svelte"
import getIcon from "../../common/icon"
import { CloseIcon } from "components/common/Icons/"
import { EVENT_TYPE_MEMBER_NAME } from "../../common/eventHandlers" import { EVENT_TYPE_MEMBER_NAME } from "../../common/eventHandlers"
import actionTypes from "./actions"
import { createEventDispatcher } from "svelte"
const dispatch = createEventDispatcher()
export let event export let event
export let eventOptions = []
export let onClose
let eventType = "" let addActionButton
let addActionDropdown
let selectedAction
let draftEventHandler = { parameters: [] } let draftEventHandler = { parameters: [] }
$: eventData = event || { handlers: [] } $: actions = event || []
$: if (!eventOptions.includes(eventType) && eventOptions.length > 0) $: selectedActionComponent =
eventType = eventOptions[0].name selectedAction &&
actionTypes.find(t => t.name === selectedAction[EVENT_TYPE_MEMBER_NAME])
.component
const closeModal = () => { const closeModal = () => {
onClose() dispatch("close")
draftEventHandler = { parameters: [] } draftEventHandler = { parameters: [] }
eventData = { handlers: [] } actions = []
} }
const updateEventHandler = (updatedHandler, index) => { const updateEventHandler = (updatedHandler, index) => {
eventData.handlers[index] = updatedHandler actions[index] = updatedHandler
} }
const updateDraftEventHandler = updatedHandler => { const deleteAction = index => {
draftEventHandler = updatedHandler actions.splice(index, 1)
actions = actions
} }
const deleteEventHandler = index => { const addAction = actionType => () => {
eventData.handlers.splice(index, 1) const newAction = {
eventData = eventData
}
const createNewEventHandler = handler => {
const newHandler = handler || {
parameters: {}, parameters: {},
[EVENT_TYPE_MEMBER_NAME]: "", [EVENT_TYPE_MEMBER_NAME]: actionType.name,
} }
eventData.handlers.push(newHandler) actions.push(newAction)
eventData = eventData selectedAction = newAction
actions = actions
addActionDropdown.hide()
} }
const deleteEvent = () => { const selectAction = action => () => {
store.setComponentProp(eventType, []) selectedAction = action
closeModal()
} }
const saveEventData = () => { const saveEventData = () => {
store.setComponentProp(eventType, eventData.handlers) dispatch("change", actions)
closeModal() closeModal()
} }
</script> </script>
<div class="container"> <div class="root">
<div class="body">
<div class="heading"> <div class="header">
<h3> <Heading small dark>Actions</Heading>
{eventData.name ? `${eventData.name} Event` : 'Create a New Component Event'} <div bind:this={addActionButton}>
</h3> <TextButton text small blue on:click={addActionDropdown.show}>
Add Action
<div style="height: 20px; width: 20px;">
<AddIcon />
</div>
</TextButton>
</div>
<DropdownMenu
bind:this={addActionDropdown}
anchor={addActionButton}
align="right">
<div class="available-actions-container">
{#each actionTypes as actionType}
<div class="available-action" on:click={addAction(actionType)}>
<span>{actionType.name}</span>
</div> </div>
<div class="event-options">
<div class="section">
<h4>Event Type</h4>
<Select bind:value={eventType}>
{#each eventOptions as option}
<option value={option.name}>{option.name}</option>
{/each} {/each}
</Select>
</div> </div>
</DropdownMenu>
</div> </div>
<div class="section"> <div class="actions-container">
<h4>Event Action(s)</h4> {#if actions && actions.length > 0}
<HandlerSelector {#each actions as action, index}
newHandler <div class="action-container">
onChanged={updateDraftEventHandler} <div class="action-header" on:click={selectAction(action)}>
onCreate={() => { <p
createNewEventHandler(draftEventHandler) class="bb-body bb-body--small bb-body--color-dark"
draftEventHandler = { parameters: [] } style="margin: var(--spacing-s) 0;">
}} {index + 1}. {action[EVENT_TYPE_MEMBER_NAME]}
handler={draftEventHandler} /> </p>
<div class="row-expander" class:rotate={action !== selectedAction}>
<ArrowDownIcon />
</div> </div>
{#if eventData}
{#each eventData.handlers as handler, index}
<HandlerSelector
{index}
onChanged={updateEventHandler}
onRemoved={() => deleteEventHandler(index)}
{handler} />
{/each}
{/if}
</div> </div>
<div class="footer"> {#if action === selectedAction}
{#if eventData.name} <div class="selected-action-container">
<Button <svelte:component
outline this={selectedActionComponent}
on:click={deleteEvent} parameters={selectedAction.parameters} />
disabled={eventData.handlers.length === 0}> <div class="delete-action-button">
<TextButton text medium on:click={() => deleteAction(index)}>
Delete Delete
</Button> </TextButton>
</div>
</div>
{/if} {/if}
<div class="save">
<Button
primary
on:click={saveEventData}
disabled={eventData.handlers.length === 0}>
Save
</Button>
</div> </div>
{/each}
{/if}
</div> </div>
<div class="close-button" on:click={closeModal}>
<CloseIcon /> <div class="footer">
<a href="https://docs.budibase.com">Learn more about Actions</a>
<Button secondary on:click={closeModal}>Cancel</Button>
<Button primary on:click={saveEventData}>Save</Button>
</div> </div>
</div> </div>
<style> <style>
.container { .root {
position: relative; max-height: 50vh;
} width: 700px;
.heading { display: flex;
margin-bottom: 20px; flex-direction: column;
} }
.close-button { .header {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: var(--spacing-xl);
padding-bottom: 0;
}
.action-header {
display: flex;
flex-direction: row;
align-items: center;
}
.action-header > p {
flex: 1;
}
.row-expander {
height: 30px;
width: 30px;
}
.available-action {
padding: var(--spacing-s);
font-size: var(--font-size-m);
cursor: pointer; cursor: pointer;
position: absolute;
top: 20px;
right: 20px;
}
.close-button :global(svg) {
width: 24px;
height: 24px;
} }
h4 { .available-action:hover {
margin-bottom: 10px; background: var(--grey-2);
} }
h3 { .actions-container {
margin: 0; flex: 1;
font-size: 24px; min-height: 0px;
font-weight: bold; padding-bottom: var(--spacing-s);
padding-top: 0;
border: var(--border-light);
border-width: 0 0 1px 0;
overflow-y: auto;
} }
.body {
padding: 40px; .action-container {
display: grid; border: var(--border-light);
grid-gap: 20px; border-width: 1px 0 0 0;
padding-left: var(--spacing-xl);
padding-right: var(--spacing-xl);
padding-top: 0;
padding-bottom: 0;
} }
.footer {
.selected-action-container {
padding-bottom: var(--spacing-s);
padding-top: var(--spacing-s);
}
.delete-action-button {
padding-top: var(--spacing-l);
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
padding: 30px 40px; flex-direction: row;
border-bottom-left-radius: 5px;
border-bottom-right-radius: 50px;
background-color: var(--grey-1);
} }
.save {
margin-left: 20px; .footer {
display: flex;
flex-direction: row;
gap: var(--spacing-s);
padding: var(--spacing-xl);
padding-top: var(--spacing-m);
}
.footer > a {
flex: 1;
color: var(--grey-5);
font-size: var(--font-size-s);
text-decoration: none;
}
.footer > a:hover {
color: var(--blue);
}
.rotate :global(svg) {
transform: rotate(90deg);
} }
</style> </style>

View File

@ -1,25 +1,24 @@
<script> <script>
import { Button, DropdownMenu } from "@budibase/bbui" import { Button, Modal } from "@budibase/bbui"
import EventEditorModal from "./EventEditorModal.svelte" import EventEditorModal from "./EventEditorModal.svelte"
import { getContext } from "svelte" import { createEventDispatcher, onMount } from "svelte"
const dispatch = createEventDispatcher()
export let value export let value
export let name export let name
let button let eventsModal
let dropdown
</script> </script>
<div bind:this={button}> <Button secondary small on:click={eventsModal.show}>Define Actions</Button>
<Button secondary small on:click={dropdown.show}>Define Actions</Button>
</div> <Modal bind:this={eventsModal} maxWidth="100vw" hideCloseButton padding="0">
<DropdownMenu bind:this={dropdown} align="right" anchor={button}>
<EventEditorModal <EventEditorModal
event={value} event={value}
eventType={name} eventType={name}
on:change on:change
on:close={dropdown.hide} /> on:close={eventsModal.hide} />
</DropdownMenu> </Modal>
<style> <style>

View File

@ -1,5 +1,5 @@
<script> <script>
import { Input, Select } from "@budibase/bbui" import { Input, DataList, Select } from "@budibase/bbui"
import { find, map, keys, reduce, keyBy } from "lodash/fp" import { find, map, keys, reduce, keyBy } from "lodash/fp"
import { pipe } from "components/common/core" import { pipe } from "components/common/core"
import { EVENT_TYPE_MEMBER_NAME } from "components/common/eventHandlers" import { EVENT_TYPE_MEMBER_NAME } from "components/common/eventHandlers"
@ -29,12 +29,12 @@
{/each} {/each}
</Select> </Select>
{:else if parameter.name === 'url'} {:else if parameter.name === 'url'}
<Select editable on:change bind:value={parameter.value}> <DataList on:change bind:value={parameter.value}>
<option value="" /> <option value="" />
{#each $store.allScreens as screen} {#each $store.allScreens as screen}
<option value={screen.route}>{screen.props._instanceName}</option> <option value={screen.route}>{screen.props._instanceName}</option>
{/each} {/each}
</Select> </DataList>
{:else} {:else}
<Input <Input
name={parameter.name} name={parameter.name}

View File

@ -0,0 +1,75 @@
<script>
import { Select, Label } from "@budibase/bbui"
import { store, backendUiStore } from "builderStore"
import fetchBindableProperties from "builderStore/fetchBindableProperties"
import SaveFields from "./SaveFields.svelte"
export let parameters
$: bindableProperties = fetchBindableProperties({
componentInstanceId: $store.currentComponentInfo._id,
components: $store.components,
screen: $store.currentPreviewItem,
models: $backendUiStore.models,
})
// just wraps binding in {{ ... }}
const toBindingExpression = bindingPath => `{{ ${bindingPath} }}`
const modelFields = modelId => {
const model = $backendUiStore.models.find(m => m._id === modelId)
return Object.keys(model.schema).map(k => ({
name: k,
type: model.schema[k].type,
}))
}
$: schemaFields =
parameters && parameters.modelId ? modelFields(parameters.modelId) : []
const onFieldsChanged = e => {
parameters.fields = e.detail
}
</script>
<div class="root">
<Label size="m" color="dark">Table</Label>
<Select secondary bind:value={parameters.modelId}>
<option value="" />
{#each $backendUiStore.models as model}
<option value={model._id}>{model.name}</option>
{/each}
</Select>
{#if parameters.modelId}
<SaveFields
parameterFields={parameters.fields}
{schemaFields}
on:fieldschanged={onFieldsChanged} />
{/if}
</div>
<style>
.root {
display: grid;
column-gap: var(--spacing-s);
row-gap: var(--spacing-s);
grid-template-columns: auto 1fr auto 1fr auto;
align-items: baseline;
}
.root :global(.relative:nth-child(2)) {
grid-column-start: 2;
grid-column-end: 6;
}
.cannot-use {
color: var(--red);
font-size: var(--font-size-s);
text-align: center;
width: 70%;
margin: auto;
}
</style>

View File

@ -0,0 +1,29 @@
<script>
import { DataList, Label } from "@budibase/bbui"
import { store } from "builderStore"
export let parameters
</script>
<div class="root">
<Label size="m" color="dark">Screen</Label>
<DataList secondary bind:value={parameters.url}>
<option value="" />
{#each $store.screens as screen}
<option value={screen.route}>{screen.props._instanceName}</option>
{/each}
</DataList>
</div>
<style>
.root {
display: flex;
flex-direction: row;
align-items: baseline;
}
.root :global(.relative) {
flex: 1;
margin-left: var(--spacing-l);
}
</style>

View File

@ -0,0 +1,115 @@
<script>
// accepts an array of field names, and outputs an object of { FieldName: value }
import { DataList, Label, TextButton, Spacer, Select } from "@budibase/bbui"
import { store, backendUiStore } from "builderStore"
import fetchBindableProperties from "builderStore/fetchBindableProperties"
import { CloseCircleIcon, AddIcon } from "components/common/Icons"
import {
readableToRuntimeBinding,
runtimeToReadableBinding,
} from "builderStore/replaceBindings"
import { createEventDispatcher } from "svelte"
const dispatch = createEventDispatcher()
export let parameterFields
export let schemaFields
const emptyField = () => ({ name: "", value: "" })
// this statement initialises fields from parameters.fields
$: fields =
fields ||
Object.keys(parameterFields || { "": "" }).map(name => ({
name,
value:
(parameterFields &&
runtimeToReadableBinding(
bindableProperties,
parameterFields[name].value
)) ||
"",
}))
$: bindableProperties = fetchBindableProperties({
componentInstanceId: $store.currentComponentInfo._id,
components: $store.components,
screen: $store.currentPreviewItem,
models: $backendUiStore.models,
})
const addField = () => {
const newFields = fields.filter(f => f.name)
newFields.push(emptyField())
fields = newFields
rebuildParameters()
}
const removeField = field => () => {
fields = fields.filter(f => f !== field)
rebuildParameters()
}
const rebuildParameters = () => {
// rebuilds paramters.fields every time a field name or value is added
// as UI below is bound to "fields" array, but we need to output a { key: value }
const newParameterFields = {}
for (let field of fields) {
if (field.name) {
// value and type is needed by the client, so it can parse
// a string into a correct type
newParameterFields[field.name] = {
type: schemaFields.find(f => f.name === field.name).type,
value: readableToRuntimeBinding(bindableProperties, field.value),
}
}
}
dispatch("fieldschanged", newParameterFields)
}
// just wraps binding in {{ ... }}
const toBindingExpression = bindingPath => `{{ ${bindingPath} }}`
</script>
{#if fields}
{#each fields as field}
<Label size="m" color="dark">Field</Label>
<Select secondary bind:value={field.name} on:blur={rebuildParameters}>
<option value="" />
{#each schemaFields as schemaField}
<option value={schemaField.name}>{schemaField.name}</option>
{/each}
</Select>
<Label size="m" color="dark">Value</Label>
<DataList secondary bind:value={field.value} on:blur={rebuildParameters}>
<option value="" />
{#each bindableProperties as bindableProp}
<option value={toBindingExpression(bindableProp.readableBinding)}>
{bindableProp.readableBinding}
</option>
{/each}
</DataList>
<div class="remove-field-container">
<TextButton text small on:click={removeField(field)}>
<CloseCircleIcon />
</TextButton>
</div>
{/each}
<div>
<Spacer small />
<TextButton text small blue on:click={addField}>
Add Field
<div style="height: 20px; width: 20px;">
<AddIcon />
</div>
</TextButton>
</div>
{/if}
<style>
.remove-field-container :global(button) {
vertical-align: bottom;
}
</style>

View File

@ -0,0 +1,134 @@
<script>
import { Select, Label } from "@budibase/bbui"
import { store, backendUiStore } from "builderStore"
import fetchBindableProperties from "builderStore/fetchBindableProperties"
import SaveFields from "./SaveFields.svelte"
import {
readableToRuntimeBinding,
runtimeToReadableBinding,
} from "builderStore/replaceBindings"
export let parameters
$: bindableProperties = fetchBindableProperties({
componentInstanceId: $store.currentComponentInfo._id,
components: $store.components,
screen: $store.currentPreviewItem,
models: $backendUiStore.models,
})
let idFields
let recordId
$: {
idFields = bindableProperties.filter(
bindable =>
bindable.type === "context" && bindable.runtimeBinding.endsWith("._id")
)
// ensure recordId is always defaulted - there is usually only one option
if (idFields.length > 0 && !parameters._id) {
recordId = idFields[0].runtimeBinding
parameters = parameters
} else if (!recordId && parameters._id) {
recordId = parameters._id
.replace("{{", "")
.replace("}}", "")
.trim()
}
}
$: parameters._id = `{{ ${recordId} }}`
// just wraps binding in {{ ... }}
const toBindingExpression = bindingPath => `{{ ${bindingPath} }}`
// finds the selected idBinding, then reads the table/view
// from the component instance that it belongs to.
// then returns the field names for that schema
const schemaFromIdBinding = recordId => {
if (!recordId) return []
const idBinding = bindableProperties.find(
prop => prop.runtimeBinding === recordId
)
if (!idBinding) return []
const { instance } = idBinding
const component = $store.components[instance._component]
// component.context is the name of the prop that holds the modelId
const modelInfo = instance[component.context]
if (!modelInfo) return []
const model = $backendUiStore.models.find(m => m._id === modelInfo.modelId)
parameters.modelId = modelInfo.modelId
return Object.keys(model.schema).map(k => ({
name: k,
type: model.schema[k].type,
}))
}
let schemaFields
$: {
if (parameters && recordId) {
schemaFields = schemaFromIdBinding(recordId)
} else {
schemaFields = []
}
}
const onFieldsChanged = e => {
parameters.fields = e.detail
}
</script>
<div class="root">
{#if idFields.length === 0}
<div class="cannot-use">
Update record can only be used within a component that provides data, such
as a List
</div>
{:else}
<Label size="m" color="dark">Record Id</Label>
<Select secondary bind:value={recordId}>
<option value="" />
{#each idFields as idField}
<option value={idField.runtimeBinding}>
{idField.readableBinding}
</option>
{/each}
</Select>
{/if}
{#if recordId}
<SaveFields
parameterFields={parameters.fields}
{schemaFields}
on:fieldschanged={onFieldsChanged} />
{/if}
</div>
<style>
.root {
display: grid;
column-gap: var(--spacing-s);
row-gap: var(--spacing-s);
grid-template-columns: auto 1fr auto 1fr auto;
align-items: baseline;
}
.root :global(.relative:nth-child(2)) {
grid-column-start: 2;
grid-column-end: 6;
}
.cannot-use {
color: var(--red);
font-size: var(--font-size-s);
text-align: center;
width: 70%;
margin: auto;
}
</style>

View File

@ -0,0 +1,23 @@
import NavigateTo from "./NavigateTo.svelte"
import UpdateRecord from "./UpdateRecord.svelte"
import CreateRecord from "./CreateRecord.svelte"
// defines what actions are available, when adding a new one
// the component is the setup panel for the action
// NOTE that the "name" is used by the client library,
// so if you want to change it, you must change it client lib too
export default [
{
name: "Create Record",
component: CreateRecord,
},
{
name: "Navigate To",
component: NavigateTo,
},
{
name: "Update Record",
component: UpdateRecord,
},
]

View File

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

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

@ -3,6 +3,11 @@
import Input from "./PropertyPanelControls/Input.svelte" import Input from "./PropertyPanelControls/Input.svelte"
import { store, backendUiStore } from "builderStore" import { store, backendUiStore } from "builderStore"
import fetchBindableProperties from "builderStore/fetchBindableProperties" import fetchBindableProperties from "builderStore/fetchBindableProperties"
import {
readableToRuntimeBinding,
runtimeToReadableBinding,
CAPTURE_VAR_INSIDE_MUSTACHE,
} from "builderStore/replaceBindings"
import { DropdownMenu } from "@budibase/bbui" import { DropdownMenu } from "@budibase/bbui"
import BindingDropdown from "components/userInterface/BindingDropdown.svelte" import BindingDropdown from "components/userInterface/BindingDropdown.svelte"
import { onMount, getContext } from "svelte" import { onMount, getContext } from "svelte"
@ -36,25 +41,12 @@
}) })
} }
const CAPTURE_VAR_INSIDE_MUSTACHE = /{{([^}]+)}}/g
function replaceBindings(textWithBindings) { function replaceBindings(textWithBindings) {
getBindableProperties() getBindableProperties()
// Find all instances of mustasche textWithBindings = readableToRuntimeBinding(
const boundValues = textWithBindings.match(CAPTURE_VAR_INSIDE_MUSTACHE) bindableProperties,
textWithBindings
// Replace with names:
boundValues &&
boundValues.forEach(boundValue => {
const binding = bindableProperties.find(({ readableBinding }) => {
return boundValue === `{{ ${readableBinding} }}`
})
if (binding) {
textWithBindings = textWithBindings.replace(
boundValue,
`{{ ${binding.runtimeBinding} }}`
) )
}
})
onChange(key, textWithBindings) onChange(key, textWithBindings)
} }
@ -76,22 +68,10 @@
const safeValue = () => { const safeValue = () => {
getBindableProperties() getBindableProperties()
let temp = value
const boundValues =
(typeof value === "string" && value.match(CAPTURE_VAR_INSIDE_MUSTACHE)) ||
[]
// Replace with names: let temp = runtimeToReadableBinding(bindableProperties, value)
boundValues.forEach(v => {
const binding = bindableProperties.find(({ runtimeBinding }) => { return !value && props.defaultValue !== undefined
return v === `{{ ${runtimeBinding} }}`
})
if (binding) {
temp = temp.replace(v, `{{ ${binding.readableBinding} }}`)
}
})
// console.log(temp)
return value === undefined && props.defaultValue !== undefined
? props.defaultValue ? props.defaultValue
: temp : temp
} }
@ -113,7 +93,7 @@
{...props} {...props}
name={key} /> name={key} />
</div> </div>
{#if control == Input} {#if control === Input && !key.startsWith('_')}
<button data-cy={`${key}-binding-button`} on:click={dropdown.show}> <button data-cy={`${key}-binding-button`} on:click={dropdown.show}>
<Icon name="edit" /> <Icon name="edit" />
</button> </button>

View File

@ -1,5 +1,5 @@
<script> <script>
import { Select } from "@budibase/bbui" import { DataList } from "@budibase/bbui"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import { store } from "builderStore" import { store } from "builderStore"
@ -10,9 +10,9 @@
const handleBlur = () => dispatch("change", value) const handleBlur = () => dispatch("change", value)
</script> </script>
<Select editable secondary on:blur={handleBlur} on:change bind:value> <DataList editable secondary on:blur={handleBlur} on:change bind:value>
<option value="" /> <option value="" />
{#each $store.allScreens as screen} {#each $store.allScreens as screen}
<option value={screen.route}>{screen.props._instanceName}</option> <option value={screen.route}>{screen.props._instanceName}</option>
{/each} {/each}
</Select> </DataList>

View File

@ -116,7 +116,7 @@
control={definition.control} control={definition.control}
label={definition.label} label={definition.label}
key={definition.key} key={definition.key}
value={componentInstance[definition.key]} value={componentInstance[definition.key] || componentInstance[definition.key].defaultValue}
{componentInstance} {componentInstance}
{onChange} {onChange}
props={{ ...excludeProps(definition, ['control', 'label']) }} /> props={{ ...excludeProps(definition, ['control', 'label']) }} />

View File

@ -6,6 +6,8 @@ import ModelViewSelect from "components/userInterface/ModelViewSelect.svelte"
import ModelViewFieldSelect from "components/userInterface/ModelViewFieldSelect.svelte" import ModelViewFieldSelect from "components/userInterface/ModelViewFieldSelect.svelte"
import Event from "components/userInterface/EventsEditor/EventPropertyControl.svelte" import Event from "components/userInterface/EventsEditor/EventPropertyControl.svelte"
import ScreenSelect from "components/userInterface/ScreenSelect.svelte" import ScreenSelect from "components/userInterface/ScreenSelect.svelte"
import { IconSelect } from "components/userInterface/IconSelect"
import Colorpicker from "@budibase/colorpicker"
import { all } from "./propertyCategories.js" import { all } from "./propertyCategories.js"
/* /*
@ -220,16 +222,41 @@ export default {
settings: [{ label: "URL", key: "url", control: Input }], settings: [{ label: "URL", key: "url", control: Input }],
}, },
}, },
// { {
// _component: "@budibase/standard-components/icon", _component: "@budibase/standard-components/icon",
// name: "Icon", name: "Icon",
// description: "A basic component for displaying icons", description: "A basic component for displaying icons",
// icon: "ri-sun-fill", icon: "ri-sun-fill",
// children: [], children: [],
// properties: { properties: {
// design: { ...all }, 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", _component: "@budibase/standard-components/link",
name: "Link", name: "Link",
@ -515,10 +542,30 @@ export default {
key: "datasource", key: "datasource",
control: ModelViewSelect, control: ModelViewSelect,
}, },
{ label: "Stripe Color", key: "stripeColor", control: Input }, {
{ label: "Border Color", key: "borderColor", control: Input }, label: "Stripe Color",
{ label: "TH Color", key: "backgroundColor", control: Input }, key: "stripeColor",
{ label: "TH Font Color", key: "color", control: Input }, control: Colorpicker,
defaultValue: "#FFFFFF",
},
{
label: "Border Color",
key: "borderColor",
control: Colorpicker,
defaultValue: "#FFFFFF",
},
{
label: "TH Color",
key: "backgroundColor",
control: Colorpicker,
defaultValue: "#FFFFFF",
},
{
label: "TH Font Color",
key: "color",
control: Colorpicker,
defaultValue: "#FFFFFF",
},
{ label: "Table", key: "model", control: ModelSelect }, { label: "Table", key: "model", control: ModelSelect },
], ],
}, },

View File

@ -13,7 +13,7 @@
async function deleteWorkflow() { async function deleteWorkflow() {
await workflowStore.actions.delete({ await workflowStore.actions.delete({
instanceId, instanceId,
workflow: $workflowStore.currentWorkflow.workflow, workflow: $workflowStore.selectedWorkflow.workflow,
}) })
onClosed() onClosed()
notifier.danger("Workflow deleted.") notifier.danger("Workflow deleted.")

View File

@ -1,42 +0,0 @@
<script>
import { store } from "builderStore"
import deepmerge from "deepmerge"
import { Label } from "@budibase/bbui"
export let value
let pages = []
let components = []
let pageName
let selectedPage
let selectedScreen
$: pages = $store.pages
$: selectedPage = pages[pageName]
$: screens = selectedPage ? selectedPage._screens : []
$: if (selectedPage) {
let result = selectedPage
for (screen of screens) {
result = deepmerge(result, screen)
}
components = result.props._children
}
</script>
<div class="bb-margin-xl block-field">
<Label small forAttr={'page'}>Page</Label>
<select class="budibase__input" bind:value={pageName}>
{#each Object.keys(pages) as page}
<option value={page}>{page}</option>
{/each}
</select>
{#if components.length > 0}
<Label small forAttr={'component'}>Component</Label>
<select class="budibase__input" bind:value>
{#each components as component}
<option value={component._id}>{component._id}</option>
{/each}
</select>
{/if}
</div>

View File

@ -2,13 +2,22 @@
import { backendUiStore } from "builderStore" import { backendUiStore } from "builderStore"
export let value export let value
$: modelId = value ? value._id : ""
function onChange(e) {
value = $backendUiStore.models.find(model => model._id === e.target.value)
}
</script> </script>
<div class="bb-margin-xl block-field"> <div class="block-field">
<select class="budibase__input" bind:value> <select
<option value="" /> class="budibase__input"
value={modelId}
on:blur={onChange}
on:change={onChange}>
<option value="">Choose an option</option>
{#each $backendUiStore.models as model} {#each $backendUiStore.models as model}
<option value={model}>{model.name}</option> <option value={model._id}>{model.name}</option>
{/each} {/each}
</select> </select>
</div> </div>

View File

@ -3,6 +3,14 @@
import { Input, Label } from "@budibase/bbui" import { Input, Label } from "@budibase/bbui"
export let value export let value
$: modelId = value && value.model ? value.model._id : ""
$: schemaFields = Object.keys(value && value.model ? value.model.schema : {})
function onChangeModel(e) {
value.model = $backendUiStore.models.find(
model => model._id === e.target.value
)
}
function setParsedValue(evt, field) { function setParsedValue(evt, field) {
const fieldSchema = value.model.schema[field] const fieldSchema = value.model.schema[field]
@ -10,23 +18,27 @@
value[field] = parseInt(evt.target.value) value[field] = parseInt(evt.target.value)
return return
} }
value[field] = evt.target.value value[field] = evt.target.value
} }
</script> </script>
<div class="bb-margin-xl block-field"> <div class="block-field">
<select class="budibase__input" bind:value={value.model}> <select
class="budibase__input"
value={modelId}
on:blur={onChangeModel}
on:change={onChangeModel}>
<option value="">Choose an option</option>
{#each $backendUiStore.models as model} {#each $backendUiStore.models as model}
<option value={model}>{model.name}</option> <option value={model._id}>{model.name}</option>
{/each} {/each}
</select> </select>
</div> </div>
{#if value.model} {#if schemaFields.length}
<div class="bb-margin-xl block-field"> <div class="bb-margin-xl block-field">
<Label small forAttr={'fields'}>Fields</Label> <Label small forAttr={'fields'}>Fields</Label>
{#each Object.keys(value.model.schema) as field} {#each schemaFields as field}
<div class="bb-margin-xl"> <div class="bb-margin-xl">
<Input <Input
thin thin

View File

@ -1,6 +1,5 @@
<script> <script>
import { fade } from "svelte/transition" import { getContext } from "svelte"
import { onMount, getContext } from "svelte"
import { backendUiStore, workflowStore } from "builderStore" import { backendUiStore, workflowStore } from "builderStore"
import { notifier } from "builderStore/store/notifications" import { notifier } from "builderStore/store/notifications"
import WorkflowBlockSetup from "./WorkflowBlockSetup.svelte" import WorkflowBlockSetup from "./WorkflowBlockSetup.svelte"
@ -9,49 +8,35 @@
const { open, close } = getContext("simple-modal") const { open, close } = getContext("simple-modal")
const ACCESS_LEVELS = [
{
name: "Admin",
key: "ADMIN",
canExecute: true,
editable: false,
},
{
name: "Power User",
key: "POWER_USER",
canExecute: true,
editable: false,
},
]
let selectedTab = "SETUP" let selectedTab = "SETUP"
let testResult
$: workflow = $: workflow =
$workflowStore.currentWorkflow && $workflowStore.currentWorkflow.workflow $workflowStore.selectedWorkflow && $workflowStore.selectedWorkflow.workflow
$: workflowBlock = $workflowStore.selectedWorkflowBlock
function deleteWorkflow() { function deleteWorkflow() {
open( open(
DeleteWorkflowModal, DeleteWorkflowModal,
{ { onClosed: close },
onClosed: close,
},
{ styleContent: { padding: "0" } } { styleContent: { padding: "0" } }
) )
} }
function deleteWorkflowBlock() { function deleteWorkflowBlock() {
workflowStore.actions.deleteWorkflowBlock(workflowBlock) workflowStore.actions.deleteWorkflowBlock($workflowStore.selectedBlock)
notifier.info("Workflow block deleted.")
} }
function testWorkflow() { async function testWorkflow() {
testResult = "PASSED" const result = await workflowStore.actions.trigger({
workflow: $workflowStore.selectedWorkflow.workflow,
})
if (result.status === 200) {
notifier.success(`Workflow ${workflow.name} triggered successfully.`)
} else {
notifier.danger(`Failed to trigger workflow ${workflow.name}.`)
}
} }
async function saveWorkflow() { async function saveWorkflow() {
const workflow = $workflowStore.currentWorkflow.workflow
await workflowStore.actions.save({ await workflowStore.actions.save({
instanceId: $backendUiStore.selectedDatabase._id, instanceId: $backendUiStore.selectedDatabase._id,
workflow, workflow,
@ -65,68 +50,27 @@
<span <span
class="hoverable" class="hoverable"
class:selected={selectedTab === 'SETUP'} class:selected={selectedTab === 'SETUP'}
on:click={() => { on:click={() => (selectedTab = 'SETUP')}>
selectedTab = 'SETUP'
testResult = null
}}>
Setup Setup
</span> </span>
{#if !workflowBlock}
<span
class="test-tab"
class:selected={selectedTab === 'TEST'}
on:click={() => (selectedTab = 'TEST')}>
Test
</span>
{/if}
</header> </header>
{#if selectedTab === 'TEST'} {#if $workflowStore.selectedBlock}
<div class="bb-margin-m"> <WorkflowBlockSetup bind:block={$workflowStore.selectedBlock} />
{#if testResult}
<button
transition:fade
class:passed={testResult === 'PASSED'}
class:failed={testResult === 'FAILED'}
class="test-result">
{testResult}
</button>
{/if}
<Button secondary wide on:click={testWorkflow}>Test Workflow</Button>
</div>
{/if}
{#if selectedTab === 'SETUP'}
{#if workflowBlock}
<WorkflowBlockSetup {workflowBlock} />
<div class="buttons"> <div class="buttons">
<Button <Button green wide data-cy="save-workflow-setup" on:click={saveWorkflow}>
green
wide
data-cy="save-workflow-setup"
on:click={saveWorkflow}>
Save Workflow Save Workflow
</Button> </Button>
<Button red wide on:click={deleteWorkflowBlock}>Delete Block</Button> <Button red wide on:click={deleteWorkflowBlock}>Delete Block</Button>
</div> </div>
{:else if $workflowStore.currentWorkflow} {:else if $workflowStore.selectedWorkflow}
<div class="panel"> <div class="panel">
<div class="panel-body"> <div class="panel-body">
<div class="block-label">Workflow: {workflow.name}</div> <div class="block-label">
<div class="config-item"> Workflow
<Label small forAttr={'useraccess'}>User Access</Label> <b>{workflow.name}</b>
<div class="access-levels">
{#each ACCESS_LEVELS as level}
<span class="access-level">
<label>{level.name}</label>
<input
type="checkbox"
disabled={!level.editable}
bind:checked={level.canExecute} />
</span>
{/each}
</div>
</div> </div>
</div> </div>
<Button secondary wide on:click={testWorkflow}>Test Workflow</Button>
<div class="buttons"> <div class="buttons">
<Button <Button
green green
@ -139,7 +83,6 @@
</div> </div>
</div> </div>
{/if} {/if}
{/if}
</section> </section>
<style> <style>
@ -181,10 +124,6 @@
margin-bottom: 20px; margin-bottom: 20px;
} }
.config-item {
margin-bottom: 20px;
}
header > span { header > span {
color: var(--grey-5); color: var(--grey-5);
margin-right: 20px; margin-right: 20px;
@ -205,35 +144,8 @@
gap: 12px; gap: 12px;
} }
.access-level {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 20px;
}
.access-level label { .access-level label {
font-weight: normal; font-weight: normal;
color: var(--ink); color: var(--ink);
} }
.test-result {
border: none;
width: 100%;
border-radius: 3px;
height: 32px;
font-size: 14px;
font-weight: 500;
color: var(--white);
text-align: center;
margin-bottom: 10px;
}
.passed {
background: var(--green);
}
.failed {
background: var(--red);
}
</style> </style>

View File

@ -1,55 +1,45 @@
<script> <script>
import { backendUiStore, store } from "builderStore"
import ComponentSelector from "./ParamInputs/ComponentSelector.svelte"
import ModelSelector from "./ParamInputs/ModelSelector.svelte" import ModelSelector from "./ParamInputs/ModelSelector.svelte"
import RecordSelector from "./ParamInputs/RecordSelector.svelte" import RecordSelector from "./ParamInputs/RecordSelector.svelte"
import { Input, TextArea, Select } from "@budibase/bbui" import { Input, TextArea, Select } from "@budibase/bbui"
export let workflowBlock export let block
$: params = block.params ? Object.entries(block.params) : []
let params
$: workflowParams = workflowBlock.params
? Object.entries(workflowBlock.params)
: []
</script> </script>
<label class="selected-label">{workflowBlock.type}: {workflowBlock.name}</label> <div class="container">
{#each workflowParams as [parameter, type]} <div class="selected-label">{block.name}</div>
{#each params as [parameter, type]}
<div class="block-field"> <div class="block-field">
<label class="label">{parameter}</label> <label class="label">{parameter}</label>
{#if Array.isArray(type)} {#if Array.isArray(type)}
<Select bind:value={workflowBlock.args[parameter]} thin> <Select bind:value={block.args[parameter]} thin secondary>
<option value="">Choose an option</option>
{#each type as option} {#each type as option}
<option value={option}>{option}</option> <option value={option}>{option}</option>
{/each} {/each}
</Select> </Select>
{:else if type === 'component'}
<ComponentSelector bind:value={workflowBlock.args[parameter]} />
{:else if type === 'accessLevel'} {:else if type === 'accessLevel'}
<Select bind:value={workflowBlock.args[parameter]} thin> <Select bind:value={block.args[parameter]} thin secondary>
<option value="ADMIN">Admin</option> <option value="ADMIN">Admin</option>
<option value="POWER_USER">Power User</option> <option value="POWER_USER">Power User</option>
</Select> </Select>
{:else if type === 'password'} {:else if type === 'password'}
<Input type="password" thin bind:value={workflowBlock.args[parameter]} /> <Input type="password" thin bind:value={block.args[parameter]} />
{:else if type === 'number'} {:else if type === 'number'}
<Input type="number" thin bind:value={workflowBlock.args[parameter]} /> <Input type="number" thin bind:value={block.args[parameter]} />
{:else if type === 'longText'} {:else if type === 'longText'}
<TextArea <TextArea type="text" thin bind:value={block.args[parameter]} />
type="text"
thin
bind:value={workflowBlock.args[parameter]}
label="" />
{:else if type === 'model'} {:else if type === 'model'}
<ModelSelector bind:value={workflowBlock.args[parameter]} /> <ModelSelector bind:value={block.args[parameter]} />
{:else if type === 'record'} {:else if type === 'record'}
<RecordSelector value={workflowBlock.args[parameter]} /> <RecordSelector bind:value={block.args[parameter]} />
{:else if type === 'string'} {:else if type === 'string'}
<Input type="text" thin bind:value={workflowBlock.args[parameter]} /> <Input type="text" thin bind:value={block.args[parameter]} />
{/if} {/if}
</div> </div>
{/each} {/each}
</div>
<style> <style>
.block-field { .block-field {
@ -57,16 +47,19 @@
} }
label { label {
text-transform: capitalize;
font-size: 14px; font-size: 14px;
font-family: sans-serif;
font-weight: 500; font-weight: 500;
color: var(--ink);
margin-bottom: 12px;
text-transform: capitalize;
margin-top: 20px; margin-top: 20px;
} }
.selected-label { .selected-label {
text-transform: capitalize;
font-size: 14px;
font-weight: 500; font-weight: 500;
font-size: 14px;
color: var(--grey-7);
} }
textarea { textarea {

View File

@ -1,30 +1,22 @@
<script> <script>
import { onMount } from "svelte" import { afterUpdate } from "svelte"
import { workflowStore, backendUiStore } from "builderStore" import { workflowStore, backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications" import { notifier } from "builderStore/store/notifications"
import Flowchart from "./flowchart/FlowChart.svelte" import Flowchart from "./flowchart/FlowChart.svelte"
let selectedWorkflow $: workflow =
let uiTree $workflowStore.selectedWorkflow && $workflowStore.selectedWorkflow.workflow
let instanceId = $backendUiStore.selectedDatabase._id $: workflowLive = workflow && workflow.live
$: selectedWorkflow = $workflowStore.currentWorkflow
$: workflowLive = selectedWorkflow && selectedWorkflow.workflow.live
$: uiTree = selectedWorkflow ? selectedWorkflow.createUiTree() : []
$: instanceId = $backendUiStore.selectedDatabase._id $: instanceId = $backendUiStore.selectedDatabase._id
function onSelect(block) { function onSelect(block) {
workflowStore.update(state => { workflowStore.update(state => {
state.selectedWorkflowBlock = block state.selectedBlock = block
return state return state
}) })
} }
function setWorkflowLive(live) { function setWorkflowLive(live) {
const { workflow } = selectedWorkflow
workflow.live = live workflow.live = live
workflowStore.actions.save({ instanceId, workflow }) workflowStore.actions.save({ instanceId, workflow })
if (live) { if (live) {
@ -36,9 +28,10 @@
</script> </script>
<section> <section>
<Flowchart blocks={uiTree} {onSelect} /> <Flowchart {workflow} {onSelect} />
<footer> </section>
{#if selectedWorkflow} <footer>
{#if workflow}
<button <button
class:highlighted={workflowLive} class:highlighted={workflowLive}
class:hoverable={workflowLive} class:hoverable={workflowLive}
@ -54,14 +47,23 @@
<i class="ri-play-fill" /> <i class="ri-play-fill" />
</button> </button>
{/if} {/if}
</footer> </footer>
</section>
<style> <style>
section {
display: flex;
flex-direction: row;
justify-content: center;
align-items: flex-start;
overflow: auto;
height: 100%;
position: relative;
}
footer { footer {
position: absolute; position: absolute;
bottom: 0; bottom: 20px;
right: 0; right: 30px;
display: flex; display: flex;
align-items: flex-end; align-items: flex-end;
} }
@ -77,7 +79,9 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin-right: 24px; }
footer > button:first-child {
margin-right: 20px;
} }
.play-button.highlighted { .play-button.highlighted {

View File

@ -7,3 +7,9 @@
<path d="M5.0625 70H9L4.5 75L0 70H3.9375V65H5.0625V70Z" fill="#ADAEC4" /> <path d="M5.0625 70H9L4.5 75L0 70H3.9375V65H5.0625V70Z" fill="#ADAEC4" />
<rect x="4" width="1" height="65" fill="#ADAEC4" /> <rect x="4" width="1" height="65" fill="#ADAEC4" />
</svg> </svg>
<style>
svg {
margin: 8px 0;
}
</style>

Before

Width:  |  Height:  |  Size: 241 B

After

Width:  |  Height:  |  Size: 290 B

View File

@ -1,24 +1,52 @@
<script> <script>
import FlowItem from "./FlowItem.svelte" import FlowItem from "./FlowItem.svelte"
import Arrow from "./Arrow.svelte" import Arrow from "./Arrow.svelte"
import { flip } from "svelte/animate"
import { fade, fly } from "svelte/transition"
export let blocks = [] export let workflow
export let onSelect export let onSelect
let blocks
$: {
blocks = []
if (workflow) {
if (workflow.definition.trigger) {
blocks.push(workflow.definition.trigger)
}
blocks = blocks.concat(workflow.definition.steps || [])
}
}
</script> </script>
<section class="canvas"> <section class="canvas">
{#each blocks as block, idx} {#each blocks as block, idx (block.id)}
<div
class="block"
animate:flip={{ duration: 600 }}
in:fade|local
out:fly|local={{ x: 100 }}>
<FlowItem {onSelect} {block} /> <FlowItem {onSelect} {block} />
{#if idx !== blocks.length - 1} {#if idx !== blocks.length - 1}
<Arrow /> <Arrow />
{/if} {/if}
</div>
{/each} {/each}
</section> </section>
<style> <style>
.canvas { section {
position: absolute;
padding: 20px 40px;
display: flex; display: flex;
align-items: center; align-items: center;
flex-direction: column; flex-direction: column;
} }
.block {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
}
</style> </style>

View File

@ -1,15 +1,21 @@
<script> <script>
import { fade } from "svelte/transition" import mustache from "mustache"
import { workflowStore } from "builderStore"
export let onSelect export let onSelect
export let block export let block
let selected
$: selected =
$workflowStore.selectedBlock != null &&
$workflowStore.selectedBlock.id === block.id
function selectBlock() { function selectBlock() {
onSelect(block) onSelect(block)
} }
</script> </script>
<div transition:fade class={`${block.type} hoverable`} on:click={selectBlock}> <div class={`${block.type} hoverable`} class:selected on:click={selectBlock}>
<header> <header>
{#if block.type === 'TRIGGER'} {#if block.type === 'TRIGGER'}
<i class="ri-lightbulb-fill" /> <i class="ri-lightbulb-fill" />
@ -24,7 +30,7 @@
</header> </header>
<hr /> <hr />
<p> <p>
{@html block.body} {@html mustache.render(block.tagline, block.args)}
</p> </p>
</div> </div>
@ -32,8 +38,8 @@
div { div {
width: 320px; width: 320px;
padding: 20px; padding: 20px;
border-radius: 5px; border-radius: var(--border-radius-m);
transition: 0.3s all; transition: 0.3s all ease;
box-shadow: 0 4px 30px 0 rgba(57, 60, 68, 0.08); box-shadow: 0 4px 30px 0 rgba(57, 60, 68, 0.08);
background-color: var(--ink); background-color: var(--ink);
font-size: 16px; font-size: 16px;
@ -69,9 +75,12 @@
p { p {
color: inherit; color: inherit;
margin-bottom: 0;
} }
div.selected,
div:hover { div:hover {
transform: scale(1.05); transform: scale(1.1);
box-shadow: 0 4px 30px 0 rgba(57, 60, 68, 0.15);
} }
</style> </style>

View File

@ -1,82 +1,39 @@
<script> <script>
import { onMount } from "svelte" import { workflowStore } from "builderStore"
import { backendUiStore, workflowStore } from "builderStore"
import { WorkflowList } from "../"
import WorkflowBlock from "./WorkflowBlock.svelte" import WorkflowBlock from "./WorkflowBlock.svelte"
import blockDefinitions from "../blockDefinitions" import FlatButtonGroup from "components/userInterface/FlatButtonGroup.svelte"
let selectedTab = "TRIGGER" let selectedTab = "TRIGGER"
let definitions = [] let buttonProps = []
$: blocks = Object.entries($workflowStore.blockDefinitions[selectedTab])
$: definitions = Object.entries(blockDefinitions[selectedTab])
$: { $: {
if ( if ($workflowStore.selectedWorkflow.hasTrigger()) {
$workflowStore.currentWorkflow.hasTrigger() && buttonProps = [
selectedTab === "TRIGGER" { value: "ACTION", text: "Action" },
) { { value: "LOGIC", text: "Logic" },
]
if (selectedTab === "TRIGGER") {
selectedTab = "ACTION" selectedTab = "ACTION"
} }
} else {
buttonProps = [{ value: "TRIGGER", text: "Trigger" }]
if (selectedTab !== "TRIGGER") {
selectedTab = "TRIGGER"
}
}
}
function onChangeTab(tab) {
selectedTab = tab
} }
</script> </script>
<section> <section>
<div class="subtabs"> <FlatButtonGroup value={selectedTab} {buttonProps} onChange={onChangeTab} />
{#if !$workflowStore.currentWorkflow.hasTrigger()}
<span
class="hoverable"
class:selected={'TRIGGER' === selectedTab}
on:click={() => (selectedTab = 'TRIGGER')}>
Triggers
</span>
{/if}
<span
class="hoverable"
class:selected={'ACTION' === selectedTab}
on:click={() => (selectedTab = 'ACTION')}>
Actions
</span>
<span
class="hoverable"
class:selected={'LOGIC' === selectedTab}
on:click={() => (selectedTab = 'LOGIC')}>
Logic
</span>
</div>
<div id="blocklist"> <div id="blocklist">
{#each definitions as [actionId, blockDefinition]} {#each blocks as [stepId, blockDefinition]}
<WorkflowBlock {blockDefinition} {actionId} blockType={selectedTab} /> <WorkflowBlock {blockDefinition} {stepId} blockType={selectedTab} />
{/each} {/each}
</div> </div>
</section> </section>
<style>
.subtabs {
margin-top: 20px;
display: grid;
grid-auto-flow: column;
grid-auto-columns: 1fr 1fr 1fr;
margin-bottom: 12px;
}
.subtabs span {
transition: 0.3s all;
text-align: center;
color: var(--grey-7);
font-weight: 400;
padding: 8px 16px;
text-rendering: optimizeLegibility;
border: none !important;
outline: none;
}
.subtabs span.selected {
background: var(--grey-3);
color: var(--ink);
border-radius: 5px;
}
.subtabs span:not(.selected) {
color: var(--ink);
}
</style>

View File

@ -1,15 +1,15 @@
<script> <script>
import { workflowStore } from "builderStore" import { workflowStore } from "builderStore"
export let blockType
export let blockDefinition export let blockDefinition
export let actionId export let stepId
export let blockType
function addBlockToWorkflow() { function addBlockToWorkflow() {
workflowStore.actions.addBlockToWorkflow({ workflowStore.actions.addBlockToWorkflow({
...blockDefinition, ...blockDefinition,
args: blockDefinition.args || {}, args: blockDefinition.args || {},
actionId, stepId,
type: blockType, type: blockType,
}) })
} }
@ -18,7 +18,7 @@
<div <div
class="workflow-block hoverable" class="workflow-block hoverable"
on:click={addBlockToWorkflow} on:click={addBlockToWorkflow}
data-cy={actionId}> data-cy={stepId}>
<div> <div>
<i class={blockDefinition.icon} /> <i class={blockDefinition.icon} />
</div> </div>
@ -31,11 +31,11 @@
<style> <style>
.workflow-block { .workflow-block {
display: grid; display: grid;
grid-template-columns: 40px auto; grid-template-columns: 20px auto;
align-items: center; align-items: center;
margin-top: 16px; margin-top: 16px;
padding: 16px 0px; padding: 12px;
border-radius: var(--border); border-radius: var(--border-radius-m);
} }
.workflow-block:hover { .workflow-block:hover {
@ -43,7 +43,7 @@
} }
.workflow-text { .workflow-text {
margin-left: 12px; margin-left: 16px;
} }
.icon { .icon {
@ -64,6 +64,7 @@
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
margin-bottom: 5px; margin-bottom: 5px;
margin-top: 0;
} }
p { p {

View File

@ -8,9 +8,9 @@
const { open, close } = getContext("simple-modal") const { open, close } = getContext("simple-modal")
$: currentWorkflowId = $: selectedWorkflowId =
$workflowStore.currentWorkflow && $workflowStore.selectedWorkflow &&
$workflowStore.currentWorkflow.workflow._id $workflowStore.selectedWorkflow.workflow._id
function newWorkflow() { function newWorkflow() {
open( open(
@ -33,7 +33,7 @@
{#each $workflowStore.workflows as workflow} {#each $workflowStore.workflows as workflow}
<li <li
class="workflow-item" class="workflow-item"
class:selected={workflow._id === currentWorkflowId} class:selected={workflow._id === selectedWorkflowId}
on:click={() => workflowStore.actions.select(workflow)}> on:click={() => workflowStore.actions.select(workflow)}>
<i class="ri-stackshare-line" class:live={workflow.live} /> <i class="ri-stackshare-line" class:live={workflow.live} />
{workflow.name} {workflow.name}

View File

@ -1,11 +1,9 @@
<script> <script>
import { onMount } from "svelte" import { workflowStore } from "builderStore"
import { backendUiStore, workflowStore } from "builderStore" import WorkflowList from "./WorkflowList/WorkflowList.svelte"
import { WorkflowList, BlockList } from "./" import BlockList from "./BlockList/BlockList.svelte"
import blockDefinitions from "./blockDefinitions"
let selectedTab = "WORKFLOWS" let selectedTab = "WORKFLOWS"
let definitions = []
</script> </script>
<header> <header>
@ -16,7 +14,7 @@
on:click={() => (selectedTab = 'WORKFLOWS')}> on:click={() => (selectedTab = 'WORKFLOWS')}>
Workflows Workflows
</span> </span>
{#if $workflowStore.currentWorkflow} {#if $workflowStore.selectedWorkflow}
<span <span
data-cy="add-workflow-component" data-cy="add-workflow-component"
class="hoverable" class="hoverable"

View File

@ -12,11 +12,13 @@
<div class="content"> <div class="content">
<slot /> <slot />
</div> </div>
{#if $workflowStore.selectedWorkflow}
<div class="nav"> <div class="nav">
<div class="inner"> <div class="inner">
<SetupPanel /> <SetupPanel />
</div> </div>
</div> </div>
{/if}
</div> </div>
<style> <style>
@ -35,13 +37,11 @@
.content { .content {
flex: 1 1 auto; flex: 1 1 auto;
margin: 20px 40px;
} }
.nav { .nav {
overflow: auto; overflow: auto;
width: 300px; width: 300px;
border-right: 1px solid var(--grey-2);
background: var(--white); background: var(--white);
} }

View File

@ -28,7 +28,8 @@ describe("fetch bindable properties", () => {
...testData() ...testData()
}) })
const contextBindings = result.filter(r => r.instance._id === "list-id" && r.type==="context") const contextBindings = result.filter(r => r.instance._id === "list-id" && r.type==="context")
expect(contextBindings.length).toBe(2) // 2 fields + _id + _rev
expect(contextBindings.length).toBe(4)
const namebinding = contextBindings.find(b => b.runtimeBinding === "data.name") const namebinding = contextBindings.find(b => b.runtimeBinding === "data.name")
expect(namebinding).toBeDefined() expect(namebinding).toBeDefined()
@ -37,6 +38,10 @@ describe("fetch bindable properties", () => {
const descriptionbinding = contextBindings.find(b => b.runtimeBinding === "data.description") const descriptionbinding = contextBindings.find(b => b.runtimeBinding === "data.description")
expect(descriptionbinding).toBeDefined() expect(descriptionbinding).toBeDefined()
expect(descriptionbinding.readableBinding).toBe("list-name.Test Model.description") expect(descriptionbinding.readableBinding).toBe("list-name.Test Model.description")
const idbinding = contextBindings.find(b => b.runtimeBinding === "data._id")
expect(idbinding).toBeDefined()
expect(idbinding.readableBinding).toBe("list-name.Test Model._id")
}) })
it("should return model schema, for grantparent context", () => { it("should return model schema, for grantparent context", () => {
@ -45,7 +50,8 @@ describe("fetch bindable properties", () => {
...testData() ...testData()
}) })
const contextBindings = result.filter(r => r.type==="context") const contextBindings = result.filter(r => r.type==="context")
expect(contextBindings.length).toBe(4) // 2 fields + _id + _rev ... x 2 models
expect(contextBindings.length).toBe(8)
const namebinding_parent = contextBindings.find(b => b.runtimeBinding === "parent.data.name") const namebinding_parent = contextBindings.find(b => b.runtimeBinding === "parent.data.name")
expect(namebinding_parent).toBeDefined() expect(namebinding_parent).toBeDefined()
@ -120,7 +126,7 @@ const testData = () => {
_id: "list-id", _id: "list-id",
_component: "@budibase/standard-components/list", _component: "@budibase/standard-components/list",
_instanceName: "list-name", _instanceName: "list-name",
model: "test-model-id", model: { isModel: true, modelId: "test-model-id", label: "Test Model", name: "all_test-model-id" },
_children: [ _children: [
{ {
_id: "list-item-heading-id", _id: "list-item-heading-id",
@ -138,7 +144,7 @@ const testData = () => {
_id: "child-list-id", _id: "child-list-id",
_component: "@budibase/standard-components/list", _component: "@budibase/standard-components/list",
_instanceName: "child-list-name", _instanceName: "child-list-name",
model: "test-model-id", model: { isModel: true, modelId: "test-model-id", label: "Test Model", name: "all_test-model-id"},
_children: [ _children: [
{ {
_id: "child-list-item-heading-id", _id: "child-list-item-heading-id",

View File

@ -688,14 +688,15 @@
lodash "^4.17.13" lodash "^4.17.13"
to-fast-properties "^2.0.0" to-fast-properties "^2.0.0"
"@budibase/bbui@^1.32.0": "@budibase/bbui@^1.33.0":
version "1.32.0" version "1.33.0"
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.32.0.tgz#4b099e51cf8aebfc963a763bb9687994a2ee26a8" resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.33.0.tgz#216b24dd815f45880e9795e66b04848329b0390f"
integrity sha512-Rrt5eLbea014TIfAbT40kP0D0AWNUi8Q0kDr3UZO6Aq4UXgjc0f53ZuJ7Kb66YRDWrqiucjf1FtvOUs3/YaD6g==
dependencies: dependencies:
sirv-cli "^0.4.6" sirv-cli "^0.4.6"
svelte-flatpickr "^2.4.0" svelte-flatpickr "^2.4.0"
"@budibase/client@^0.1.19": "@budibase/client@^0.1.21":
version "0.1.21" version "0.1.21"
resolved "https://registry.yarnpkg.com/@budibase/client/-/client-0.1.21.tgz#db414445c132b373f6c25e39d62628eb60cd8ac3" resolved "https://registry.yarnpkg.com/@budibase/client/-/client-0.1.21.tgz#db414445c132b373f6c25e39d62628eb60cd8ac3"
integrity sha512-/ju0vYbWh9MUjmxkGNlOL4S/VQd4p5mbz5rHu0yt55ak9t/yyzI6PzBBxlucBeRbXYd9OFynFjy1pvYt1v+z9Q== integrity sha512-/ju0vYbWh9MUjmxkGNlOL4S/VQd4p5mbz5rHu0yt55ak9t/yyzI6PzBBxlucBeRbXYd9OFynFjy1pvYt1v+z9Q==
@ -756,6 +757,10 @@
debug "^3.1.0" debug "^3.1.0"
lodash.once "^4.1.1" 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"
"@hapi/address@^2.1.2": "@hapi/address@^2.1.2":
version "2.1.4" version "2.1.4"
resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5" resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5"
@ -1382,7 +1387,6 @@ array-equal@^1.0.0:
array-filter@^1.0.0: array-filter@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-1.0.0.tgz#baf79e62e6ef4c2a4c0b831232daffec251f9d83" resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-1.0.0.tgz#baf79e62e6ef4c2a4c0b831232daffec251f9d83"
integrity sha1-uveeYubvTCpMC4MSMtr/7CUfnYM=
array-union@^2.1.0: array-union@^2.1.0:
version "2.1.0" version "2.1.0"
@ -1441,7 +1445,6 @@ atob@^2.1.2:
available-typed-arrays@^1.0.0, available-typed-arrays@^1.0.2: available-typed-arrays@^1.0.0, available-typed-arrays@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz#6b098ca9d8039079ee3f77f7b783c4480ba513f5" resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz#6b098ca9d8039079ee3f77f7b783c4480ba513f5"
integrity sha512-XWX3OX8Onv97LMk/ftVyBibpGwY5a8SmuxZPzeOxqmuEqUCOM9ZE+uIaD1VNJ5QnvU2UQusvmKbuM1FR8QWGfQ==
dependencies: dependencies:
array-filter "^1.0.0" array-filter "^1.0.0"
@ -1816,10 +1819,6 @@ class-utils@^0.3.5:
isobject "^3.0.0" isobject "^3.0.0"
static-extend "^0.1.1" static-extend "^0.1.1"
classnames@^2.2.5:
version "2.2.6"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce"
cli-cursor@^1.0.2: cli-cursor@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987" resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987"
@ -1967,10 +1966,6 @@ core-js-pure@^3.0.0:
version "3.6.5" version "3.6.5"
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.6.5.tgz#c79e75f5e38dbc85a662d91eea52b8256d53b813" resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.6.5.tgz#c79e75f5e38dbc85a662d91eea52b8256d53b813"
core-js@^3.1.3:
version "3.6.5"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.6.5.tgz#7395dc273af37fb2e50e9bd3d9fe841285231d1a"
core-util-is@1.0.2, core-util-is@~1.0.0: core-util-is@1.0.2, core-util-is@~1.0.0:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
@ -2405,7 +2400,6 @@ decode-uri-component@^0.2.0:
deep-equal@^2.0.1: deep-equal@^2.0.1:
version "2.0.3" version "2.0.3"
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.0.3.tgz#cad1c15277ad78a5c01c49c2dee0f54de8a6a7b0" resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.0.3.tgz#cad1c15277ad78a5c01c49c2dee0f54de8a6a7b0"
integrity sha512-Spqdl4H+ky45I9ByyJtXteOm9CaIrPmnIPmOhrkKGNYWeDgCvJ8jNYVCTjChxW4FqGuZnLHADc8EKRMX6+CgvA==
dependencies: dependencies:
es-abstract "^1.17.5" es-abstract "^1.17.5"
es-get-iterator "^1.1.0" es-get-iterator "^1.1.0"
@ -2594,7 +2588,6 @@ es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.5:
es-abstract@^1.17.4: es-abstract@^1.17.4:
version "1.17.6" version "1.17.6"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.6.tgz#9142071707857b2cacc7b89ecb670316c3e2d52a" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.6.tgz#9142071707857b2cacc7b89ecb670316c3e2d52a"
integrity sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==
dependencies: dependencies:
es-to-primitive "^1.2.1" es-to-primitive "^1.2.1"
function-bind "^1.1.1" function-bind "^1.1.1"
@ -2611,7 +2604,6 @@ es-abstract@^1.17.4:
es-abstract@^1.18.0-next.0: es-abstract@^1.18.0-next.0:
version "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" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.0.tgz#b302834927e624d8e5837ed48224291f2c66e6fc"
integrity sha512-elZXTZXKn51hUBdJjSZGYRujuzilgXo8vSPQzjGYXLvSlGiCo8VO8ZGV3kjo9a0WNJJ57hENagwbtlRuHuzkcQ==
dependencies: dependencies:
es-to-primitive "^1.2.1" es-to-primitive "^1.2.1"
function-bind "^1.1.1" function-bind "^1.1.1"
@ -2629,7 +2621,6 @@ es-abstract@^1.18.0-next.0:
es-get-iterator@^1.1.0: es-get-iterator@^1.1.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.0.tgz#bb98ad9d6d63b31aacdc8f89d5d0ee57bcb5b4c8" resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.0.tgz#bb98ad9d6d63b31aacdc8f89d5d0ee57bcb5b4c8"
integrity sha512-UfrmHuWQlNMTs35e1ypnvikg6jCz3SK8v8ImvmDsh36fCVUR1MqoFDiyn0/k52C8NqO3YsO8Oe0azeesNuqSsQ==
dependencies: dependencies:
es-abstract "^1.17.4" es-abstract "^1.17.4"
has-symbols "^1.0.1" has-symbols "^1.0.1"
@ -2889,13 +2880,6 @@ fd-slicer@~1.1.0:
dependencies: dependencies:
pend "~1.2.0" pend "~1.2.0"
feather-icons@^4.21.0:
version "4.28.0"
resolved "https://registry.yarnpkg.com/feather-icons/-/feather-icons-4.28.0.tgz#e1892a401fe12c4559291770ff6e68b0168e760f"
dependencies:
classnames "^2.2.5"
core-js "^3.1.3"
figures@^1.7.0: figures@^1.7.0:
version "1.7.0" version "1.7.0"
resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e"
@ -3329,7 +3313,6 @@ is-accessor-descriptor@^1.0.0:
is-arguments@^1.0.4: is-arguments@^1.0.4:
version "1.0.4" version "1.0.4"
resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.0.4.tgz#3faf966c7cba0ff437fb31f6250082fcf0448cf3" resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.0.4.tgz#3faf966c7cba0ff437fb31f6250082fcf0448cf3"
integrity sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==
is-arrayish@^0.2.1: is-arrayish@^0.2.1:
version "0.2.1" version "0.2.1"
@ -3338,7 +3321,6 @@ is-arrayish@^0.2.1:
is-bigint@^1.0.0: is-bigint@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.0.tgz#73da8c33208d00f130e9b5e15d23eac9215601c4" resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.0.tgz#73da8c33208d00f130e9b5e15d23eac9215601c4"
integrity sha512-t5mGUXC/xRheCK431ylNiSkGGpBp8bHENBcENTkDT6ppwPzEVxNGZRvgvmOEfbWkFhA7D2GEuE2mmQTr78sl2g==
is-binary-path@~2.1.0: is-binary-path@~2.1.0:
version "2.1.0" version "2.1.0"
@ -3349,7 +3331,6 @@ is-binary-path@~2.1.0:
is-boolean-object@^1.0.0: is-boolean-object@^1.0.0:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.0.1.tgz#10edc0900dd127697a92f6f9807c7617d68ac48e" 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: is-buffer@^1.1.5:
version "1.1.6" version "1.1.6"
@ -3362,7 +3343,6 @@ is-callable@^1.1.4, is-callable@^1.1.5:
is-callable@^1.2.0: is-callable@^1.2.0:
version "1.2.1" version "1.2.1"
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.1.tgz#4d1e21a4f437509d25ce55f8184350771421c96d" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.1.tgz#4d1e21a4f437509d25ce55f8184350771421c96d"
integrity sha512-wliAfSzx6V+6WfMOmus1xy0XvSgf/dlStkvTfq7F0g4bOIW0PSUbnyse3NhDwdyYS1ozfUtAAySqTws3z9Eqgg==
is-ci@^2.0.0: is-ci@^2.0.0:
version "2.0.0" version "2.0.0"
@ -3450,7 +3430,6 @@ is-installed-globally@^0.3.2:
is-map@^2.0.1: is-map@^2.0.1:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.1.tgz#520dafc4307bb8ebc33b813de5ce7c9400d644a1" 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: is-module@^1.0.0:
version "1.0.0" version "1.0.0"
@ -3459,12 +3438,10 @@ is-module@^1.0.0:
is-negative-zero@^2.0.0: is-negative-zero@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.0.tgz#9553b121b0fac28869da9ed459e20c7543788461" 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: is-number-object@^1.0.3:
version "1.0.4" version "1.0.4"
resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.4.tgz#36ac95e741cf18b283fc1ddf5e83da798e3ec197" 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: is-number@^3.0.0:
version "3.0.0" version "3.0.0"
@ -3525,14 +3502,12 @@ is-regex@^1.0.5:
is-regex@^1.1.0, is-regex@^1.1.1: is-regex@^1.1.0, is-regex@^1.1.1:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9"
integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==
dependencies: dependencies:
has-symbols "^1.0.1" has-symbols "^1.0.1"
is-set@^2.0.1: is-set@^2.0.1:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.1.tgz#d1604afdab1724986d30091575f54945da7e5f43" 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: is-stream@^1.1.0:
version "1.1.0" version "1.1.0"
@ -3545,7 +3520,6 @@ is-stream@^2.0.0:
is-string@^1.0.4, is-string@^1.0.5: is-string@^1.0.4, is-string@^1.0.5:
version "1.0.5" version "1.0.5"
resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6" 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: is-symbol@^1.0.2:
version "1.0.3" version "1.0.3"
@ -3556,7 +3530,6 @@ is-symbol@^1.0.2:
is-typed-array@^1.1.3: is-typed-array@^1.1.3:
version "1.1.3" version "1.1.3"
resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.3.tgz#a4ff5a5e672e1a55f99c7f54e59597af5c1df04d" resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.3.tgz#a4ff5a5e672e1a55f99c7f54e59597af5c1df04d"
integrity sha512-BSYUBOK/HJibQ30wWkWold5txYwMUXQct9YHAQJr8fSwvZoiglcqB0pd7vEN23+Tsi9IUEjztdOSzl4qLVYGTQ==
dependencies: dependencies:
available-typed-arrays "^1.0.0" available-typed-arrays "^1.0.0"
es-abstract "^1.17.4" es-abstract "^1.17.4"
@ -3570,12 +3543,10 @@ is-typedarray@~1.0.0:
is-weakmap@^2.0.1: is-weakmap@^2.0.1:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2" resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2"
integrity sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==
is-weakset@^2.0.1: is-weakset@^2.0.1:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.1.tgz#e9a0af88dbd751589f5e50d80f4c98b780884f83" 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: is-windows@^1.0.2:
version "1.0.2" version "1.0.2"
@ -3600,7 +3571,6 @@ isarray@1.0.0, isarray@~1.0.0:
isarray@^2.0.5: isarray@^2.0.5:
version "2.0.5" version "2.0.5"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==
isbuffer@~0.0.0: isbuffer@~0.0.0:
version "0.0.0" version "0.0.0"
@ -4715,12 +4685,10 @@ object-inspect@^1.7.0:
object-inspect@^1.8.0: object-inspect@^1.8.0:
version "1.8.0" version "1.8.0"
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0"
integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==
object-is@^1.1.2: object-is@^1.1.2:
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.2.tgz#c5d2e87ff9e119f78b7a088441519e2eec1573b6" resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.2.tgz#c5d2e87ff9e119f78b7a088441519e2eec1573b6"
integrity sha512-5lHCz+0uufF6wZ7CRFWJN3hp8Jqblpgve06U5CMQ3f//6iDjPr2PEo9MWCjEssDsa+UZEL4PkFpr+BMop6aKzQ==
dependencies: dependencies:
define-properties "^1.1.3" define-properties "^1.1.3"
es-abstract "^1.17.5" es-abstract "^1.17.5"
@ -5238,7 +5206,6 @@ regex-not@^1.0.0, regex-not@^1.0.2:
regexp.prototype.flags@^1.3.0: regexp.prototype.flags@^1.3.0:
version "1.3.0" version "1.3.0"
resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz#7aba89b3c13a64509dabcf3ca8d9fbb9bdf5cb75" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz#7aba89b3c13a64509dabcf3ca8d9fbb9bdf5cb75"
integrity sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==
dependencies: dependencies:
define-properties "^1.1.3" define-properties "^1.1.3"
es-abstract "^1.17.0-next.1" es-abstract "^1.17.0-next.1"
@ -5246,7 +5213,6 @@ regexp.prototype.flags@^1.3.0:
regexparam@^1.3.0: regexparam@^1.3.0:
version "1.3.0" version "1.3.0"
resolved "https://registry.yarnpkg.com/regexparam/-/regexparam-1.3.0.tgz#2fe42c93e32a40eff6235d635e0ffa344b92965f" 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: regexpu-core@^4.7.0:
version "4.7.0" version "4.7.0"
@ -5655,7 +5621,6 @@ shortid@^2.2.15:
side-channel@^1.0.2: side-channel@^1.0.2:
version "1.0.3" version "1.0.3"
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.3.tgz#cdc46b057550bbab63706210838df5d4c19519c3" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.3.tgz#cdc46b057550bbab63706210838df5d4c19519c3"
integrity sha512-A6+ByhlLkksFoUepsGxfj5x1gTSrs+OydsRptUxeNCabQpCFUvcwIczgOigI8vhY/OJCnPnyE9rGiwgvr9cS1g==
dependencies: dependencies:
es-abstract "^1.18.0-next.0" es-abstract "^1.18.0-next.0"
object-inspect "^1.8.0" object-inspect "^1.8.0"
@ -5996,11 +5961,6 @@ supports-color@^7.1.0:
dependencies: dependencies:
has-flag "^4.0.0" has-flag "^4.0.0"
svelte-filepond@^0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/svelte-filepond/-/svelte-filepond-0.0.1.tgz#7c20379213dac746192499d3d1de649d6db51c4b"
integrity sha512-R5z/Gj/2VSdV70GxvW226ww4zrVyV2EFTXrsWIdn7sEB7uYWIJvvciUNQVTfYZfcAgGsa4gprGsuqWNG3auSKw==
svelte-flatpickr@^2.4.0: svelte-flatpickr@^2.4.0:
version "2.4.0" version "2.4.0"
resolved "https://registry.yarnpkg.com/svelte-flatpickr/-/svelte-flatpickr-2.4.0.tgz#190871fc3305956c8c8fd3601cd036b8ac71ef49" resolved "https://registry.yarnpkg.com/svelte-flatpickr/-/svelte-flatpickr-2.4.0.tgz#190871fc3305956c8c8fd3601cd036b8ac71ef49"
@ -6363,7 +6323,6 @@ whatwg-url@^8.0.0:
which-boxed-primitive@^1.0.1: which-boxed-primitive@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.1.tgz#cbe8f838ebe91ba2471bb69e9edbda67ab5a5ec1" resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.1.tgz#cbe8f838ebe91ba2471bb69e9edbda67ab5a5ec1"
integrity sha512-7BT4TwISdDGBgaemWU0N0OU7FeAEJ9Oo2P1PHRm/FCWoEi2VLWC9b6xvxAA3C/NMpxg3HXVgi0sMmGbNUbNepQ==
dependencies: dependencies:
is-bigint "^1.0.0" is-bigint "^1.0.0"
is-boolean-object "^1.0.0" is-boolean-object "^1.0.0"
@ -6374,7 +6333,6 @@ which-boxed-primitive@^1.0.1:
which-collection@^1.0.1: which-collection@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.1.tgz#70eab71ebbbd2aefaf32f917082fc62cdcb70906" resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.1.tgz#70eab71ebbbd2aefaf32f917082fc62cdcb70906"
integrity sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==
dependencies: dependencies:
is-map "^2.0.1" is-map "^2.0.1"
is-set "^2.0.1" is-set "^2.0.1"
@ -6388,7 +6346,6 @@ which-module@^2.0.0:
which-typed-array@^1.1.2: which-typed-array@^1.1.2:
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.2.tgz#e5f98e56bda93e3dac196b01d47c1156679c00b2" resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.2.tgz#e5f98e56bda93e3dac196b01d47c1156679c00b2"
integrity sha512-KT6okrd1tE6JdZAy3o2VhMoYPh3+J6EMZLyrxBQsZflI1QCZIxMrIYLkosd8Twf+YfknVIHmYQPgJt238p8dnQ==
dependencies: dependencies:
available-typed-arrays "^1.0.2" available-typed-arrays "^1.0.2"
es-abstract "^1.17.5" es-abstract "^1.17.5"

View File

@ -1,6 +1,6 @@
{ {
"name": "budibase", "name": "budibase",
"version": "0.1.19", "version": "0.1.21",
"description": "Budibase CLI", "description": "Budibase CLI",
"repository": "https://github.com/Budibase/Budibase", "repository": "https://github.com/Budibase/Budibase",
"homepage": "https://www.budibase.com", "homepage": "https://www.budibase.com",
@ -17,7 +17,7 @@
"author": "Budibase", "author": "Budibase",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"@budibase/server": "^0.1.19", "@budibase/server": "^0.1.21",
"@inquirer/password": "^0.0.6-alpha.0", "@inquirer/password": "^0.0.6-alpha.0",
"chalk": "^2.4.2", "chalk": "^2.4.2",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/client", "name": "@budibase/client",
"version": "0.1.19", "version": "0.1.21",
"license": "MPL-2.0", "license": "MPL-2.0",
"main": "dist/budibase-client.js", "main": "dist/budibase-client.js",
"module": "dist/budibase-client.esm.mjs", "module": "dist/budibase-client.esm.mjs",

View File

@ -1,5 +1,4 @@
import { authenticate } from "./authenticate" import { authenticate } from "./authenticate"
import { triggerWorkflow } from "./workflow"
import appStore from "../state/store" import appStore from "../state/store"
const apiCall = method => async ({ url, body }) => { const apiCall = method => async ({ url, body }) => {
@ -53,7 +52,49 @@ const apiOpts = {
delete: del, delete: del,
} }
const createRecord = async params =>
await post({
url: `/api/${params.modelId}/records`,
body: makeRecordRequestBody(params),
})
const updateRecord = async params => {
const record = makeRecordRequestBody(params)
record._id = params._id
await patch({
url: `/api/${params.modelId}/records/${params._id}`,
body: record,
})
}
const makeRecordRequestBody = parameters => {
const body = {}
for (let fieldName in parameters.fields) {
const field = parameters.fields[fieldName]
// ensure fields sent are of the correct type
if (field.type === "boolean") {
if (field.value === "true") body[fieldName] = true
if (field.value === "false") body[fieldName] = false
} else if (field.type === "number") {
const val = parseFloat(field.value)
if (!isNaN(val)) {
body[fieldName] = val
}
} else if (field.type === "datetime") {
const date = new Date(field.value)
if (!isNaN(date.getTime())) {
body[fieldName] = date.toISOString()
}
} else {
body[fieldName] = field.value
}
}
return body
}
export default { export default {
authenticate: authenticate(apiOpts), authenticate: authenticate(apiOpts),
triggerWorkflow: triggerWorkflow(apiOpts), createRecord,
updateRecord,
} }

View File

@ -1,18 +0,0 @@
const delay = ms => new Promise(resolve => setTimeout(resolve, ms))
export default {
NAVIGATE: () => {
// TODO client navigation
},
DELAY: async ({ args }) => await delay(args.time),
FILTER: ({ args }) => {
const { field, condition, value } = args
switch (condition) {
case "equals":
if (field !== value) return
break
default:
return
}
},
}

View File

@ -1,68 +0,0 @@
import renderTemplateString from "../../state/renderTemplateString"
import appStore from "../../state/store"
import Orchestrator from "./orchestrator"
import clientActions from "./actions"
// Execute a workflow from a running budibase app
export const clientStrategy = ({ api }) => ({
context: {},
bindContextArgs: function(args) {
const mappedArgs = { ...args }
// bind the workflow action args to the workflow context, if required
for (let arg in args) {
const argValue = args[arg]
// We don't want to render mustache templates on non-strings
if (typeof argValue !== "string") continue
// Render the string with values from the workflow context and state
mappedArgs[arg] = renderTemplateString(argValue, {
context: this.context,
state: appStore.get(),
})
}
return mappedArgs
},
run: async function(workflow) {
for (let block of workflow.steps) {
// This code gets run in the browser
if (block.environment === "CLIENT") {
const action = clientActions[block.actionId]
await action({
context: this.context,
args: this.bindContextArgs(block.args),
id: block.id,
})
}
// this workflow block gets executed on the server
if (block.environment === "SERVER") {
const EXECUTE_WORKFLOW_URL = `/api/workflows/action`
const response = await api.post({
url: EXECUTE_WORKFLOW_URL,
body: {
action: block.actionId,
args: this.bindContextArgs(block.args, api),
},
})
this.context = {
...this.context,
[block.actionId]: response,
}
}
}
},
})
export const triggerWorkflow = api => async ({ workflow }) => {
const workflowOrchestrator = new Orchestrator(api)
workflowOrchestrator.strategy = clientStrategy
const EXECUTE_WORKFLOW_URL = `/api/workflows/${workflow}`
const workflowDefinition = await api.get({ url: EXECUTE_WORKFLOW_URL })
workflowOrchestrator.execute(workflowDefinition)
}

View File

@ -1,22 +0,0 @@
/**
* The workflow orchestrator is a class responsible for executing workflows.
* It relies on the strategy pattern, which allows composable behaviour to be
* passed into its execute() function. This allows custom execution behaviour based
* on where the orchestrator is run.
*
*/
export default class Orchestrator {
constructor(api) {
this.api = api
}
set strategy(strategy) {
this._strategy = strategy({ api: this.api })
}
async execute(workflow) {
if (workflow.live) {
this._strategy.run(workflow.definition)
}
}
}

View File

@ -50,7 +50,6 @@ export const createApp = ({
treeNode, treeNode,
onScreenSlotRendered, onScreenSlotRendered,
setupState: stateManager.setup, setupState: stateManager.setup,
getCurrentState: stateManager.getCurrentState,
}) })
return getInitialiseParams return getInitialiseParams

View File

@ -1,12 +1,13 @@
import setBindableComponentProp from "./setBindableComponentProp" import setBindableComponentProp from "./setBindableComponentProp"
import { attachChildren } from "../render/attachChildren" import { attachChildren } from "../render/attachChildren"
import store from "../state/store"
export const trimSlash = str => str.replace(/^\/+|\/+$/g, "") export const trimSlash = str => str.replace(/^\/+|\/+$/g, "")
export const bbFactory = ({ export const bbFactory = ({
componentLibraries, componentLibraries,
onScreenSlotRendered, onScreenSlotRendered,
getCurrentState, runEventActions,
}) => { }) => {
const apiCall = method => (url, body) => { const apiCall = method => (url, body) => {
return fetch(url, { return fetch(url, {
@ -26,13 +27,6 @@ export const bbFactory = ({
delete: apiCall("DELETE"), delete: apiCall("DELETE"),
} }
const safeCallEvent = (event, context) => {
const isFunction = obj =>
!!(obj && obj.constructor && obj.call && obj.apply)
if (isFunction(event)) event(context)
}
return (treeNode, setupState) => { return (treeNode, setupState) => {
const attachParams = { const attachParams = {
componentLibraries, componentLibraries,
@ -44,12 +38,18 @@ export const bbFactory = ({
return { return {
attachChildren: attachChildren(attachParams), attachChildren: attachChildren(attachParams),
props: treeNode.props, props: treeNode.props,
call: safeCallEvent, call: async eventName =>
eventName &&
(await runEventActions(
treeNode.props[eventName],
store.getState(treeNode.contextStoreKey)
)),
setBinding: setBindableComponentProp(treeNode), setBinding: setBindableComponentProp(treeNode),
api, api,
parent, parent,
store: store.getStore(treeNode.contextStoreKey),
// these parameters are populated by screenRouter // these parameters are populated by screenRouter
routeParams: () => getCurrentState()["##routeParams"], routeParams: () => store.getState()["##routeParams"],
} }
} }
} }

View File

@ -1,20 +1,38 @@
import api from "../api" import renderTemplateString from "./renderTemplateString"
export const EVENT_TYPE_MEMBER_NAME = "##eventHandlerType" export const EVENT_TYPE_MEMBER_NAME = "##eventHandlerType"
export const eventHandlers = routeTo => { export const eventHandlers = routeTo => {
const handler = (parameters, execute) => ({ const handlers = {
execute, "Navigate To": param => routeTo(param && param.url),
parameters,
})
return {
"Navigate To": handler(["url"], param => routeTo(param && param.url)),
"Trigger Workflow": handler(["workflow"], api.triggerWorkflow),
} }
// when an event is called, this is what gets run
const runEventActions = async (actions, state) => {
if (!actions) return
// calls event handlers sequentially
for (let action of actions) {
const handler = handlers[action[EVENT_TYPE_MEMBER_NAME]]
const parameters = createParameters(action.parameters, state)
if (handler) {
await handler(parameters)
}
}
}
return runEventActions
} }
export const isEventType = prop => // this will take a parameters obj, iterate all keys, and do a mustache render
Array.isArray(prop) && // for every string. It will work recursively if it encounnters an {}
prop.length > 0 && const createParameters = (parameterTemplateObj, state) => {
!prop[0][EVENT_TYPE_MEMBER_NAME] === undefined const parameters = {}
for (let key in parameterTemplateObj) {
if (typeof parameterTemplateObj[key] === "string") {
parameters[key] = renderTemplateString(parameterTemplateObj[key], state)
} else if (typeof parameterTemplateObj[key] === "object") {
parameters[key] = createParameters(parameterTemplateObj[key], state)
}
}
return parameters
}

View File

@ -1,8 +1,4 @@
import { import { eventHandlers } from "./eventHandlers"
isEventType,
eventHandlers,
EVENT_TYPE_MEMBER_NAME,
} from "./eventHandlers"
import { bbFactory } from "./bbComponentApi" import { bbFactory } from "./bbComponentApi"
import renderTemplateString from "./renderTemplateString" import renderTemplateString from "./renderTemplateString"
import appStore from "./store" import appStore from "./store"
@ -25,33 +21,23 @@ export const createStateManager = ({
onScreenSlotRendered, onScreenSlotRendered,
routeTo, routeTo,
}) => { }) => {
let handlerTypes = eventHandlers(routeTo) let runEventActions = eventHandlers(routeTo)
// creating a reference to the current state
// this avoids doing store.get() ... which is expensive on
// hot paths, according to the svelte docs.
// the state object reference never changes (although it's internals do)
// so this should work fine for us
let currentState
appStore.subscribe(s => (currentState = s))
const getCurrentState = () => currentState
const bb = bbFactory({ const bb = bbFactory({
getCurrentState,
componentLibraries, componentLibraries,
onScreenSlotRendered, onScreenSlotRendered,
runEventActions,
}) })
const setup = _setup({ handlerTypes, getCurrentState, bb }) const setup = _setup(bb)
return { return {
setup, setup,
destroy: () => {}, destroy: () => {},
getCurrentState,
} }
} }
const _setup = ({ handlerTypes, getCurrentState, bb }) => node => { const _setup = bb => node => {
const props = node.props const props = node.props
const initialProps = { ...props } const initialProps = { ...props }
@ -70,53 +56,10 @@ const _setup = ({ handlerTypes, getCurrentState, bb }) => node => {
node.stateBound = true node.stateBound = true
} }
} }
if (isEventType(propValue)) {
const state = appStore.getState(node.contextStoreKey)
const handlersInfos = []
for (let event of propValue) {
const handlerInfo = {
handlerType: event[EVENT_TYPE_MEMBER_NAME],
parameters: event.parameters,
} }
const resolvedParams = {} const setup = _setup(bb)
for (let paramName in handlerInfo.parameters) {
const paramValue = handlerInfo.parameters[paramName]
resolvedParams[paramName] = () =>
renderTemplateString(paramValue, state)
}
handlerInfo.parameters = resolvedParams
handlersInfos.push(handlerInfo)
}
if (handlersInfos.length === 0) {
initialProps[propName] = doNothing
} else {
initialProps[propName] = async context => {
for (let handlerInfo of handlersInfos) {
const handler = makeHandler(handlerTypes, handlerInfo)
await handler(context)
}
}
}
}
}
const setup = _setup({ handlerTypes, getCurrentState, bb })
initialProps._bb = bb(node, setup) initialProps._bb = bb(node, setup)
return initialProps return initialProps
} }
const makeHandler = (handlerTypes, handlerInfo) => {
const handlerType = handlerTypes[handlerInfo.handlerType]
return async context => {
const parameters = {}
for (let paramName in handlerInfo.parameters) {
parameters[paramName] = handlerInfo.parameters[paramName](context)
}
await handlerType.execute(parameters)
}
}

View File

@ -11,6 +11,7 @@ const contextStoreKey = (dataProviderId, childIndex) =>
`${dataProviderId}${childIndex >= 0 ? ":" + childIndex : ""}` `${dataProviderId}${childIndex >= 0 ? ":" + childIndex : ""}`
// creates a store for a datacontext (e.g. each item in a list component) // creates a store for a datacontext (e.g. each item in a list component)
// overrides store if already exists
const create = (data, dataProviderId, childIndex, parentContextStoreId) => { const create = (data, dataProviderId, childIndex, parentContextStoreId) => {
const key = contextStoreKey(dataProviderId, childIndex) const key = contextStoreKey(dataProviderId, childIndex)
const state = { data } const state = { data }
@ -22,14 +23,13 @@ const create = (data, dataProviderId, childIndex, parentContextStoreId) => {
? contextStores[parentContextStoreId].state ? contextStores[parentContextStoreId].state
: rootState : rootState
if (!contextStores[key]) {
contextStores[key] = { contextStores[key] = {
store: writable(state), store: writable(state),
subscriberCount: 0, subscriberCount: 0,
state, state,
parentContextStoreId, parentContextStoreId,
} }
}
return key return key
} }
@ -94,6 +94,9 @@ const set = (value, dataProviderId, childIndex) =>
const getState = contextStoreKey => const getState = contextStoreKey =>
contextStoreKey ? contextStores[contextStoreKey].state : rootState contextStoreKey ? contextStores[contextStoreKey].state : rootState
const getStore = contextStoreKey =>
contextStoreKey ? contextStores[contextStoreKey] : rootStore
export default { export default {
subscribe, subscribe,
update, update,
@ -101,4 +104,5 @@ export default {
getState, getState,
create, create,
contextStoreKey, contextStoreKey,
getStore,
} }

View File

@ -181,8 +181,7 @@ const maketestlib = window => ({
currentProps = Object.assign(currentProps, props) currentProps = Object.assign(currentProps, props)
if (currentProps.onClick) { if (currentProps.onClick) {
node.addEventListener("click", () => { node.addEventListener("click", () => {
const testText = currentProps.testText || "hello" currentProps._bb.call("onClick")
currentProps._bb.call(props.onClick, { testText })
}) })
} }
} }

View File

@ -4,6 +4,7 @@ WORKDIR /app
ENV CLOUD=1 ENV CLOUD=1
ENV COUCH_DB_URL=https://couchdb.budi.live:5984 ENV COUCH_DB_URL=https://couchdb.budi.live:5984
env BUDIBASE_ENVIRONMENT=PRODUCTION
# copy files and install dependencies # copy files and install dependencies
COPY . ./ COPY . ./

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/server", "name": "@budibase/server",
"version": "0.1.19", "version": "0.1.21",
"description": "Budibase Web Server", "description": "Budibase Web Server",
"main": "src/electron.js", "main": "src/electron.js",
"repository": { "repository": {
@ -42,7 +42,7 @@
"author": "Michael Shanks", "author": "Michael Shanks",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"@budibase/client": "^0.1.19", "@budibase/client": "^0.1.21",
"@koa/router": "^8.0.0", "@koa/router": "^8.0.0",
"@sendgrid/mail": "^7.1.1", "@sendgrid/mail": "^7.1.1",
"@sentry/node": "^5.19.2", "@sentry/node": "^5.19.2",
@ -55,6 +55,7 @@
"electron-updater": "^4.3.1", "electron-updater": "^4.3.1",
"fix-path": "^3.0.0", "fix-path": "^3.0.0",
"fs-extra": "^8.1.0", "fs-extra": "^8.1.0",
"joi": "^17.2.1",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"koa": "^2.7.0", "koa": "^2.7.0",
"koa-body": "^4.1.0", "koa-body": "^4.1.0",
@ -74,6 +75,7 @@
"tar-fs": "^2.1.0", "tar-fs": "^2.1.0",
"uuid": "^3.3.2", "uuid": "^3.3.2",
"validate.js": "^0.13.1", "validate.js": "^0.13.1",
"worker-farm": "^1.7.0",
"yargs": "^13.2.4", "yargs": "^13.2.4",
"zlib": "^1.0.5" "zlib": "^1.0.5"
}, },

View File

@ -16,7 +16,7 @@ exports.authenticate = async ctx => {
const { clientId } = await masterDb.get(ctx.user.appId) const { clientId } = await masterDb.get(ctx.user.appId)
if (!clientId) { if (!clientId) {
ctx.throw(400, "ClientId not suplied") ctx.throw(400, "ClientId not supplied")
} }
// find the instance that the user is associated with // find the instance that the user is associated with
const db = new CouchDB(ClientDb.name(clientId)) const db = new CouchDB(ClientDb.name(clientId))

View File

@ -25,7 +25,7 @@ exports.save = async function(ctx) {
...ctx.request.body, ...ctx.request.body,
} }
// update renamed record fields when model is updated // rename record fields when table column is renamed
const { _rename } = modelToSave const { _rename } = modelToSave
if (_rename) { if (_rename) {
const records = await db.query(`database/all_${modelToSave._id}`, { const records = await db.query(`database/all_${modelToSave._id}`, {
@ -41,6 +41,15 @@ exports.save = async function(ctx) {
delete modelToSave._rename delete modelToSave._rename
} }
// update schema of non-statistics views when new columns are added
for (let view in modelToSave.views) {
const modelView = modelToSave.views[view]
if (!modelView) continue
if (modelView.schema.group || modelView.schema.field) continue
modelView.schema = modelToSave.schema
}
const result = await db.post(modelToSave) const result = await db.post(modelToSave)
modelToSave._rev = result.rev modelToSave._rev = result.rev

View File

@ -2,6 +2,16 @@ const CouchDB = require("../../db")
const validateJs = require("validate.js") const validateJs = require("validate.js")
const newid = require("../../db/newid") const newid = require("../../db/newid")
function emitEvent(eventType, ctx, record) {
ctx.eventEmitter &&
ctx.eventEmitter.emit(eventType, {
args: {
record,
},
instanceId: ctx.user.instanceId,
})
}
validateJs.extend(validateJs.validators.datetime, { validateJs.extend(validateJs.validators.datetime, {
parse: function(value) { parse: function(value) {
return new Date(value).getTime() return new Date(value).getTime()
@ -12,6 +22,40 @@ validateJs.extend(validateJs.validators.datetime, {
}, },
}) })
exports.patch = async function(ctx) {
const db = new CouchDB(ctx.user.instanceId)
const record = await db.get(ctx.params.id)
const model = await db.get(record.modelId)
const patchfields = ctx.request.body
for (let key in patchfields) {
if (!model.schema[key]) continue
record[key] = patchfields[key]
}
const validateResult = await validate({
record,
model,
})
if (!validateResult.valid) {
ctx.status = 400
ctx.body = {
status: 400,
errors: validateResult.errors,
}
return
}
const response = await db.put(record)
record._rev = response.rev
record.type = "record"
ctx.body = record
ctx.status = 200
ctx.message = `${model.name} updated successfully.`
return
}
exports.save = async function(ctx) { exports.save = async function(ctx) {
const db = new CouchDB(ctx.user.instanceId) const db = new CouchDB(ctx.user.instanceId)
const record = ctx.request.body const record = ctx.request.body
@ -76,13 +120,7 @@ exports.save = async function(ctx) {
} }
} }
ctx.eventEmitter && emitEvent(`record:save`, ctx, record)
ctx.eventEmitter.emit(`record:save`, {
args: {
record,
},
instanceId: ctx.user.instanceId,
})
ctx.body = record ctx.body = record
ctx.status = 200 ctx.status = 200
ctx.message = `${model.name} created successfully` ctx.message = `${model.name} created successfully`
@ -145,7 +183,7 @@ exports.destroy = async function(ctx) {
return return
} }
ctx.body = await db.remove(ctx.params.recordId, ctx.params.revId) ctx.body = await db.remove(ctx.params.recordId, ctx.params.revId)
ctx.eventEmitter && ctx.eventEmitter.emit(`record:delete`, record) emitEvent(`record:delete`, ctx, record)
} }
exports.validate = async function(ctx) { exports.validate = async function(ctx) {

View File

@ -87,7 +87,10 @@ exports.processLocalFileUpload = async function(ctx) {
pendingFileUploads = { _id: "_local/fileuploads", uploads: [] } pendingFileUploads = { _id: "_local/fileuploads", uploads: [] }
}) })
pendingFileUploads.uploads = [...filesToProcess, ...pendingFileUploads.uploads] pendingFileUploads.uploads = [
...filesToProcess,
...pendingFileUploads.uploads,
]
await db.put(pendingFileUploads) await db.put(pendingFileUploads)
ctx.body = filesToProcess ctx.body = filesToProcess

View File

@ -103,9 +103,10 @@ function viewTemplate({ field, modelId, groupBy, filters = [], calculation }) {
let schema = null let schema = null
if (calculation) { if (calculation) {
schema = groupBy schema = {
? { ...GROUP_PROPERTY, ...SCHEMA_MAP[calculation] } ...(groupBy ? GROUP_PROPERTY : FIELD_PROPERTY),
: { ...FIELD_PROPERTY, ...SCHEMA_MAP[calculation] } ...SCHEMA_MAP[calculation],
}
} }
return { return {

View File

@ -1,24 +0,0 @@
const userController = require("../../user")
module.exports = async function createUser({ args, instanceId }) {
const ctx = {
params: {
instanceId,
},
request: {
body: args.user,
},
}
try {
const response = await userController.create(ctx)
return {
user: response,
}
} catch (err) {
console.error(err)
return {
user: null,
}
}
}

View File

@ -1,29 +0,0 @@
const recordController = require("../../record")
module.exports = async function saveRecord({ args, context }) {
const { model, ...record } = args.record
const ctx = {
params: {
instanceId: context.instanceId,
modelId: model._id,
},
request: {
body: record,
},
user: { instanceId: context.instanceId },
}
try {
await recordController.save(ctx)
return {
record: ctx.body,
}
} catch (err) {
console.error(err)
return {
record: null,
error: err.message,
}
}
}

View File

@ -1,26 +0,0 @@
const sgMail = require("@sendgrid/mail")
sgMail.setApiKey(process.env.SENDGRID_API_KEY)
module.exports = async function sendEmail({ args }) {
const msg = {
to: args.to,
from: args.from,
subject: args.subject,
text: args.text,
}
try {
await sgMail.send(msg)
return {
success: true,
...args,
}
} catch (err) {
console.error(err)
return {
success: false,
error: err.message,
}
}
}

View File

@ -1,85 +1,81 @@
const ACTION = { const ACTION = {
SET_STATE: {
name: "Update UI State",
tagline: "Update <b>{{path}}</b> to <b>{{value}}</b>",
icon: "ri-refresh-line",
description: "Update your User Interface with some data.",
environment: "CLIENT",
params: {
path: "string",
value: "longText",
},
},
NAVIGATE: {
name: "Navigate",
tagline: "Navigate to <b>{{url}}</b>",
icon: "ri-navigation-line",
description: "Navigate to another page.",
environment: "CLIENT",
params: {
url: "string",
},
},
SAVE_RECORD: { SAVE_RECORD: {
name: "Save Record", name: "Save Record",
tagline: "<b>Save</b> a <b>{{record.model.name}}</b> record", tagline: "<b>Save</b> a <b>{{record.model.name}}</b> record",
icon: "ri-save-3-fill", icon: "ri-save-3-fill",
description: "Save a record to your database.", description: "Save a record to your database.",
environment: "SERVER",
params: { params: {
record: "record", record: "record",
}, },
args: { args: {
record: {}, record: {},
}, },
type: "ACTION",
}, },
DELETE_RECORD: { DELETE_RECORD: {
description: "Delete a record from your database.", description: "Delete a record from your database.",
icon: "ri-delete-bin-line", icon: "ri-delete-bin-line",
name: "Delete Record", name: "Delete Record",
tagline: "<b>Delete</b> a <b>{{record.model.name}}</b> record", tagline: "<b>Delete</b> a <b>{{record.model.name}}</b> record",
environment: "SERVER", params: {},
params: { args: {},
record: "record", type: "ACTION",
}, },
args: {
record: {},
},
},
// FIND_RECORD: {
// description: "Find a record in your database.",
// tagline: "<b>Find</b> a <b>{{record.model.name}}</b> record",
// icon: "ri-search-line",
// name: "Find Record",
// environment: "SERVER",
// params: {
// record: "string",
// },
// },
CREATE_USER: { CREATE_USER: {
description: "Create a new user.", description: "Create a new user.",
tagline: "Create user <b>{{username}}</b>", tagline: "Create user <b>{{username}}</b>",
icon: "ri-user-add-fill", icon: "ri-user-add-fill",
name: "Create User", name: "Create User",
environment: "SERVER",
params: { params: {
username: "string", username: "string",
password: "password", password: "password",
accessLevelId: "accessLevel", accessLevelId: "accessLevel",
}, },
args: {
accessLevelId: "POWER_USER",
},
type: "ACTION",
}, },
SEND_EMAIL: { SEND_EMAIL: {
description: "Send an email.", description: "Send an email.",
tagline: "Send email to <b>{{to}}</b>", tagline: "Send email to <b>{{to}}</b>",
icon: "ri-mail-open-fill", icon: "ri-mail-open-fill",
name: "Send Email", name: "Send Email",
environment: "SERVER",
params: { params: {
to: "string", to: "string",
from: "string", from: "string",
subject: "longText", subject: "longText",
text: "longText", text: "longText",
}, },
type: "ACTION",
},
}
const LOGIC = {
FILTER: {
name: "Filter",
tagline: "{{filter}} <b>{{condition}}</b> {{value}}",
icon: "ri-git-branch-line",
description: "Filter any workflows which do not meet certain conditions.",
params: {
filter: "string",
condition: ["equals"],
value: "string",
},
args: {
condition: "equals",
},
type: "LOGIC",
},
DELAY: {
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",
}, },
} }
@ -89,11 +85,11 @@ const TRIGGER = {
event: "record:save", event: "record:save",
icon: "ri-save-line", icon: "ri-save-line",
tagline: "Record is added to <b>{{model.name}}</b>", tagline: "Record is added to <b>{{model.name}}</b>",
description: "Save a record to your database.", description: "Fired when a record is saved to your database.",
environment: "SERVER",
params: { params: {
model: "model", model: "model",
}, },
type: "TRIGGER",
}, },
RECORD_DELETED: { RECORD_DELETED: {
name: "Record Deleted", name: "Record Deleted",
@ -101,70 +97,17 @@ const TRIGGER = {
icon: "ri-delete-bin-line", icon: "ri-delete-bin-line",
tagline: "Record is deleted from <b>{{model.name}}</b>", tagline: "Record is deleted from <b>{{model.name}}</b>",
description: "Fired when a record is deleted from your database.", description: "Fired when a record is deleted from your database.",
environment: "SERVER",
params: { params: {
model: "model", model: "model",
}, },
}, type: "TRIGGER",
// CLICK: {
// name: "Click",
// icon: "ri-cursor-line",
// tagline: "{{component}} is clicked",
// description: "Trigger when you click on an element in the UI.",
// environment: "CLIENT",
// params: {
// component: "component"
// }
// },
// LOAD: {
// name: "Load",
// icon: "ri-loader-line",
// tagline: "{{component}} is loaded",
// description: "Trigger an element has finished loading.",
// environment: "CLIENT",
// params: {
// component: "component"
// }
// },
// INPUT: {
// name: "Input",
// icon: "ri-text",
// tagline: "Text entered into {{component}",
// description: "Trigger when you type into an input box.",
// environment: "CLIENT",
// params: {
// component: "component"
// }
// },
}
const LOGIC = {
FILTER: {
name: "Filter",
tagline: "{{field}} <b>{{condition}}</b> {{value}}",
icon: "ri-git-branch-line",
description: "Filter any workflows which do not meet certain conditions.",
environment: "CLIENT",
params: {
filter: "string",
condition: ["equals"],
value: "string",
},
},
DELAY: {
name: "Delay",
icon: "ri-time-fill",
tagline: "Delay for <b>{{time}}</b> milliseconds",
description: "Delay the workflow until an amount of time has passed.",
environment: "CLIENT",
params: {
time: "number",
},
}, },
} }
export default { // This contains the definitions for the steps and triggers that make up a workflow, a workflow comprises
// of many steps and a single trigger
module.exports = {
ACTION, ACTION,
TRIGGER,
LOGIC, LOGIC,
TRIGGER,
} }

View File

@ -1,5 +1,13 @@
const CouchDB = require("../../../db") const CouchDB = require("../../../db")
const newid = require("../../../db/newid") const newid = require("../../../db/newid")
const blockDefinitions = require("./blockDefinitions")
const triggers = require("../../../workflows/triggers")
/*************************
* *
* BUILDER FUNCTIONS *
* *
*************************/
exports.create = async function(ctx) { exports.create = async function(ctx) {
const db = new CouchDB(ctx.user.instanceId) const db = new CouchDB(ctx.user.instanceId)
@ -53,22 +61,47 @@ exports.find = async function(ctx) {
ctx.body = await db.get(ctx.params.id) ctx.body = await db.get(ctx.params.id)
} }
exports.executeAction = async function(ctx) {
const { args, action } = ctx.request.body
const workflowAction = require(`./actions/${action}`)
const response = await workflowAction({
args,
instanceId: ctx.user.instanceId,
})
ctx.body = response
}
exports.fetchActionScript = async function(ctx) {
const workflowAction = require(`./actions/${ctx.action}`)
ctx.body = workflowAction
}
exports.destroy = async function(ctx) { exports.destroy = async function(ctx) {
const db = new CouchDB(ctx.user.instanceId) const db = new CouchDB(ctx.user.instanceId)
ctx.body = await db.remove(ctx.params.id, ctx.params.rev) ctx.body = await db.remove(ctx.params.id, ctx.params.rev)
} }
exports.getActionList = async function(ctx) {
ctx.body = blockDefinitions.ACTION
}
exports.getTriggerList = async function(ctx) {
ctx.body = blockDefinitions.TRIGGER
}
exports.getLogicList = async function(ctx) {
ctx.body = blockDefinitions.LOGIC
}
module.exports.getDefinitionList = async function(ctx) {
ctx.body = {
logic: blockDefinitions.LOGIC,
trigger: blockDefinitions.TRIGGER,
action: blockDefinitions.ACTION,
}
}
/*********************
* *
* API FUNCTIONS *
* *
*********************/
exports.trigger = async function(ctx) {
const db = new CouchDB(ctx.user.instanceId)
let workflow = await db.get(ctx.params.id)
await triggers.externalTrigger(workflow, {
...ctx.request.body,
instanceId: ctx.user.instanceId,
})
ctx.status = 200
ctx.body = {
message: `Workflow ${workflow._id} has been triggered.`,
workflow,
}
}

View File

@ -22,6 +22,11 @@ router
authorized(WRITE_MODEL, ctx => ctx.params.modelId), authorized(WRITE_MODEL, ctx => ctx.params.modelId),
recordController.save recordController.save
) )
.patch(
"/api/:modelId/records/:id",
authorized(WRITE_MODEL, ctx => ctx.params.modelId),
recordController.patch
)
.post( .post(
"/api/:modelId/records/validate", "/api/:modelId/records/validate",
authorized(WRITE_MODEL, ctx => ctx.params.modelId), authorized(WRITE_MODEL, ctx => ctx.params.modelId),

View File

@ -37,7 +37,7 @@ describe("/accesslevels", () => {
beforeEach(async () => { beforeEach(async () => {
instanceId = (await createInstance(request, appId))._id instanceId = (await createInstance(request, appId))._id
model = await createModel(request, appId, instanceId) model = await createModel(request, appId, instanceId)
view = await createView(request, appId, instanceId) view = await createView(request, appId, instanceId, model._id)
}) })
describe("create", () => { describe("create", () => {

View File

@ -51,6 +51,12 @@ exports.createModel = async (request, appId, instanceId, model) => {
type: "string", type: "string",
}, },
}, },
description: {
type: "text",
constraints: {
type: "string",
},
},
}, },
} }
@ -61,9 +67,10 @@ exports.createModel = async (request, appId, instanceId, model) => {
return res.body return res.body
} }
exports.createView = async (request, appId, instanceId, view) => { exports.createView = async (request, appId, instanceId, modelId, view) => {
view = view || { view = view || {
map: "function(doc) { emit(doc[doc.key], doc._id); } ", map: "function(doc) { emit(doc[doc.key], doc._id); } ",
modelId: modelId,
} }
const res = await request const res = await request

View File

@ -120,6 +120,10 @@ describe("/models", () => {
testModel = await createModel(request, app._id, instance._id, testModel) testModel = await createModel(request, app._id, instance._id, testModel)
}); });
afterEach(() => {
delete testModel._rev
});
it("returns all the models for that instance in the response body", done => { it("returns all the models for that instance in the response body", done => {
request request
.get(`/api/models`) .get(`/api/models`)

View File

@ -30,13 +30,12 @@ describe("/records", () => {
model = await createModel(request, app._id, instance._id) model = await createModel(request, app._id, instance._id)
record = { record = {
name: "Test Contact", name: "Test Contact",
description: "original description",
status: "new", status: "new",
modelId: model._id modelId: model._id
} }
}) })
describe("save, load, update, delete", () => {
const createRecord = async r => const createRecord = async r =>
await request await request
.post(`/api/${model._id}/records`) .post(`/api/${model._id}/records`)
@ -45,6 +44,17 @@ describe("/records", () => {
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
const loadRecord = async id =>
await request
.get(`/api/${model._id}/records/${id}`)
.set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/)
.expect(200)
describe("save, load, update, delete", () => {
it("returns a success message when the record is created", async () => { it("returns a success message when the record is created", async () => {
const res = await createRecord() const res = await createRecord()
expect(res.res.statusMessage).toEqual(`${model.name} created successfully`) expect(res.res.statusMessage).toEqual(`${model.name} created successfully`)
@ -144,6 +154,35 @@ describe("/records", () => {
}) })
}) })
describe("patch", () => {
it("should update only the fields that are supplied", async () => {
const rec = await createRecord()
const existing = rec.body
const res = await request
.patch(`/api/${model._id}/records/${existing._id}`)
.send({
_id: existing._id,
_rev: existing._rev,
modelId: model._id,
name: "Updated Name",
})
.set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/)
.expect(200)
expect(res.res.statusMessage).toEqual(`${model.name} updated successfully.`)
expect(res.body.name).toEqual("Updated Name")
expect(res.body.description).toEqual(existing.description)
const savedRecord = await loadRecord(res.body._id)
expect(savedRecord.body.description).toEqual(existing.description)
expect(savedRecord.body.name).toEqual("Updated Name")
})
})
describe("validate", () => { describe("validate", () => {
it("should return no errors on valid record", async () => { it("should return no errors on valid record", async () => {
const result = await request const result = await request

View File

@ -72,8 +72,14 @@ describe("/views", () => {
type: "text", type: "text",
constraints: { constraints: {
type: "string" type: "string"
} },
} },
description: {
type: "text",
constraints: {
type: "string"
},
},
} }
} }
}); });

View File

@ -23,7 +23,7 @@ const TEST_WORKFLOW = {
], ],
next: { next: {
actionId: "abc123", stepId: "abc123",
type: "SERVER", type: "SERVER",
conditions: { conditions: {
} }

View File

@ -1,21 +1,77 @@
const Router = require("@koa/router") const Router = require("@koa/router")
const controller = require("../controllers/workflow") const controller = require("../controllers/workflow")
const authorized = require("../../middleware/authorized") const authorized = require("../../middleware/authorized")
const joiValidator = require("../../middleware/joi-validator")
const { BUILDER } = require("../../utilities/accessLevels") const { BUILDER } = require("../../utilities/accessLevels")
const Joi = require("joi")
const router = Router() const router = Router()
// prettier-ignore
function generateStepSchema(allowStepTypes) {
return Joi.object({
stepId: Joi.string().required(),
id: Joi.string().required(),
description: Joi.string().required(),
name: Joi.string().required(),
tagline: Joi.string().required(),
icon: Joi.string().required(),
params: Joi.object(),
// TODO: validate args a bit more deeply
args: Joi.object(),
type: Joi.string().required().valid(...allowStepTypes),
}).unknown(true)
}
// prettier-ignore
const workflowValidator = joiValidator.body(Joi.object({
live: Joi.bool(),
id: Joi.string().required(),
rev: Joi.string().required(),
name: Joi.string().required(),
type: Joi.string().valid("workflow").required(),
definition: Joi.object({
steps: Joi.array().required().items(generateStepSchema(["ACTION", "LOGIC"])),
trigger: generateStepSchema(["TRIGGER"]).required(),
}).required().unknown(true),
}).unknown(true))
router router
.get(
"/api/workflows/trigger/list",
authorized(BUILDER),
controller.getTriggerList
)
.get(
"/api/workflows/action/list",
authorized(BUILDER),
controller.getActionList
)
.get(
"/api/workflows/logic/list",
authorized(BUILDER),
controller.getLogicList
)
.get(
"/api/workflows/definitions/list",
authorized(BUILDER),
controller.getDefinitionList
)
.get("/api/workflows", authorized(BUILDER), controller.fetch) .get("/api/workflows", authorized(BUILDER), controller.fetch)
.get("/api/workflows/:id", authorized(BUILDER), controller.find) .get("/api/workflows/:id", authorized(BUILDER), controller.find)
.get( .put(
"/api/workflows/:id/:action", "/api/workflows",
authorized(BUILDER), authorized(BUILDER),
controller.fetchActionScript workflowValidator,
controller.update
) )
.put("/api/workflows", authorized(BUILDER), controller.update) .post(
.post("/api/workflows", authorized(BUILDER), controller.create) "/api/workflows",
.post("/api/workflows/action", controller.executeAction) authorized(BUILDER),
workflowValidator,
controller.create
)
.post("/api/workflows/:id/trigger", controller.trigger)
.delete("/api/workflows/:id/:rev", authorized(BUILDER), controller.destroy) .delete("/api/workflows/:id/:rev", authorized(BUILDER), controller.destroy)
module.exports = router module.exports = router

View File

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

View File

@ -7,4 +7,5 @@ module.exports = {
COUCH_DB_URL: process.env.COUCH_DB_URL, COUCH_DB_URL: process.env.COUCH_DB_URL,
SALT_ROUNDS: process.env.SALT_ROUNDS, SALT_ROUNDS: process.env.SALT_ROUNDS,
LOGGER: process.env.LOGGER, LOGGER: process.env.LOGGER,
BUDIBASE_ENVIRONMENT: process.env.BUDIBASE_ENVIRONMENT,
} }

View File

@ -1,33 +1,11 @@
const EventEmitter = require("events").EventEmitter const EventEmitter = require("events").EventEmitter
const CouchDB = require("../db")
const { Orchestrator, serverStrategy } = require("./workflow") /**
* 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
* future.
*/
const emitter = new EventEmitter() const emitter = new EventEmitter()
async function executeRelevantWorkflows(event, eventType) {
const db = new CouchDB(event.instanceId)
const workflowsToTrigger = await db.query("database/by_workflow_trigger", {
key: [eventType],
include_docs: true,
})
const workflows = workflowsToTrigger.rows.map(wf => wf.doc)
// Create orchestrator
const workflowOrchestrator = new Orchestrator()
workflowOrchestrator.strategy = serverStrategy
for (let workflow of workflows) {
workflowOrchestrator.execute(workflow, event)
}
}
emitter.on("record:save", async function(event) {
await executeRelevantWorkflows(event, "record:save")
})
emitter.on("record:delete", async function(event) {
await executeRelevantWorkflows(event, "record:delete")
})
module.exports = emitter module.exports = emitter

View File

@ -1,54 +0,0 @@
const mustache = require("mustache")
/**
* The workflow orchestrator is a class responsible for executing workflows.
* It relies on the strategy pattern, which allows composable behaviour to be
* passed into its execute() function. This allows custom execution behaviour based
* on where the orchestrator is run.
*
*/
exports.Orchestrator = class Orchestrator {
set strategy(strategy) {
this._strategy = strategy()
}
async execute(workflow, context) {
if (workflow.live) {
this._strategy.run(workflow.definition, context)
}
}
}
exports.serverStrategy = () => ({
context: {},
bindContextArgs: function(args) {
const mappedArgs = { ...args }
// bind the workflow action args to the workflow context, if required
for (let arg in args) {
const argValue = args[arg]
// We don't want to render mustache templates on non-strings
if (typeof argValue !== "string") continue
mappedArgs[arg] = mustache.render(argValue, { context: this.context })
}
return mappedArgs
},
run: async function(workflow, context) {
for (let block of workflow.steps) {
if (block.type === "CLIENT") continue
const action = require(`../api/controllers/workflow/actions/${block.actionId}`)
const response = await action({
args: this.bindContextArgs(block.args),
context,
})
this.context = {
...this.context,
[block.id]: response,
}
}
},
})

View File

@ -0,0 +1,16 @@
function validate(schema, property) {
// Return a Koa middleware function
return (ctx, next) => {
if (schema) {
const { error } = schema.validate(ctx[property])
if (error) {
ctx.throw(400, `Invalid ${property} - ${error.message}`)
}
}
return next()
}
}
module.exports.body = schema => {
return validate(schema, "body")
}

View File

@ -0,0 +1,112 @@
const userController = require("../api/controllers/user")
const recordController = require("../api/controllers/record")
const sgMail = require("@sendgrid/mail")
sgMail.setApiKey(process.env.SENDGRID_API_KEY)
let BUILTIN_ACTIONS = {
CREATE_USER: async function({ args, context }) {
const { username, password, accessLevelId } = args
const ctx = {
user: {
instanceId: context.instanceId,
},
request: {
body: { username, password, accessLevelId },
},
}
try {
const response = await userController.create(ctx)
return {
user: response,
}
} catch (err) {
console.error(err)
return {
user: null,
}
}
},
SAVE_RECORD: async function({ args, context }) {
const { model, ...record } = args.record
const ctx = {
params: {
instanceId: context.instanceId,
modelId: model._id,
},
request: {
body: record,
},
user: { instanceId: context.instanceId },
}
try {
await recordController.save(ctx)
return {
record: ctx.body,
}
} catch (err) {
console.error(err)
return {
record: null,
error: err.message,
}
}
},
SEND_EMAIL: async function({ args }) {
const msg = {
to: args.to,
from: args.from,
subject: args.subject,
text: args.text,
}
try {
await sgMail.send(msg)
return {
success: true,
...args,
}
} catch (err) {
console.error(err)
return {
success: false,
error: err.message,
}
}
},
DELETE_RECORD: async function({ args, context }) {
const { model, ...record } = args.record
// TODO: better logging of when actions are missed due to missing parameters
if (record.recordId == null || record.revId == null) {
return
}
let ctx = {
params: {
modelId: model._id,
recordId: record.recordId,
revId: record.revId,
},
user: { instanceId: context.instanceId },
}
try {
await recordController.destroy(ctx)
} catch (err) {
console.error(err)
return {
record: null,
error: err.message,
}
}
},
}
module.exports.getAction = async function(actionName) {
if (BUILTIN_ACTIONS[actionName] != null) {
return BUILTIN_ACTIONS[actionName]
}
// TODO: load async actions here
}

View File

@ -0,0 +1,31 @@
const triggers = require("./triggers")
const environment = require("../environment")
const workerFarm = require("worker-farm")
const singleThread = require("./thread")
let workers = workerFarm(require.resolve("./thread"))
function runWorker(job) {
return new Promise((resolve, reject) => {
workers(job, err => {
if (err) {
reject(err)
} else {
resolve()
}
})
})
}
/**
* This module is built purely to kick off the worker farm and manage the inputs/outputs
*/
module.exports.init = function() {
triggers.workflowQueue.process(async job => {
if (environment.BUDIBASE_ENVIRONMENT === "PRODUCTION") {
await runWorker(job)
} else {
await singleThread(job)
}
})
}

View File

@ -0,0 +1,24 @@
const wait = ms => new Promise(resolve => setTimeout(resolve, ms))
let LOGIC = {
DELAY: async function delay({ args }) {
await wait(args.time)
},
FILTER: async function filter({ args }) {
const { field, condition, value } = args
switch (condition) {
case "equals":
if (field !== value) return
break
default:
return
}
},
}
module.exports.getLogic = function(logicName) {
if (LOGIC[logicName] != null) {
return LOGIC[logicName]
}
}

View File

@ -0,0 +1,44 @@
let events = require("events")
// Bull works with a Job wrapper around all messages that contains a lot more information about
// the state of the message, implement this for the sake of maintaining API consistency
function newJob(queue, message) {
return {
timestamp: Date.now(),
queue: queue,
data: message,
}
}
// designed to replicate Bull (https://github.com/OptimalBits/bull) in memory as a sort of mock
class InMemoryQueue {
// opts is not used by this as there is no real use case when in memory, but is the same API as Bull
constructor(name, opts) {
this._name = name
this._opts = opts
this._messages = []
this._emitter = new events.EventEmitter()
}
// same API as bull, provide a callback and it will respond when messages are available
process(func) {
this._emitter.on("message", async () => {
if (this._messages.length <= 0) {
return
}
let msg = this._messages.shift()
let resp = func(msg)
if (resp.then != null) {
await resp
}
})
}
// simply puts a message to the queue and emits to the queue for processing
add(msg) {
this._messages.push(newJob(this._name, msg))
this._emitter.emit("message")
}
}
module.exports = InMemoryQueue

View File

@ -0,0 +1,68 @@
const mustache = require("mustache")
const actions = require("./actions")
const logic = require("./logic")
/**
* 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
* inputs and handles any outputs.
*/
class Orchestrator {
constructor(workflow) {
this._context = {}
this._workflow = workflow
}
async getStep(type, stepId) {
let step = null
if (type === "ACTION") {
step = await actions.getAction(stepId)
} else if (type === "LOGIC") {
step = logic.getLogic(stepId)
}
if (step == null) {
throw `Cannot find workflow step by name ${stepId}`
}
return step
}
async execute(context) {
let workflow = this._workflow
for (let block of workflow.definition.steps) {
let step = await this.getStep(block.type, block.stepId)
let args = { ...block.args }
// bind the workflow action args to the workflow context, if required
for (let arg of Object.keys(args)) {
const argValue = args[arg]
// We don't want to render mustache templates on non-strings
if (typeof argValue !== "string") continue
args[arg] = mustache.render(argValue, { context: this._context })
}
const response = await step({
args,
context,
})
this._context = {
...this._context,
[block.id]: response,
}
}
}
}
// 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)
await workflowOrchestrator.execute(job.data.event)
if (cb) {
cb()
}
} catch (err) {
if (cb) {
cb(err)
}
}
}

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