merge with master
This commit is contained in:
commit
e609414f57
|
@ -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*'
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "0.1.19",
|
"version": "0.1.21",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,4 +5,5 @@ package-lock.json
|
||||||
release/
|
release/
|
||||||
dist/
|
dist/
|
||||||
cypress/screenshots
|
cypress/screenshots
|
||||||
|
cypress/videos
|
||||||
routify
|
routify
|
||||||
|
|
|
@ -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")
|
||||||
|
|
||||||
})
|
})
|
||||||
})
|
})
|
Binary file not shown.
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -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",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
`;
|
|
|
@ -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",
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 |
|
@ -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"
|
||||||
|
|
|
@ -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
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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,
|
||||||
|
},
|
||||||
|
]
|
|
@ -1 +0,0 @@
|
||||||
export { default } from "./EventsEditor.svelte"
|
|
|
@ -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
|
@ -0,0 +1,3 @@
|
||||||
|
import "@fortawesome/fontawesome-free/js/all.js"
|
||||||
|
|
||||||
|
export { default as IconSelect } from "./IconSelect.svelte"
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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']) }} />
|
||||||
|
|
|
@ -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 },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
@ -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.")
|
||||||
|
|
|
@ -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>
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 |
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -50,7 +50,6 @@ export const createApp = ({
|
||||||
treeNode,
|
treeNode,
|
||||||
onScreenSlotRendered,
|
onScreenSlotRendered,
|
||||||
setupState: stateManager.setup,
|
setupState: stateManager.setup,
|
||||||
getCurrentState: stateManager.getCurrentState,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return getInitialiseParams
|
return getInitialiseParams
|
||||||
|
|
|
@ -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"],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 })
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 . ./
|
||||||
|
|
|
@ -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"
|
||||||
},
|
},
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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,
|
||||||
}
|
}
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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", () => {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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`)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -72,8 +72,14 @@ describe("/views", () => {
|
||||||
type: "text",
|
type: "text",
|
||||||
constraints: {
|
constraints: {
|
||||||
type: "string"
|
type: "string"
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
|
description: {
|
||||||
|
type: "text",
|
||||||
|
constraints: {
|
||||||
|
type: "string"
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -23,7 +23,7 @@ const TEST_WORKFLOW = {
|
||||||
|
|
||||||
],
|
],
|
||||||
next: {
|
next: {
|
||||||
actionId: "abc123",
|
stepId: "abc123",
|
||||||
type: "SERVER",
|
type: "SERVER",
|
||||||
conditions: {
|
conditions: {
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
})
|
})
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
|
@ -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")
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -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]
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
|
@ -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
Loading…
Reference in New Issue