merge
This commit is contained in:
commit
b186f0fbb2
|
@ -16,7 +16,7 @@ jobs:
|
|||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [10.x]
|
||||
node-version: [12.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
|
|
@ -15,7 +15,7 @@ jobs:
|
|||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest, ubuntu-latest, windows-latest]
|
||||
node-version: [10.x]
|
||||
node-version: [12.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
|
|
@ -4,6 +4,6 @@ node_modules_win
|
|||
package-lock.json
|
||||
release/
|
||||
dist/
|
||||
cypress/screenshots
|
||||
cypress/videos
|
||||
routify
|
||||
cypress/videos
|
||||
cypress/screenshots
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
context("Create a automation", () => {
|
||||
before(() => {
|
||||
cy.server()
|
||||
cy.visit("localhost:4001/_builder")
|
||||
|
||||
cy.createApp(
|
||||
"Automation Test App",
|
||||
"This app is used to test that automations do in fact work!"
|
||||
)
|
||||
})
|
||||
|
||||
// https://on.cypress.io/interacting-with-elements
|
||||
it("should create a automation", () => {
|
||||
cy.createTestTableWithData()
|
||||
|
||||
cy.contains("automate").click()
|
||||
cy.contains("Create New Automation").click()
|
||||
cy.get("input").type("Add Record")
|
||||
cy.contains("Save").click()
|
||||
|
||||
// Add trigger
|
||||
cy.get("[data-cy=add-automation-component]").click()
|
||||
cy.get("[data-cy=RECORD_SAVED]").click()
|
||||
cy.get("[data-cy=automation-block-setup]").within(() => {
|
||||
cy.get("select")
|
||||
.first()
|
||||
.select("dog")
|
||||
})
|
||||
|
||||
// Create action
|
||||
cy.get("[data-cy=SAVE_RECORD]").click()
|
||||
cy.get("[data-cy=automation-block-setup]").within(() => {
|
||||
cy.get("select")
|
||||
.first()
|
||||
.select("dog")
|
||||
cy.get("input")
|
||||
.first()
|
||||
.type("goodboy")
|
||||
cy.get("input")
|
||||
.eq(1)
|
||||
.type("11")
|
||||
})
|
||||
|
||||
// Save
|
||||
cy.contains("Save Automation").click()
|
||||
|
||||
// Activate Automation
|
||||
cy.get("[data-cy=activate-automation]").click()
|
||||
cy.contains("Add Record").should("be.visible")
|
||||
cy.get(".stop-button.highlighted").should("be.visible")
|
||||
})
|
||||
|
||||
it("should add record when a new record is added", () => {
|
||||
cy.contains("backend").click()
|
||||
cy.addRecord(["Rover", 15])
|
||||
cy.reload()
|
||||
cy.contains("goodboy").should("have.text", "goodboy")
|
||||
})
|
||||
})
|
|
@ -1,52 +0,0 @@
|
|||
context("Create a workflow", () => {
|
||||
before(() => {
|
||||
cy.server()
|
||||
cy.visit("localhost:4001/_builder")
|
||||
|
||||
cy.createApp(
|
||||
"Workflow Test App",
|
||||
"This app is used to test that workflows do in fact work!"
|
||||
)
|
||||
})
|
||||
|
||||
// https://on.cypress.io/interacting-with-elements
|
||||
it("should create a workflow", () => {
|
||||
cy.createTestTableWithData()
|
||||
|
||||
cy.contains("workflow").click()
|
||||
cy.contains("Create New Workflow").click()
|
||||
cy.get("input").type("Add Record")
|
||||
cy.contains("Save").click()
|
||||
|
||||
// Add trigger
|
||||
cy.get("[data-cy=add-workflow-component]").click()
|
||||
cy.get("[data-cy=RECORD_SAVED]").click()
|
||||
cy.get(".budibase__input").select("dog")
|
||||
|
||||
// Create action
|
||||
cy.get("[data-cy=SAVE_RECORD]").click()
|
||||
cy.get(".budibase__input").select("dog")
|
||||
cy.get(".container input")
|
||||
.first()
|
||||
.type("goodboy")
|
||||
cy.get(".container input")
|
||||
.eq(1)
|
||||
.type("11")
|
||||
|
||||
// Save
|
||||
cy.contains("Save Workflow").click()
|
||||
|
||||
// Activate Workflow
|
||||
cy.get("[data-cy=activate-workflow]").click()
|
||||
cy.contains("Add Record").should("be.visible")
|
||||
cy.get(".stop-button.highlighted").should("be.visible")
|
||||
})
|
||||
|
||||
it("should add record when a new record is added", () => {
|
||||
cy.contains("backend").click()
|
||||
|
||||
cy.addRecord(["Rover", 15])
|
||||
cy.reload()
|
||||
cy.contains("goodboy").should("have.text", "goodboy")
|
||||
})
|
||||
})
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -100,7 +100,7 @@
|
|||
"jest": "^24.8.0",
|
||||
"ncp": "^2.0.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"rollup": "^1.12.0",
|
||||
"rollup": "^2.11.2",
|
||||
"rollup-plugin-alias": "^1.5.2",
|
||||
"rollup-plugin-commonjs": "^10.0.0",
|
||||
"rollup-plugin-copy": "^3.0.0",
|
||||
|
@ -110,10 +110,10 @@
|
|||
"rollup-plugin-node-globals": "^1.4.0",
|
||||
"rollup-plugin-node-resolve": "^5.2.0",
|
||||
"rollup-plugin-svelte": "^5.0.3",
|
||||
"rollup-plugin-terser": "^4.0.4",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"rollup-plugin-url": "^2.2.2",
|
||||
"start-server-and-test": "^1.11.0",
|
||||
"svelte": "3.23.x",
|
||||
"svelte": "^3.24.1",
|
||||
"svelte-jester": "^1.0.6"
|
||||
},
|
||||
"gitHead": "115189f72a850bfb52b65ec61d932531bf327072"
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { getStore } from "./store"
|
||||
import { getBackendUiStore } from "./store/backend"
|
||||
import { getWorkflowStore } from "./store/workflow/"
|
||||
import { getAutomationStore } from "./store/automation/"
|
||||
import analytics from "../analytics"
|
||||
|
||||
export const store = getStore()
|
||||
export const backendUiStore = getBackendUiStore()
|
||||
export const workflowStore = getWorkflowStore()
|
||||
export const automationStore = getAutomationStore()
|
||||
|
||||
export const initialise = async () => {
|
||||
try {
|
||||
|
|
|
@ -1,59 +1,59 @@
|
|||
import { generate } from "shortid"
|
||||
|
||||
/**
|
||||
* Class responsible for the traversing of the workflow definition.
|
||||
* Workflow definitions are stored in linked lists.
|
||||
* Class responsible for the traversing of the automation definition.
|
||||
* Automation definitions are stored in linked lists.
|
||||
*/
|
||||
export default class Workflow {
|
||||
constructor(workflow) {
|
||||
this.workflow = workflow
|
||||
export default class Automation {
|
||||
constructor(automation) {
|
||||
this.automation = automation
|
||||
}
|
||||
|
||||
hasTrigger() {
|
||||
return this.workflow.definition.trigger
|
||||
return this.automation.definition.trigger
|
||||
}
|
||||
|
||||
addBlock(block) {
|
||||
// Make sure to add trigger if doesn't exist
|
||||
if (!this.hasTrigger() && block.type === "TRIGGER") {
|
||||
const trigger = { id: generate(), ...block }
|
||||
this.workflow.definition.trigger = trigger
|
||||
this.automation.definition.trigger = trigger
|
||||
return trigger
|
||||
}
|
||||
|
||||
const newBlock = { id: generate(), ...block }
|
||||
this.workflow.definition.steps = [
|
||||
...this.workflow.definition.steps,
|
||||
this.automation.definition.steps = [
|
||||
...this.automation.definition.steps,
|
||||
newBlock,
|
||||
]
|
||||
return newBlock
|
||||
}
|
||||
|
||||
updateBlock(updatedBlock, id) {
|
||||
const { steps, trigger } = this.workflow.definition
|
||||
const { steps, trigger } = this.automation.definition
|
||||
|
||||
if (trigger && trigger.id === id) {
|
||||
this.workflow.definition.trigger = updatedBlock
|
||||
this.automation.definition.trigger = updatedBlock
|
||||
return
|
||||
}
|
||||
|
||||
const stepIdx = steps.findIndex(step => step.id === id)
|
||||
if (stepIdx < 0) throw new Error("Block not found.")
|
||||
steps.splice(stepIdx, 1, updatedBlock)
|
||||
this.workflow.definition.steps = steps
|
||||
this.automation.definition.steps = steps
|
||||
}
|
||||
|
||||
deleteBlock(id) {
|
||||
const { steps, trigger } = this.workflow.definition
|
||||
const { steps, trigger } = this.automation.definition
|
||||
|
||||
if (trigger && trigger.id === id) {
|
||||
this.workflow.definition.trigger = null
|
||||
this.automation.definition.trigger = null
|
||||
return
|
||||
}
|
||||
|
||||
const stepIdx = steps.findIndex(step => step.id === id)
|
||||
if (stepIdx < 0) throw new Error("Block not found.")
|
||||
steps.splice(stepIdx, 1)
|
||||
this.workflow.definition.steps = steps
|
||||
this.automation.definition.steps = steps
|
||||
}
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
import { writable } from "svelte/store"
|
||||
import api from "../../api"
|
||||
import Automation from "./Automation"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
|
||||
const automationActions = store => ({
|
||||
fetch: async () => {
|
||||
const responses = await Promise.all([
|
||||
api.get(`/api/automations`),
|
||||
api.get(`/api/automations/definitions/list`),
|
||||
])
|
||||
const jsonResponses = await Promise.all(responses.map(x => x.json()))
|
||||
store.update(state => {
|
||||
state.automations = jsonResponses[0]
|
||||
state.blockDefinitions = {
|
||||
TRIGGER: jsonResponses[1].trigger,
|
||||
ACTION: jsonResponses[1].action,
|
||||
LOGIC: jsonResponses[1].logic,
|
||||
}
|
||||
return state
|
||||
})
|
||||
},
|
||||
create: async ({ name }) => {
|
||||
const automation = {
|
||||
name,
|
||||
type: "automation",
|
||||
definition: {
|
||||
steps: [],
|
||||
},
|
||||
}
|
||||
const CREATE_AUTOMATION_URL = `/api/automations`
|
||||
const response = await api.post(CREATE_AUTOMATION_URL, automation)
|
||||
const json = await response.json()
|
||||
store.update(state => {
|
||||
state.automations = [...state.automations, json.automation]
|
||||
store.actions.select(json.automation)
|
||||
return state
|
||||
})
|
||||
},
|
||||
save: async ({ automation }) => {
|
||||
const UPDATE_AUTOMATION_URL = `/api/automations`
|
||||
const response = await api.put(UPDATE_AUTOMATION_URL, automation)
|
||||
const json = await response.json()
|
||||
store.update(state => {
|
||||
const existingIdx = state.automations.findIndex(
|
||||
existing => existing._id === automation._id
|
||||
)
|
||||
state.automations.splice(existingIdx, 1, json.automation)
|
||||
state.automations = state.automations
|
||||
store.actions.select(json.automation)
|
||||
return state
|
||||
})
|
||||
},
|
||||
delete: async ({ automation }) => {
|
||||
const { _id, _rev } = automation
|
||||
const DELETE_AUTOMATION_URL = `/api/automations/${_id}/${_rev}`
|
||||
await api.delete(DELETE_AUTOMATION_URL)
|
||||
|
||||
store.update(state => {
|
||||
const existingIdx = state.automations.findIndex(
|
||||
existing => existing._id === _id
|
||||
)
|
||||
state.automations.splice(existingIdx, 1)
|
||||
state.automations = state.automations
|
||||
state.selectedAutomation = null
|
||||
state.selectedBlock = null
|
||||
return state
|
||||
})
|
||||
},
|
||||
trigger: async ({ automation }) => {
|
||||
const { _id } = automation
|
||||
const TRIGGER_AUTOMATION_URL = `/api/automations/${_id}/trigger`
|
||||
return await api.post(TRIGGER_AUTOMATION_URL)
|
||||
},
|
||||
select: automation => {
|
||||
store.update(state => {
|
||||
state.selectedAutomation = new Automation(cloneDeep(automation))
|
||||
state.selectedBlock = null
|
||||
return state
|
||||
})
|
||||
},
|
||||
addBlockToAutomation: block => {
|
||||
store.update(state => {
|
||||
const newBlock = state.selectedAutomation.addBlock(cloneDeep(block))
|
||||
state.selectedBlock = newBlock
|
||||
return state
|
||||
})
|
||||
},
|
||||
deleteAutomationBlock: block => {
|
||||
store.update(state => {
|
||||
const idx = state.selectedAutomation.automation.definition.steps.findIndex(
|
||||
x => x.id === block.id
|
||||
)
|
||||
state.selectedAutomation.deleteBlock(block.id)
|
||||
|
||||
// Select next closest step
|
||||
const steps = state.selectedAutomation.automation.definition.steps
|
||||
let nextSelectedBlock
|
||||
if (steps[idx] != null) {
|
||||
nextSelectedBlock = steps[idx]
|
||||
} else if (steps[idx - 1] != null) {
|
||||
nextSelectedBlock = steps[idx - 1]
|
||||
} else {
|
||||
nextSelectedBlock =
|
||||
state.selectedAutomation.automation.definition.trigger || null
|
||||
}
|
||||
state.selectedBlock = nextSelectedBlock
|
||||
return state
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export const getAutomationStore = () => {
|
||||
const INITIAL_AUTOMATION_STATE = {
|
||||
automations: [],
|
||||
blockDefinitions: {
|
||||
TRIGGER: [],
|
||||
ACTION: [],
|
||||
LOGIC: [],
|
||||
},
|
||||
selectedAutomation: null,
|
||||
}
|
||||
const store = writable(INITIAL_AUTOMATION_STATE)
|
||||
store.actions = automationActions(store)
|
||||
return store
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
import Automation from "../Automation"
|
||||
import TEST_AUTOMATION from "./testAutomation"
|
||||
|
||||
const TEST_BLOCK = {
|
||||
id: "AUXJQGZY7",
|
||||
name: "Delay",
|
||||
icon: "ri-time-fill",
|
||||
tagline: "Delay for <b>{{time}}</b> milliseconds",
|
||||
description: "Delay the automation until an amount of time has passed.",
|
||||
params: { time: "number" },
|
||||
type: "LOGIC",
|
||||
args: { time: "5000" },
|
||||
stepId: "DELAY",
|
||||
}
|
||||
|
||||
describe("Automation Data Object", () => {
|
||||
let automation
|
||||
|
||||
beforeEach(() => {
|
||||
automation = new Automation({ ...TEST_AUTOMATION })
|
||||
})
|
||||
|
||||
it("adds a automation block to the automation", () => {
|
||||
automation.addBlock(TEST_BLOCK)
|
||||
expect(automation.automation.definition)
|
||||
})
|
||||
|
||||
it("updates a automation block with new attributes", () => {
|
||||
const firstBlock = automation.automation.definition.steps[0]
|
||||
const updatedBlock = {
|
||||
...firstBlock,
|
||||
name: "UPDATED",
|
||||
}
|
||||
automation.updateBlock(updatedBlock, firstBlock.id)
|
||||
expect(automation.automation.definition.steps[0]).toEqual(updatedBlock)
|
||||
})
|
||||
|
||||
it("deletes a automation block successfully", () => {
|
||||
const { steps } = automation.automation.definition
|
||||
const originalLength = steps.length
|
||||
|
||||
const lastBlock = steps[steps.length - 1]
|
||||
automation.deleteBlock(lastBlock.id)
|
||||
expect(automation.automation.definition.steps.length).toBeLessThan(
|
||||
originalLength
|
||||
)
|
||||
})
|
||||
})
|
|
@ -1,5 +1,5 @@
|
|||
export default {
|
||||
name: "Test workflow",
|
||||
name: "Test automation",
|
||||
definition: {
|
||||
steps: [
|
||||
{
|
||||
|
@ -68,7 +68,7 @@ export default {
|
|||
stepId: "RECORD_SAVED",
|
||||
},
|
||||
},
|
||||
type: "workflow",
|
||||
type: "automation",
|
||||
ok: true,
|
||||
id: "b384f861f4754e1693835324a7fcca62",
|
||||
rev: "1-aa1c2cbd868ef02e26f8fad531dd7e37",
|
|
@ -87,7 +87,7 @@ export const getBackendUiStore = () => {
|
|||
state.models = state.models.filter(
|
||||
existing => existing._id !== model._id
|
||||
)
|
||||
state.selectedModel = state.models[0] || {}
|
||||
state.selectedModel = {}
|
||||
return state
|
||||
})
|
||||
},
|
||||
|
|
|
@ -1,125 +0,0 @@
|
|||
import { writable } from "svelte/store"
|
||||
import api from "../../api"
|
||||
import Workflow from "./Workflow"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
|
||||
const workflowActions = store => ({
|
||||
fetch: async () => {
|
||||
const responses = await Promise.all([
|
||||
api.get(`/api/workflows`),
|
||||
api.get(`/api/workflows/definitions/list`),
|
||||
])
|
||||
const jsonResponses = await Promise.all(responses.map(x => x.json()))
|
||||
store.update(state => {
|
||||
state.workflows = jsonResponses[0]
|
||||
state.blockDefinitions = {
|
||||
TRIGGER: jsonResponses[1].trigger,
|
||||
ACTION: jsonResponses[1].action,
|
||||
LOGIC: jsonResponses[1].logic,
|
||||
}
|
||||
return state
|
||||
})
|
||||
},
|
||||
create: async ({ name }) => {
|
||||
const workflow = {
|
||||
name,
|
||||
definition: {
|
||||
steps: [],
|
||||
},
|
||||
}
|
||||
const CREATE_WORKFLOW_URL = `/api/workflows`
|
||||
const response = await api.post(CREATE_WORKFLOW_URL, workflow)
|
||||
const json = await response.json()
|
||||
store.update(state => {
|
||||
state.workflows = [...state.workflows, json.workflow]
|
||||
store.actions.select(json.workflow)
|
||||
return state
|
||||
})
|
||||
},
|
||||
save: async ({ workflow }) => {
|
||||
const UPDATE_WORKFLOW_URL = `/api/workflows`
|
||||
const response = await api.put(UPDATE_WORKFLOW_URL, workflow)
|
||||
const json = await response.json()
|
||||
store.update(state => {
|
||||
const existingIdx = state.workflows.findIndex(
|
||||
existing => existing._id === workflow._id
|
||||
)
|
||||
state.workflows.splice(existingIdx, 1, json.workflow)
|
||||
state.workflows = state.workflows
|
||||
store.actions.select(json.workflow)
|
||||
return state
|
||||
})
|
||||
},
|
||||
delete: async ({ workflow }) => {
|
||||
const { _id, _rev } = workflow
|
||||
const DELETE_WORKFLOW_URL = `/api/workflows/${_id}/${_rev}`
|
||||
await api.delete(DELETE_WORKFLOW_URL)
|
||||
|
||||
store.update(state => {
|
||||
const existingIdx = state.workflows.findIndex(
|
||||
existing => existing._id === _id
|
||||
)
|
||||
state.workflows.splice(existingIdx, 1)
|
||||
state.workflows = state.workflows
|
||||
state.selectedWorkflow = null
|
||||
state.selectedBlock = null
|
||||
return state
|
||||
})
|
||||
},
|
||||
trigger: async ({ workflow }) => {
|
||||
const { _id } = workflow
|
||||
const TRIGGER_WORKFLOW_URL = `/api/workflows/${_id}/trigger`
|
||||
return await api.post(TRIGGER_WORKFLOW_URL)
|
||||
},
|
||||
select: workflow => {
|
||||
store.update(state => {
|
||||
state.selectedWorkflow = new Workflow(cloneDeep(workflow))
|
||||
state.selectedBlock = null
|
||||
return state
|
||||
})
|
||||
},
|
||||
addBlockToWorkflow: block => {
|
||||
store.update(state => {
|
||||
const newBlock = state.selectedWorkflow.addBlock(cloneDeep(block))
|
||||
state.selectedBlock = newBlock
|
||||
return state
|
||||
})
|
||||
},
|
||||
deleteWorkflowBlock: block => {
|
||||
store.update(state => {
|
||||
const idx = state.selectedWorkflow.workflow.definition.steps.findIndex(
|
||||
x => x.id === block.id
|
||||
)
|
||||
state.selectedWorkflow.deleteBlock(block.id)
|
||||
|
||||
// Select next closest step
|
||||
const steps = state.selectedWorkflow.workflow.definition.steps
|
||||
let nextSelectedBlock
|
||||
if (steps[idx] != null) {
|
||||
nextSelectedBlock = steps[idx]
|
||||
} else if (steps[idx - 1] != null) {
|
||||
nextSelectedBlock = steps[idx - 1]
|
||||
} else {
|
||||
nextSelectedBlock =
|
||||
state.selectedWorkflow.workflow.definition.trigger || null
|
||||
}
|
||||
state.selectedBlock = nextSelectedBlock
|
||||
return state
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export const getWorkflowStore = () => {
|
||||
const INITIAL_WORKFLOW_STATE = {
|
||||
workflows: [],
|
||||
blockDefinitions: {
|
||||
TRIGGER: [],
|
||||
ACTION: [],
|
||||
LOGIC: [],
|
||||
},
|
||||
selectedWorkflow: null,
|
||||
}
|
||||
const store = writable(INITIAL_WORKFLOW_STATE)
|
||||
store.actions = workflowActions(store)
|
||||
return store
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
import Workflow from "../Workflow"
|
||||
import TEST_WORKFLOW from "./testWorkflow"
|
||||
|
||||
const TEST_BLOCK = {
|
||||
id: "AUXJQGZY7",
|
||||
name: "Delay",
|
||||
icon: "ri-time-fill",
|
||||
tagline: "Delay for <b>{{time}}</b> milliseconds",
|
||||
description: "Delay the workflow until an amount of time has passed.",
|
||||
params: { time: "number" },
|
||||
type: "LOGIC",
|
||||
args: { time: "5000" },
|
||||
stepId: "DELAY",
|
||||
}
|
||||
|
||||
describe("Workflow Data Object", () => {
|
||||
let workflow
|
||||
|
||||
beforeEach(() => {
|
||||
workflow = new Workflow({ ...TEST_WORKFLOW })
|
||||
})
|
||||
|
||||
it("adds a workflow block to the workflow", () => {
|
||||
workflow.addBlock(TEST_BLOCK)
|
||||
expect(workflow.workflow.definition)
|
||||
})
|
||||
|
||||
it("updates a workflow block with new attributes", () => {
|
||||
const firstBlock = workflow.workflow.definition.steps[0]
|
||||
const updatedBlock = {
|
||||
...firstBlock,
|
||||
name: "UPDATED",
|
||||
}
|
||||
workflow.updateBlock(updatedBlock, firstBlock.id)
|
||||
expect(workflow.workflow.definition.steps[0]).toEqual(updatedBlock)
|
||||
})
|
||||
|
||||
it("deletes a workflow block successfully", () => {
|
||||
const { steps } = workflow.workflow.definition
|
||||
const originalLength = steps.length
|
||||
|
||||
const lastBlock = steps[steps.length - 1]
|
||||
workflow.deleteBlock(lastBlock.id)
|
||||
expect(workflow.workflow.definition.steps.length).toBeLessThan(
|
||||
originalLength
|
||||
)
|
||||
})
|
||||
})
|
|
@ -1,49 +1,48 @@
|
|||
<script>
|
||||
import { afterUpdate } from "svelte"
|
||||
import { workflowStore, backendUiStore } from "builderStore"
|
||||
import { automationStore, backendUiStore } from "builderStore"
|
||||
import { notifier } from "builderStore/store/notifications"
|
||||
import Flowchart from "./flowchart/FlowChart.svelte"
|
||||
|
||||
$: workflow =
|
||||
$workflowStore.selectedWorkflow && $workflowStore.selectedWorkflow.workflow
|
||||
$: workflowLive = workflow && workflow.live
|
||||
$: automation = $automationStore.selectedAutomation?.automation
|
||||
$: automationLive = automation?.live
|
||||
$: instanceId = $backendUiStore.selectedDatabase._id
|
||||
|
||||
function onSelect(block) {
|
||||
workflowStore.update(state => {
|
||||
automationStore.update(state => {
|
||||
state.selectedBlock = block
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
function setWorkflowLive(live) {
|
||||
workflow.live = live
|
||||
workflowStore.actions.save({ instanceId, workflow })
|
||||
function setAutomationLive(live) {
|
||||
automation.live = live
|
||||
automationStore.actions.save({ instanceId, automation })
|
||||
if (live) {
|
||||
notifier.info(`Workflow ${workflow.name} enabled.`)
|
||||
notifier.info(`Automation ${automation.name} enabled.`)
|
||||
} else {
|
||||
notifier.danger(`Workflow ${workflow.name} disabled.`)
|
||||
notifier.danger(`Automation ${automation.name} disabled.`)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<section>
|
||||
<Flowchart {workflow} {onSelect} />
|
||||
<Flowchart {automation} {onSelect} />
|
||||
</section>
|
||||
<footer>
|
||||
{#if workflow}
|
||||
{#if automation}
|
||||
<button
|
||||
class:highlighted={workflowLive}
|
||||
class:hoverable={workflowLive}
|
||||
class:highlighted={automationLive}
|
||||
class:hoverable={automationLive}
|
||||
class="stop-button hoverable">
|
||||
<i class="ri-stop-fill" on:click={() => setWorkflowLive(false)} />
|
||||
<i class="ri-stop-fill" on:click={() => setAutomationLive(false)} />
|
||||
</button>
|
||||
<button
|
||||
class:highlighted={!workflowLive}
|
||||
class:hoverable={!workflowLive}
|
||||
class:highlighted={!automationLive}
|
||||
class:hoverable={!automationLive}
|
||||
class="play-button hoverable"
|
||||
data-cy="activate-workflow"
|
||||
on:click={() => setWorkflowLive(true)}>
|
||||
data-cy="activate-automation"
|
||||
on:click={() => setAutomationLive(true)}>
|
||||
<i class="ri-play-fill" />
|
||||
</button>
|
||||
{/if}
|
||||
|
@ -62,7 +61,7 @@
|
|||
|
||||
footer {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
bottom: var(--spacing-xl);
|
||||
right: 30px;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
|
@ -81,7 +80,7 @@
|
|||
justify-content: center;
|
||||
}
|
||||
footer > button:first-child {
|
||||
margin-right: 20px;
|
||||
margin-right: var(--spacing-m);
|
||||
}
|
||||
|
||||
.play-button.highlighted {
|
|
@ -10,6 +10,6 @@
|
|||
|
||||
<style>
|
||||
svg {
|
||||
margin: 8px 0;
|
||||
margin: var(--spacing-m) 0;
|
||||
}
|
||||
</style>
|
Before Width: | Height: | Size: 290 B After Width: | Height: | Size: 303 B |
|
@ -0,0 +1,78 @@
|
|||
<script>
|
||||
import mustache from "mustache"
|
||||
import { get } from "lodash/fp"
|
||||
import { backendUiStore } from "builderStore"
|
||||
|
||||
export let block
|
||||
|
||||
$: inputs = enrichInputs(block.inputs)
|
||||
$: tagline = formatTagline(block.tagline, block.schema, inputs)
|
||||
$: html = (tagline || "")
|
||||
.replace(/{{\s*/g, "<span>")
|
||||
.replace(/\s*}}/g, "</span>")
|
||||
|
||||
function enrichInputs(inputs) {
|
||||
let enrichedInputs = { ...inputs, enriched: {} }
|
||||
const modelId = inputs.modelId || inputs.record?.modelId
|
||||
if (modelId) {
|
||||
enrichedInputs.enriched.model = $backendUiStore.models.find(
|
||||
model => model._id === modelId
|
||||
)
|
||||
}
|
||||
return enrichedInputs
|
||||
}
|
||||
|
||||
function formatTagline(tagline, schema, inputs) {
|
||||
// Add bold tags around inputs
|
||||
let formattedTagline = tagline
|
||||
.replace(/{{/g, "<b>{{")
|
||||
.replace(/}}/, "}}</b>")
|
||||
|
||||
// Extract schema paths for any input bindings
|
||||
let inputPaths = formattedTagline.match(/{{\s*\S+\s*}}/g) || []
|
||||
inputPaths = inputPaths.map(path => path.replace(/[{}]/g, "").trim())
|
||||
const schemaPaths = inputPaths.map(path =>
|
||||
path.replace(/\./g, ".properties.")
|
||||
)
|
||||
|
||||
// Replace any enum bindings with their pretty equivalents
|
||||
schemaPaths.forEach((path, idx) => {
|
||||
const prettyValues = get(`${path}.pretty`, schema)
|
||||
if (prettyValues) {
|
||||
const enumValues = get(`${path}.enum`, schema)
|
||||
const inputPath = inputPaths[idx]
|
||||
const value = get(inputPath, { inputs })
|
||||
const valueIdx = enumValues.indexOf(value)
|
||||
const prettyValue = prettyValues[valueIdx]
|
||||
if (prettyValue == null) {
|
||||
return
|
||||
}
|
||||
formattedTagline = formattedTagline.replace(
|
||||
new RegExp(`{{\s*${inputPath}\s*}}`),
|
||||
prettyValue
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// Fill in bindings with mustache
|
||||
return mustache.render(formattedTagline, { inputs })
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{@html html}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div {
|
||||
line-height: 1.25;
|
||||
}
|
||||
div :global(span) {
|
||||
font-size: 0.9em;
|
||||
background-color: var(--purple-light);
|
||||
padding: var(--spacing-xs);
|
||||
border-radius: var(--border-radius-m);
|
||||
display: inline-block;
|
||||
margin: 1px;
|
||||
}
|
||||
</style>
|
|
@ -4,17 +4,17 @@
|
|||
import { flip } from "svelte/animate"
|
||||
import { fade, fly } from "svelte/transition"
|
||||
|
||||
export let workflow
|
||||
export let automation
|
||||
export let onSelect
|
||||
let blocks
|
||||
|
||||
$: {
|
||||
blocks = []
|
||||
if (workflow) {
|
||||
if (workflow.definition.trigger) {
|
||||
blocks.push(workflow.definition.trigger)
|
||||
if (automation) {
|
||||
if (automation.definition.trigger) {
|
||||
blocks.push(automation.definition.trigger)
|
||||
}
|
||||
blocks = blocks.concat(workflow.definition.steps || [])
|
||||
blocks = blocks.concat(automation.definition.steps || [])
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -37,7 +37,7 @@
|
|||
<style>
|
||||
section {
|
||||
position: absolute;
|
||||
padding: 20px 40px;
|
||||
padding: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
|
@ -0,0 +1,103 @@
|
|||
<script>
|
||||
import { automationStore } from "builderStore"
|
||||
import AutomationBlockTagline from "./AutomationBlockTagline.svelte"
|
||||
|
||||
export let onSelect
|
||||
export let block
|
||||
let selected
|
||||
|
||||
$: selected = $automationStore.selectedBlock?.id === block.id
|
||||
$: steps =
|
||||
$automationStore.selectedAutomation?.automation?.definition?.steps ?? []
|
||||
$: blockIdx = steps.findIndex(step => step.id === block.id)
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={`block ${block.type} hoverable`}
|
||||
class:selected
|
||||
on:click={() => onSelect(block)}>
|
||||
<header>
|
||||
{#if block.type === 'TRIGGER'}
|
||||
<i class="ri-lightbulb-fill" />
|
||||
<span>When this happens...</span>
|
||||
{:else if block.type === 'ACTION'}
|
||||
<i class="ri-flashlight-fill" />
|
||||
<span>Do this...</span>
|
||||
{:else if block.type === 'LOGIC'}
|
||||
<i class="ri-git-branch-line" />
|
||||
<span>Only continue if...</span>
|
||||
{/if}
|
||||
<div class="label">
|
||||
{#if block.type === 'TRIGGER'}Trigger{:else}Step {blockIdx + 1}{/if}
|
||||
</div>
|
||||
</header>
|
||||
<hr />
|
||||
<p>
|
||||
<AutomationBlockTagline {block} />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.block {
|
||||
width: 360px;
|
||||
padding: 20px;
|
||||
border-radius: var(--border-radius-m);
|
||||
transition: 0.3s all ease;
|
||||
box-shadow: 0 4px 30px 0 rgba(57, 60, 68, 0.08);
|
||||
background-color: var(--ink);
|
||||
font-size: 16px;
|
||||
color: var(--white);
|
||||
}
|
||||
.block.selected,
|
||||
.block:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 4px 30px 0 rgba(57, 60, 68, 0.15);
|
||||
}
|
||||
|
||||
header {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
header span {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
header .label {
|
||||
font-size: 14px;
|
||||
padding: var(--spacing-s);
|
||||
color: var(--grey-8);
|
||||
border-radius: var(--border-radius-m);
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
header i {
|
||||
font-size: 20px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.ACTION {
|
||||
background-color: var(--white);
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.TRIGGER {
|
||||
background-color: var(--ink);
|
||||
color: var(--white);
|
||||
}
|
||||
.TRIGGER header .label {
|
||||
background-color: var(--grey-9);
|
||||
color: var(--grey-5);
|
||||
}
|
||||
|
||||
.LOGIC {
|
||||
background-color: var(--blue-light);
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
p {
|
||||
color: inherit;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,88 @@
|
|||
<script>
|
||||
import Modal from "svelte-simple-modal"
|
||||
import { notifier } from "builderStore/store/notifications"
|
||||
import { onMount, getContext } from "svelte"
|
||||
import { backendUiStore, automationStore } from "builderStore"
|
||||
import CreateAutomationModal from "./CreateAutomationModal.svelte"
|
||||
import { Button } from "@budibase/bbui"
|
||||
|
||||
const { open, close } = getContext("simple-modal")
|
||||
|
||||
$: selectedAutomationId = $automationStore.selectedAutomation?.automation?._id
|
||||
|
||||
function newAutomation() {
|
||||
open(
|
||||
CreateAutomationModal,
|
||||
{
|
||||
onClosed: close,
|
||||
},
|
||||
{ styleContent: { padding: "0" } }
|
||||
)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
automationStore.actions.fetch()
|
||||
})
|
||||
</script>
|
||||
|
||||
<section>
|
||||
<Button purple wide on:click={newAutomation}>Create New Automation</Button>
|
||||
<ul>
|
||||
{#each $automationStore.automations as automation}
|
||||
<li
|
||||
class="automation-item"
|
||||
class:selected={automation._id === selectedAutomationId}
|
||||
on:click={() => automationStore.actions.select(automation)}>
|
||||
<i class="ri-stackshare-line" class:live={automation.live} />
|
||||
{automation.name}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: var(--spacing-xl) 0 0 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
i {
|
||||
color: var(--grey-6);
|
||||
}
|
||||
i.live {
|
||||
color: var(--purple);
|
||||
}
|
||||
|
||||
li {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.automation-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
border-radius: var(--border-radius-m);
|
||||
padding: var(--spacing-s) var(--spacing-m);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
color: var(--ink);
|
||||
}
|
||||
.automation-item i {
|
||||
font-size: 24px;
|
||||
margin-right: var(--spacing-m);
|
||||
}
|
||||
.automation-item:hover {
|
||||
cursor: pointer;
|
||||
background: var(--grey-1);
|
||||
}
|
||||
.automation-item.selected {
|
||||
background: var(--grey-2);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,95 @@
|
|||
<script>
|
||||
import { store, backendUiStore, automationStore } from "builderStore"
|
||||
import { notifier } from "builderStore/store/notifications"
|
||||
import ActionButton from "components/common/ActionButton.svelte"
|
||||
import { Input } from "@budibase/bbui"
|
||||
|
||||
export let onClosed
|
||||
|
||||
let name
|
||||
|
||||
$: valid = !!name
|
||||
$: instanceId = $backendUiStore.selectedDatabase._id
|
||||
$: appId = $store.appId
|
||||
|
||||
async function createAutomation() {
|
||||
await automationStore.actions.create({
|
||||
name,
|
||||
instanceId,
|
||||
})
|
||||
onClosed()
|
||||
notifier.success(`Automation ${name} created.`)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<header>
|
||||
<i class="ri-stackshare-line" />
|
||||
Create Automation
|
||||
</header>
|
||||
<div class="content">
|
||||
<Input bind:value={name} label="Name" />
|
||||
</div>
|
||||
<footer>
|
||||
<a href="https://docs.budibase.com">
|
||||
<i class="ri-information-line" />
|
||||
<span>Learn about automations</span>
|
||||
</a>
|
||||
<ActionButton secondary on:click={onClosed}>Cancel</ActionButton>
|
||||
<ActionButton disabled={!valid} on:click={createAutomation}>
|
||||
Save
|
||||
</ActionButton>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
padding: var(--spacing-xl);
|
||||
}
|
||||
|
||||
header {
|
||||
font-size: var(--font-size-xl);
|
||||
color: var(--ink);
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
header i {
|
||||
margin-right: var(--spacing-m);
|
||||
font-size: 20px;
|
||||
background: var(--purple);
|
||||
color: var(--white);
|
||||
padding: var(--spacing-s);
|
||||
border-radius: var(--border-radius-m);
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: var(--spacing-xl) 0;
|
||||
}
|
||||
|
||||
footer {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-gap: var(--spacing-m);
|
||||
grid-auto-columns: 3fr 1fr 1fr;
|
||||
}
|
||||
footer a {
|
||||
color: var(--ink);
|
||||
font-size: 14px;
|
||||
vertical-align: middle;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
}
|
||||
footer a span {
|
||||
text-decoration: underline;
|
||||
}
|
||||
footer i {
|
||||
font-size: 20px;
|
||||
margin-right: var(--spacing-m);
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,54 @@
|
|||
<script>
|
||||
import { automationStore } from "builderStore"
|
||||
import AutomationList from "./AutomationList/AutomationList.svelte"
|
||||
import BlockList from "./BlockList/BlockList.svelte"
|
||||
|
||||
let selectedTab = "AUTOMATIONS"
|
||||
</script>
|
||||
|
||||
<header>
|
||||
<span
|
||||
data-cy="automation-list"
|
||||
class="hoverable automation-header"
|
||||
class:selected={selectedTab === 'AUTOMATIONS'}
|
||||
on:click={() => (selectedTab = 'AUTOMATIONS')}>
|
||||
Automations
|
||||
</span>
|
||||
{#if $automationStore.selectedAutomation}
|
||||
<span
|
||||
data-cy="add-automation-component"
|
||||
class="hoverable"
|
||||
class:selected={selectedTab === 'ADD'}
|
||||
on:click={() => (selectedTab = 'ADD')}>
|
||||
Steps
|
||||
</span>
|
||||
{/if}
|
||||
</header>
|
||||
{#if selectedTab === 'AUTOMATIONS'}
|
||||
<AutomationList />
|
||||
{:else if selectedTab === 'ADD'}
|
||||
<BlockList />
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
header {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
background: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.automation-header {
|
||||
margin-right: var(--spacing-xl);
|
||||
}
|
||||
|
||||
span:not(.selected) {
|
||||
color: var(--grey-5);
|
||||
}
|
||||
|
||||
span:not(.selected):hover {
|
||||
color: var(--ink);
|
||||
}
|
||||
</style>
|
|
@ -1,14 +1,14 @@
|
|||
<script>
|
||||
import { workflowStore } from "builderStore"
|
||||
import { automationStore } from "builderStore"
|
||||
|
||||
export let blockDefinition
|
||||
export let stepId
|
||||
export let blockType
|
||||
|
||||
function addBlockToWorkflow() {
|
||||
workflowStore.actions.addBlockToWorkflow({
|
||||
function addBlockToAutomation() {
|
||||
automationStore.actions.addBlockToAutomation({
|
||||
...blockDefinition,
|
||||
args: blockDefinition.args || {},
|
||||
inputs: blockDefinition.inputs || {},
|
||||
stepId,
|
||||
type: blockType,
|
||||
})
|
||||
|
@ -16,43 +16,32 @@
|
|||
</script>
|
||||
|
||||
<div
|
||||
class="workflow-block hoverable"
|
||||
on:click={addBlockToWorkflow}
|
||||
class="automation-block hoverable"
|
||||
on:click={addBlockToAutomation}
|
||||
data-cy={stepId}>
|
||||
<div>
|
||||
<i class={blockDefinition.icon} />
|
||||
</div>
|
||||
<div class="workflow-text">
|
||||
<div class="automation-text">
|
||||
<h4>{blockDefinition.name}</h4>
|
||||
<p>{blockDefinition.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.workflow-block {
|
||||
.automation-block {
|
||||
display: grid;
|
||||
grid-template-columns: 20px auto;
|
||||
align-items: center;
|
||||
margin-top: 16px;
|
||||
padding: 12px;
|
||||
margin-top: var(--spacing-s);
|
||||
padding: var(--spacing-m);
|
||||
border-radius: var(--border-radius-m);
|
||||
}
|
||||
|
||||
.workflow-block:hover {
|
||||
.automation-block:hover {
|
||||
background-color: var(--grey-1);
|
||||
}
|
||||
|
||||
.workflow-text {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
background: var(--blue-light);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.automation-block:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
i {
|
||||
|
@ -60,14 +49,16 @@
|
|||
font-size: 20px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
.automation-text {
|
||||
margin-left: 16px;
|
||||
}
|
||||
.automation-text h4 {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 5px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
p {
|
||||
.automation-text p {
|
||||
font-size: 12px;
|
||||
color: var(--grey-7);
|
||||
margin: 0;
|
|
@ -1,14 +1,17 @@
|
|||
<script>
|
||||
import { workflowStore } from "builderStore"
|
||||
import WorkflowBlock from "./WorkflowBlock.svelte"
|
||||
import { sortBy } from "lodash/fp"
|
||||
import { automationStore } from "builderStore"
|
||||
import AutomationBlock from "./AutomationBlock.svelte"
|
||||
import FlatButtonGroup from "components/userInterface/FlatButtonGroup.svelte"
|
||||
|
||||
let selectedTab = "TRIGGER"
|
||||
let buttonProps = []
|
||||
$: blocks = Object.entries($workflowStore.blockDefinitions[selectedTab])
|
||||
$: blocks = sortBy(entry => entry[1].name)(
|
||||
Object.entries($automationStore.blockDefinitions[selectedTab])
|
||||
)
|
||||
|
||||
$: {
|
||||
if ($workflowStore.selectedWorkflow.hasTrigger()) {
|
||||
if ($automationStore.selectedAutomation.hasTrigger()) {
|
||||
buttonProps = [
|
||||
{ value: "ACTION", text: "Action" },
|
||||
{ value: "LOGIC", text: "Logic" },
|
||||
|
@ -33,7 +36,13 @@
|
|||
<FlatButtonGroup value={selectedTab} {buttonProps} onChange={onChangeTab} />
|
||||
<div id="blocklist">
|
||||
{#each blocks as [stepId, blockDefinition]}
|
||||
<WorkflowBlock {blockDefinition} {stepId} blockType={selectedTab} />
|
||||
<AutomationBlock {blockDefinition} {stepId} blockType={selectedTab} />
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
#blocklist {
|
||||
margin-top: var(--spacing-xl);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,3 @@
|
|||
export { default as AutomationPanel } from "./AutomationPanel.svelte"
|
||||
export { default as BlockList } from "./BlockList/BlockList.svelte"
|
||||
export { default as AutomationList } from "./AutomationList/AutomationList.svelte"
|
|
@ -0,0 +1,104 @@
|
|||
<script>
|
||||
import ModelSelector from "./ParamInputs/ModelSelector.svelte"
|
||||
import RecordSelector from "./ParamInputs/RecordSelector.svelte"
|
||||
import { Input, TextArea, Select, Label } from "@budibase/bbui"
|
||||
import { automationStore } from "builderStore"
|
||||
import BindableInput from "../../userInterface/BindableInput.svelte"
|
||||
|
||||
export let block
|
||||
$: inputs = Object.entries(block.schema?.inputs?.properties || {})
|
||||
$: bindings = getAvailableBindings(
|
||||
block,
|
||||
$automationStore.selectedAutomation?.automation?.definition
|
||||
)
|
||||
|
||||
function getAvailableBindings(block, automation) {
|
||||
if (!block || !automation) {
|
||||
return []
|
||||
}
|
||||
|
||||
// Find previous steps to the selected one
|
||||
let allSteps = [...automation.steps]
|
||||
if (automation.trigger) {
|
||||
allSteps = [automation.trigger, ...allSteps]
|
||||
}
|
||||
const blockIdx = allSteps.findIndex(step => step.id === block.id)
|
||||
|
||||
// Extract all outputs from all previous steps as available bindings
|
||||
let bindings = []
|
||||
for (let idx = 0; idx < blockIdx; idx++) {
|
||||
const outputs = Object.entries(
|
||||
allSteps[idx].schema?.outputs?.properties ?? {}
|
||||
)
|
||||
bindings = bindings.concat(
|
||||
outputs.map(([name, value]) => ({
|
||||
label: name,
|
||||
type: value.type,
|
||||
description: value.description,
|
||||
category: idx === 0 ? "Trigger outputs" : `Step ${idx} outputs`,
|
||||
path: idx === 0 ? `trigger.${name}` : `steps.${idx}.${name}`,
|
||||
}))
|
||||
)
|
||||
}
|
||||
return bindings
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container" data-cy="automation-block-setup">
|
||||
<div class="block-label">{block.name}</div>
|
||||
{#each inputs as [key, value]}
|
||||
<div class="bb-margin-xl block-field">
|
||||
<div class="field-label">{value.title}</div>
|
||||
{#if value.type === 'string' && value.enum}
|
||||
<Select bind:value={block.inputs[key]} thin secondary>
|
||||
<option value="">Choose an option</option>
|
||||
{#each value.enum as option, idx}
|
||||
<option value={option}>
|
||||
{value.pretty ? value.pretty[idx] : option}
|
||||
</option>
|
||||
{/each}
|
||||
</Select>
|
||||
{:else if value.customType === 'password'}
|
||||
<Input type="password" thin bind:value={block.inputs[key]} />
|
||||
{:else if value.customType === 'model'}
|
||||
<ModelSelector bind:value={block.inputs[key]} />
|
||||
{:else if value.customType === 'record'}
|
||||
<RecordSelector bind:value={block.inputs[key]} {bindings} />
|
||||
{:else if value.type === 'string' || value.type === 'number'}
|
||||
<BindableInput
|
||||
type="string"
|
||||
thin
|
||||
bind:value={block.inputs[key]}
|
||||
{bindings} />
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.block-field {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
color: var(--ink);
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
.block-label {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
color: var(--grey-7);
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 150px;
|
||||
font-family: inherit;
|
||||
padding: 12px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { store, backendUiStore, workflowStore } from "builderStore"
|
||||
import { store, backendUiStore, automationStore } from "builderStore"
|
||||
import { notifier } from "builderStore/store/notifications"
|
||||
import ActionButton from "components/common/ActionButton.svelte"
|
||||
|
||||
|
@ -10,32 +10,33 @@
|
|||
$: valid = !!name
|
||||
$: instanceId = $backendUiStore.selectedDatabase._id
|
||||
|
||||
async function deleteWorkflow() {
|
||||
await workflowStore.actions.delete({
|
||||
async function deleteAutomation() {
|
||||
await automationStore.actions.delete({
|
||||
instanceId,
|
||||
workflow: $workflowStore.selectedWorkflow.workflow,
|
||||
automation: $automationStore.selectedAutomation.automation,
|
||||
})
|
||||
onClosed()
|
||||
notifier.danger("Workflow deleted.")
|
||||
notifier.danger("Automation deleted.")
|
||||
}
|
||||
</script>
|
||||
|
||||
<header>
|
||||
<i class="ri-stackshare-line" />
|
||||
Delete Workflow
|
||||
Delete Automation
|
||||
</header>
|
||||
<div>
|
||||
<p>
|
||||
Are you sure you want to delete this workflow? This action can't be undone.
|
||||
Are you sure you want to delete this automation? This action can't be
|
||||
undone.
|
||||
</p>
|
||||
</div>
|
||||
<footer>
|
||||
<a href="https://docs.budibase.com">
|
||||
<i class="ri-information-line" />
|
||||
Learn about workflows
|
||||
Learn about automations
|
||||
</a>
|
||||
<ActionButton on:click={onClosed}>Cancel</ActionButton>
|
||||
<ActionButton alert on:click={deleteWorkflow}>Delete</ActionButton>
|
||||
<ActionButton alert on:click={deleteAutomation}>Delete</ActionButton>
|
||||
</footer>
|
||||
|
||||
<style>
|
|
@ -1,23 +1,15 @@
|
|||
<script>
|
||||
import { backendUiStore } from "builderStore"
|
||||
import { Select } from "@budibase/bbui"
|
||||
|
||||
export let value
|
||||
$: modelId = value ? value._id : ""
|
||||
|
||||
function onChange(e) {
|
||||
value = $backendUiStore.models.find(model => model._id === e.target.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="block-field">
|
||||
<select
|
||||
class="budibase__input"
|
||||
value={modelId}
|
||||
on:blur={onChange}
|
||||
on:change={onChange}>
|
||||
<Select bind:value secondary thin>
|
||||
<option value="">Choose an option</option>
|
||||
{#each $backendUiStore.models as model}
|
||||
<option value={model._id}>{model.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</Select>
|
||||
</div>
|
|
@ -0,0 +1,69 @@
|
|||
<script>
|
||||
import { backendUiStore } from "builderStore"
|
||||
import { Input, Select, Label } from "@budibase/bbui"
|
||||
import BindableInput from "../../../userInterface/BindableInput.svelte"
|
||||
|
||||
export let value
|
||||
export let bindings
|
||||
|
||||
$: model = $backendUiStore.models.find(model => model._id === value?.modelId)
|
||||
$: schemaFields = Object.entries(model?.schema ?? {})
|
||||
|
||||
// Ensure any nullish modelId values get set to empty string so
|
||||
// that the select works
|
||||
$: if (value?.modelId == null) value = { modelId: "" }
|
||||
|
||||
function schemaHasOptions(schema) {
|
||||
return !!schema.constraints?.inclusion?.length
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="block-field">
|
||||
<Select bind:value={value.modelId} thin secondary>
|
||||
<option value="">Choose an option</option>
|
||||
{#each $backendUiStore.models as model}
|
||||
<option value={model._id}>{model.name}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{#if schemaFields.length}
|
||||
<div class="bb-margin-xl block-field">
|
||||
{#each schemaFields as [field, schema]}
|
||||
<div class="bb-margin-xl capitalise">
|
||||
{#if schemaHasOptions(schema)}
|
||||
<div class="field-label">{field}</div>
|
||||
<Select thin secondary bind:value={value[field]}>
|
||||
<option value="">Choose an option</option>
|
||||
{#each schema.constraints.inclusion as option}
|
||||
<option value={option}>{option}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
{:else if schema.type === 'string' || schema.type === 'number'}
|
||||
<BindableInput
|
||||
thin
|
||||
bind:value={value[field]}
|
||||
label={field}
|
||||
type="string"
|
||||
{bindings} />
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.field-label {
|
||||
color: var(--ink);
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
.capitalise :global(label),
|
||||
.field-label {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,144 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
import { backendUiStore, automationStore } from "builderStore"
|
||||
import { notifier } from "builderStore/store/notifications"
|
||||
import AutomationBlockSetup from "./AutomationBlockSetup.svelte"
|
||||
import DeleteAutomationModal from "./DeleteAutomationModal.svelte"
|
||||
import { Button, Input, Label } from "@budibase/bbui"
|
||||
|
||||
const { open, close } = getContext("simple-modal")
|
||||
|
||||
let selectedTab = "SETUP"
|
||||
|
||||
$: automation = $automationStore.selectedAutomation?.automation
|
||||
$: allowDeleteBlock =
|
||||
$automationStore.selectedBlock?.type !== "TRIGGER" ||
|
||||
!automation?.definition?.steps?.length
|
||||
|
||||
function deleteAutomation() {
|
||||
open(
|
||||
DeleteAutomationModal,
|
||||
{ onClosed: close },
|
||||
{ styleContent: { padding: "0" } }
|
||||
)
|
||||
}
|
||||
|
||||
function deleteAutomationBlock() {
|
||||
automationStore.actions.deleteAutomationBlock(
|
||||
$automationStore.selectedBlock
|
||||
)
|
||||
}
|
||||
|
||||
async function testAutomation() {
|
||||
const result = await automationStore.actions.trigger({
|
||||
automation: $automationStore.selectedAutomation.automation,
|
||||
})
|
||||
if (result.status === 200) {
|
||||
notifier.success(`Automation ${automation.name} triggered successfully.`)
|
||||
} else {
|
||||
notifier.danger(`Failed to trigger automation ${automation.name}.`)
|
||||
}
|
||||
}
|
||||
|
||||
async function saveAutomation() {
|
||||
await automationStore.actions.save({
|
||||
instanceId: $backendUiStore.selectedDatabase._id,
|
||||
automation,
|
||||
})
|
||||
notifier.success(`Automation ${automation.name} saved.`)
|
||||
}
|
||||
</script>
|
||||
|
||||
<section>
|
||||
<header>
|
||||
<span
|
||||
class="hoverable"
|
||||
class:selected={selectedTab === 'SETUP'}
|
||||
on:click={() => (selectedTab = 'SETUP')}>
|
||||
Setup
|
||||
</span>
|
||||
</header>
|
||||
<div class="content">
|
||||
{#if $automationStore.selectedBlock}
|
||||
<AutomationBlockSetup bind:block={$automationStore.selectedBlock} />
|
||||
{:else if $automationStore.selectedAutomation}
|
||||
<div class="block-label">
|
||||
Automation
|
||||
<b>{automation.name}</b>
|
||||
</div>
|
||||
<Button secondary wide on:click={testAutomation}>Test Automation</Button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="buttons">
|
||||
{#if $automationStore.selectedBlock}
|
||||
<Button
|
||||
green
|
||||
wide
|
||||
data-cy="save-automation-setup"
|
||||
on:click={saveAutomation}>
|
||||
Save Automation
|
||||
</Button>
|
||||
<Button
|
||||
disabled={!allowDeleteBlock}
|
||||
red
|
||||
wide
|
||||
on:click={deleteAutomationBlock}>
|
||||
Delete Step
|
||||
</Button>
|
||||
{:else if $automationStore.selectedAutomation}
|
||||
<Button
|
||||
green
|
||||
wide
|
||||
data-cy="save-automation-setup"
|
||||
on:click={saveAutomation}>
|
||||
Save Automation
|
||||
</Button>
|
||||
<Button red wide on:click={deleteAutomation}>Delete Automation</Button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<style>
|
||||
section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
header {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
font-family: inter, sans-serif;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-xl);
|
||||
color: var(--ink);
|
||||
}
|
||||
header > span {
|
||||
color: var(--grey-5);
|
||||
margin-right: var(--spacing-xl);
|
||||
cursor: pointer;
|
||||
}
|
||||
.selected {
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.block-label {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
color: var(--grey-7);
|
||||
margin-bottom: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: grid;
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,3 @@
|
|||
export { default as AutomationBuilder } from "./AutomationBuilder/AutomationBuilder.svelte"
|
||||
export { default as SetupPanel } from "./SetupPanel/SetupPanel.svelte"
|
||||
export { default as AutomationPanel } from "./AutomationPanel/AutomationPanel.svelte"
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import { writable } from "svelte/store"
|
||||
|
||||
import { store, workflowStore, backendUiStore } from "builderStore"
|
||||
import { store, automationStore, backendUiStore } from "builderStore"
|
||||
import { string, object } from "yup"
|
||||
import api, { get } from "builderStore/api"
|
||||
import Form from "@svelteschool/svelte-forms"
|
||||
|
@ -133,7 +133,7 @@
|
|||
if (applicationPkg.ok) {
|
||||
backendUiStore.actions.reset()
|
||||
await store.setPackage(pkg)
|
||||
workflowStore.actions.fetch()
|
||||
automationStore.actions.fetch()
|
||||
} else {
|
||||
throw new Error(pkg)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
<script>
|
||||
import GenericBindingPopover from "./GenericBindingPopover.svelte"
|
||||
import { Input, Icon } from "@budibase/bbui"
|
||||
|
||||
export let bindings = []
|
||||
export let value
|
||||
let anchor
|
||||
let popover = undefined
|
||||
let enrichedValue
|
||||
let inputProps
|
||||
|
||||
// Extract all other props to pass to input component
|
||||
$: {
|
||||
let { bindings, ...otherProps } = $$props
|
||||
inputProps = otherProps
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container" bind:this={anchor}>
|
||||
<Input {...inputProps} bind:value />
|
||||
<button on:click={popover.show}>
|
||||
<Icon name="edit" />
|
||||
</button>
|
||||
</div>
|
||||
<GenericBindingPopover
|
||||
{anchor}
|
||||
{bindings}
|
||||
bind:value
|
||||
bind:popover
|
||||
align="right" />
|
||||
|
||||
<style>
|
||||
.container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
button {
|
||||
position: absolute;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
background: var(--grey-4);
|
||||
right: 7px;
|
||||
bottom: 7px;
|
||||
}
|
||||
button:hover {
|
||||
background: var(--grey-5);
|
||||
cursor: pointer;
|
||||
}
|
||||
button :global(svg) {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(-50%) !important;
|
||||
}
|
||||
</style>
|
|
@ -3,7 +3,7 @@
|
|||
import { find, map, keys, reduce, keyBy } from "lodash/fp"
|
||||
import { pipe } from "components/common/core"
|
||||
import { EVENT_TYPE_MEMBER_NAME } from "components/common/eventHandlers"
|
||||
import { store, workflowStore } from "builderStore"
|
||||
import { store, automationStore } from "builderStore"
|
||||
import { ArrowDownIcon } from "components/common/Icons/"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
|
||||
|
@ -18,14 +18,14 @@
|
|||
</script>
|
||||
|
||||
<div class="handler-option">
|
||||
{#if parameter.name === 'workflow'}
|
||||
{#if parameter.name === 'automation'}
|
||||
<span>{parameter.name}</span>
|
||||
{/if}
|
||||
{#if parameter.name === 'workflow'}
|
||||
{#if parameter.name === 'automation'}
|
||||
<Select on:change bind:value={parameter.value}>
|
||||
<option value="" />
|
||||
{#each $workflowStore.workflows.filter(wf => wf.live) as workflow}
|
||||
<option value={workflow._id}>{workflow.name}</option>
|
||||
{#each $automationStore.automations.filter(wf => wf.live) as automation}
|
||||
<option value={automation._id}>{automation.name}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
{:else if parameter.name === 'url'}
|
||||
|
|
|
@ -0,0 +1,129 @@
|
|||
<script>
|
||||
import groupBy from "lodash/fp/groupBy"
|
||||
import { TextArea, Label, Body, Button, Popover } from "@budibase/bbui"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let value = ""
|
||||
export let bindings = []
|
||||
export let anchor
|
||||
export let align
|
||||
export let popover = null
|
||||
|
||||
$: categories = Object.entries(groupBy("category", bindings))
|
||||
|
||||
function onClickBinding(binding) {
|
||||
value += `{{ ${binding.path} }}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<Popover {anchor} {align} bind:this={popover}>
|
||||
<div class="container">
|
||||
<div class="bindings">
|
||||
<Label large>Available bindings</Label>
|
||||
<div class="bindings__wrapper">
|
||||
<div class="bindings__list">
|
||||
{#each categories as [categoryName, bindings]}
|
||||
<Label small>{categoryName}</Label>
|
||||
{#each bindings as binding}
|
||||
<div class="binding" on:click={() => onClickBinding(binding)}>
|
||||
<span class="binding__label">{binding.label}</span>
|
||||
<span class="binding__type">{binding.type}</span>
|
||||
<br />
|
||||
<div class="binding__description">{binding.description}</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="editor">
|
||||
<Label large>Data binding</Label>
|
||||
<Body small>
|
||||
Binding connects one piece of data to another and makes it dynamic.
|
||||
Click the objects on the left to add them to the textbox.
|
||||
</Body>
|
||||
<TextArea thin bind:value placeholder="..." />
|
||||
<div class="controls">
|
||||
<a href="#">
|
||||
<Body small>Learn more about binding</Body>
|
||||
</a>
|
||||
<Button on:click={popover.hide} primary>Done</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Popover>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
display: grid;
|
||||
grid-template-columns: 280px 1fr;
|
||||
width: 800px;
|
||||
}
|
||||
|
||||
.bindings {
|
||||
border-right: 1px solid var(--grey-4);
|
||||
flex: 0 0 240px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
padding-right: var(--spacing-l);
|
||||
}
|
||||
.bindings :global(label) {
|
||||
margin: var(--spacing-m) 0;
|
||||
}
|
||||
.bindings :global(label:first-child) {
|
||||
margin-top: 0;
|
||||
}
|
||||
.bindings__wrapper {
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.bindings__list {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.binding {
|
||||
font-size: 12px;
|
||||
padding: var(--spacing-s);
|
||||
border-radius: var(--border-radius-m);
|
||||
}
|
||||
.binding:hover {
|
||||
background-color: var(--grey-2);
|
||||
cursor: pointer;
|
||||
}
|
||||
.binding__label {
|
||||
font-weight: 500;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
.binding__description {
|
||||
color: var(--grey-8);
|
||||
margin-top: 2px;
|
||||
}
|
||||
.binding__type {
|
||||
font-family: monospace;
|
||||
background-color: var(--grey-2);
|
||||
border-radius: var(--border-radius-m);
|
||||
padding: 2px;
|
||||
margin-left: 2px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.editor {
|
||||
padding-left: var(--spacing-l);
|
||||
}
|
||||
.editor :global(textarea) {
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
grid-gap: var(--spacing-l);
|
||||
align-items: center;
|
||||
margin-top: var(--spacing-m);
|
||||
}
|
||||
</style>
|
|
@ -1,51 +0,0 @@
|
|||
<script>
|
||||
import { backendUiStore } from "builderStore"
|
||||
import { Input, Label } from "@budibase/bbui"
|
||||
|
||||
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) {
|
||||
const fieldSchema = value.model.schema[field]
|
||||
if (fieldSchema.type === "number") {
|
||||
value[field] = parseInt(evt.target.value)
|
||||
return
|
||||
}
|
||||
value[field] = evt.target.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="block-field">
|
||||
<select
|
||||
class="budibase__input"
|
||||
value={modelId}
|
||||
on:blur={onChangeModel}
|
||||
on:change={onChangeModel}>
|
||||
<option value="">Choose an option</option>
|
||||
{#each $backendUiStore.models as model}
|
||||
<option value={model._id}>{model.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{#if schemaFields.length}
|
||||
<div class="bb-margin-xl block-field">
|
||||
<Label small forAttr={'fields'}>Fields</Label>
|
||||
{#each schemaFields as field}
|
||||
<div class="bb-margin-xl">
|
||||
<Input
|
||||
thin
|
||||
value={value[field]}
|
||||
label={field}
|
||||
on:change={e => setParsedValue(e, field)} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
|
@ -1,151 +0,0 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
import { backendUiStore, workflowStore } from "builderStore"
|
||||
import { notifier } from "builderStore/store/notifications"
|
||||
import WorkflowBlockSetup from "./WorkflowBlockSetup.svelte"
|
||||
import DeleteWorkflowModal from "./DeleteWorkflowModal.svelte"
|
||||
import { Button, Input, Label } from "@budibase/bbui"
|
||||
|
||||
const { open, close } = getContext("simple-modal")
|
||||
|
||||
let selectedTab = "SETUP"
|
||||
|
||||
$: workflow =
|
||||
$workflowStore.selectedWorkflow && $workflowStore.selectedWorkflow.workflow
|
||||
|
||||
function deleteWorkflow() {
|
||||
open(
|
||||
DeleteWorkflowModal,
|
||||
{ onClosed: close },
|
||||
{ styleContent: { padding: "0" } }
|
||||
)
|
||||
}
|
||||
|
||||
function deleteWorkflowBlock() {
|
||||
workflowStore.actions.deleteWorkflowBlock($workflowStore.selectedBlock)
|
||||
}
|
||||
|
||||
async function testWorkflow() {
|
||||
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() {
|
||||
await workflowStore.actions.save({
|
||||
instanceId: $backendUiStore.selectedDatabase._id,
|
||||
workflow,
|
||||
})
|
||||
notifier.success(`Workflow ${workflow.name} saved.`)
|
||||
}
|
||||
</script>
|
||||
|
||||
<section>
|
||||
<header>
|
||||
<span
|
||||
class="hoverable"
|
||||
class:selected={selectedTab === 'SETUP'}
|
||||
on:click={() => (selectedTab = 'SETUP')}>
|
||||
Setup
|
||||
</span>
|
||||
</header>
|
||||
{#if $workflowStore.selectedBlock}
|
||||
<WorkflowBlockSetup bind:block={$workflowStore.selectedBlock} />
|
||||
<div class="buttons">
|
||||
<Button green wide data-cy="save-workflow-setup" on:click={saveWorkflow}>
|
||||
Save Workflow
|
||||
</Button>
|
||||
<Button red wide on:click={deleteWorkflowBlock}>Delete Block</Button>
|
||||
</div>
|
||||
{:else if $workflowStore.selectedWorkflow}
|
||||
<div class="panel">
|
||||
<div class="panel-body">
|
||||
<div class="block-label">
|
||||
Workflow
|
||||
<b>{workflow.name}</b>
|
||||
</div>
|
||||
</div>
|
||||
<Button secondary wide on:click={testWorkflow}>Test Workflow</Button>
|
||||
<div class="buttons">
|
||||
<Button
|
||||
green
|
||||
wide
|
||||
data-cy="save-workflow-setup"
|
||||
on:click={saveWorkflow}>
|
||||
Save Workflow
|
||||
</Button>
|
||||
<Button red wide on:click={deleteWorkflow}>Delete Workflow</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<style>
|
||||
section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
header {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
font-family: inter;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.selected {
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.block-label {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
color: var(--grey-7);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
header > span {
|
||||
color: var(--grey-5);
|
||||
margin-right: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.buttons {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
display: grid;
|
||||
width: 260px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.access-level label {
|
||||
font-weight: normal;
|
||||
color: var(--ink);
|
||||
}
|
||||
</style>
|
|
@ -1,71 +0,0 @@
|
|||
<script>
|
||||
import ModelSelector from "./ParamInputs/ModelSelector.svelte"
|
||||
import RecordSelector from "./ParamInputs/RecordSelector.svelte"
|
||||
import { Input, TextArea, Select } from "@budibase/bbui"
|
||||
|
||||
export let block
|
||||
$: params = block.params ? Object.entries(block.params) : []
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<div class="selected-label">{block.name}</div>
|
||||
{#each params as [parameter, type]}
|
||||
<div class="block-field">
|
||||
<label class="label">{parameter}</label>
|
||||
{#if Array.isArray(type)}
|
||||
<Select bind:value={block.args[parameter]} thin secondary>
|
||||
<option value="">Choose an option</option>
|
||||
{#each type as option}
|
||||
<option value={option}>{option}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
{:else if type === 'accessLevel'}
|
||||
<Select bind:value={block.args[parameter]} thin secondary>
|
||||
<option value="ADMIN">Admin</option>
|
||||
<option value="POWER_USER">Power User</option>
|
||||
</Select>
|
||||
{:else if type === 'password'}
|
||||
<Input type="password" thin bind:value={block.args[parameter]} />
|
||||
{:else if type === 'number'}
|
||||
<Input type="number" thin bind:value={block.args[parameter]} />
|
||||
{:else if type === 'longText'}
|
||||
<TextArea type="text" thin bind:value={block.args[parameter]} />
|
||||
{:else if type === 'model'}
|
||||
<ModelSelector bind:value={block.args[parameter]} />
|
||||
{:else if type === 'record'}
|
||||
<RecordSelector bind:value={block.args[parameter]} />
|
||||
{:else if type === 'string'}
|
||||
<Input type="text" thin bind:value={block.args[parameter]} />
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.block-field {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 14px;
|
||||
font-family: sans-serif;
|
||||
font-weight: 500;
|
||||
color: var(--ink);
|
||||
margin-bottom: 12px;
|
||||
text-transform: capitalize;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.selected-label {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
color: var(--grey-7);
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 150px;
|
||||
font-family: inherit;
|
||||
padding: 12px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
|
@ -1,86 +0,0 @@
|
|||
<script>
|
||||
import mustache from "mustache"
|
||||
import { workflowStore } from "builderStore"
|
||||
|
||||
export let onSelect
|
||||
export let block
|
||||
let selected
|
||||
|
||||
$: selected =
|
||||
$workflowStore.selectedBlock != null &&
|
||||
$workflowStore.selectedBlock.id === block.id
|
||||
|
||||
function selectBlock() {
|
||||
onSelect(block)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class={`${block.type} hoverable`} class:selected on:click={selectBlock}>
|
||||
<header>
|
||||
{#if block.type === 'TRIGGER'}
|
||||
<i class="ri-lightbulb-fill" />
|
||||
When this happens...
|
||||
{:else if block.type === 'ACTION'}
|
||||
<i class="ri-flashlight-fill" />
|
||||
Do this...
|
||||
{:else if block.type === 'LOGIC'}
|
||||
<i class="ri-pause-fill" />
|
||||
Only continue if...
|
||||
{/if}
|
||||
</header>
|
||||
<hr />
|
||||
<p>
|
||||
{@html mustache.render(block.tagline, block.args)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div {
|
||||
width: 320px;
|
||||
padding: 20px;
|
||||
border-radius: var(--border-radius-m);
|
||||
transition: 0.3s all ease;
|
||||
box-shadow: 0 4px 30px 0 rgba(57, 60, 68, 0.08);
|
||||
background-color: var(--ink);
|
||||
font-size: 16px;
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
header {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
header i {
|
||||
font-size: 20px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.ACTION {
|
||||
background-color: var(--white);
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.TRIGGER {
|
||||
background-color: var(--ink);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.LOGIC {
|
||||
background-color: var(--blue-light);
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
p {
|
||||
color: inherit;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
div.selected,
|
||||
div:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 4px 30px 0 rgba(57, 60, 68, 0.15);
|
||||
}
|
||||
</style>
|
|
@ -1,88 +0,0 @@
|
|||
<script>
|
||||
import { store, backendUiStore, workflowStore } from "builderStore"
|
||||
import { notifier } from "builderStore/store/notifications"
|
||||
import ActionButton from "components/common/ActionButton.svelte"
|
||||
import { Input } from "@budibase/bbui"
|
||||
|
||||
export let onClosed
|
||||
|
||||
let name
|
||||
|
||||
$: valid = !!name
|
||||
$: instanceId = $backendUiStore.selectedDatabase._id
|
||||
$: appId = $store.appId
|
||||
|
||||
async function createWorkflow() {
|
||||
await workflowStore.actions.create({
|
||||
name,
|
||||
instanceId,
|
||||
})
|
||||
onClosed()
|
||||
notifier.success(`Workflow ${name} created.`)
|
||||
}
|
||||
</script>
|
||||
|
||||
<header>
|
||||
<i class="ri-stackshare-line" />
|
||||
Create Workflow
|
||||
</header>
|
||||
<div>
|
||||
<Input bind:value={name} label="Name" />
|
||||
</div>
|
||||
<footer>
|
||||
<a href="https://docs.budibase.com">
|
||||
<i class="ri-information-line" />
|
||||
Learn about workflows
|
||||
</a>
|
||||
<ActionButton secondary on:click={onClosed}>Cancel</ActionButton>
|
||||
<ActionButton disabled={!valid} on:click={createWorkflow}>Save</ActionButton>
|
||||
</footer>
|
||||
|
||||
<style>
|
||||
header {
|
||||
font-size: 24px;
|
||||
color: var(--ink);
|
||||
font-weight: bold;
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
header i {
|
||||
margin-right: 10px;
|
||||
font-size: 20px;
|
||||
background: var(--blue-light);
|
||||
color: var(--grey-4);
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
div {
|
||||
padding: 0 30px 30px 30px;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
footer {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-gap: 5px;
|
||||
grid-auto-columns: 3fr 1fr 1fr;
|
||||
padding: 20px;
|
||||
background: var(--grey-1);
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
footer a {
|
||||
color: var(--primary);
|
||||
font-size: 14px;
|
||||
vertical-align: middle;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
footer i {
|
||||
font-size: 20px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
</style>
|
|
@ -1,122 +0,0 @@
|
|||
<script>
|
||||
import Modal from "svelte-simple-modal"
|
||||
import { notifier } from "builderStore/store/notifications"
|
||||
import { onMount, getContext } from "svelte"
|
||||
import { backendUiStore, workflowStore } from "builderStore"
|
||||
import CreateWorkflowModal from "./CreateWorkflowModal.svelte"
|
||||
import { Button } from "@budibase/bbui"
|
||||
|
||||
const { open, close } = getContext("simple-modal")
|
||||
|
||||
$: selectedWorkflowId =
|
||||
$workflowStore.selectedWorkflow &&
|
||||
$workflowStore.selectedWorkflow.workflow._id
|
||||
|
||||
function newWorkflow() {
|
||||
open(
|
||||
CreateWorkflowModal,
|
||||
{
|
||||
onClosed: close,
|
||||
},
|
||||
{ styleContent: { padding: "0" } }
|
||||
)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
workflowStore.actions.fetch()
|
||||
})
|
||||
</script>
|
||||
|
||||
<section>
|
||||
<Button purple wide on:click={newWorkflow}>Create New Workflow</Button>
|
||||
<ul>
|
||||
{#each $workflowStore.workflows as workflow}
|
||||
<li
|
||||
class="workflow-item"
|
||||
class:selected={workflow._id === selectedWorkflowId}
|
||||
on:click={() => workflowStore.actions.select(workflow)}>
|
||||
<i class="ri-stackshare-line" class:live={workflow.live} />
|
||||
{workflow.name}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
i {
|
||||
color: #adaec4;
|
||||
}
|
||||
|
||||
i:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.live {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
li {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.workflow-item {
|
||||
display: flex;
|
||||
border-radius: 5px;
|
||||
padding-left: 12px;
|
||||
align-items: center;
|
||||
height: 36px;
|
||||
margin-bottom: 4px;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.workflow-item i {
|
||||
font-size: 24px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.workflow-item:hover {
|
||||
cursor: pointer;
|
||||
background: var(--grey-1);
|
||||
}
|
||||
|
||||
.workflow-item.selected {
|
||||
background: var(--grey-2);
|
||||
}
|
||||
|
||||
.new-workflow-button {
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--grey-4);
|
||||
border-radius: 3px;
|
||||
width: 100%;
|
||||
padding: 8px 16px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: white;
|
||||
color: var(--ink);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all 2ms;
|
||||
}
|
||||
|
||||
.new-workflow-button:hover {
|
||||
background: var(--grey-1);
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: var(--ink);
|
||||
font-size: 16px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
</style>
|
|
@ -1,54 +0,0 @@
|
|||
<script>
|
||||
import { workflowStore } from "builderStore"
|
||||
import WorkflowList from "./WorkflowList/WorkflowList.svelte"
|
||||
import BlockList from "./BlockList/BlockList.svelte"
|
||||
|
||||
let selectedTab = "WORKFLOWS"
|
||||
</script>
|
||||
|
||||
<header>
|
||||
<span
|
||||
data-cy="workflow-list"
|
||||
class="hoverable workflow-header"
|
||||
class:selected={selectedTab === 'WORKFLOWS'}
|
||||
on:click={() => (selectedTab = 'WORKFLOWS')}>
|
||||
Workflows
|
||||
</span>
|
||||
{#if $workflowStore.selectedWorkflow}
|
||||
<span
|
||||
data-cy="add-workflow-component"
|
||||
class="hoverable"
|
||||
class:selected={selectedTab === 'ADD'}
|
||||
on:click={() => (selectedTab = 'ADD')}>
|
||||
Add
|
||||
</span>
|
||||
{/if}
|
||||
</header>
|
||||
{#if selectedTab === 'WORKFLOWS'}
|
||||
<WorkflowList />
|
||||
{:else if selectedTab === 'ADD'}
|
||||
<BlockList />
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
header {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
background: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.workflow-header {
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
span:not(.selected) {
|
||||
color: var(--grey-5);
|
||||
}
|
||||
|
||||
span:not(.selected):hover {
|
||||
color: var(--ink);
|
||||
}
|
||||
</style>
|
|
@ -1,3 +0,0 @@
|
|||
export { default as WorkflowPanel } from "./WorkflowPanel.svelte"
|
||||
export { default as BlockList } from "./BlockList/BlockList.svelte"
|
||||
export { default as WorkflowList } from "./WorkflowList/WorkflowList.svelte"
|
|
@ -1,3 +0,0 @@
|
|||
export { default as WorkflowBuilder } from "./WorkflowBuilder/WorkflowBuilder.svelte"
|
||||
export { default as SetupPanel } from "./SetupPanel/SetupPanel.svelte"
|
||||
export { default as WorkflowPanel } from "./WorkflowPanel/WorkflowPanel.svelte"
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import Modal from "svelte-simple-modal"
|
||||
import { store, workflowStore, backendUiStore } from "builderStore"
|
||||
import { store, automationStore, backendUiStore } from "builderStore"
|
||||
import SettingsLink from "components/settings/Link.svelte"
|
||||
import { get } from "builderStore/api"
|
||||
|
||||
|
@ -21,17 +21,17 @@
|
|||
if (res.ok) {
|
||||
backendUiStore.actions.reset()
|
||||
await store.setPackage(pkg)
|
||||
workflowStore.actions.fetch()
|
||||
await automationStore.actions.fetch()
|
||||
return pkg
|
||||
} else {
|
||||
throw new Error(pkg)
|
||||
}
|
||||
}
|
||||
|
||||
// handles navigation between frontend, backend, workflow.
|
||||
// handles navigation between frontend, backend, automation.
|
||||
// this remembers your last place on each of the sections
|
||||
// e.g. if one of your screens is selected on front end, then
|
||||
// you browse to backend, when you click fronend, you will be
|
||||
// you browse to backend, when you click frontend, you will be
|
||||
// brought back to the same screen
|
||||
const topItemNavigate = path => () => {
|
||||
const activeTopNav = $layout.children.find(c => $isActive(c.path))
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
<script>
|
||||
import { automationStore } from "builderStore"
|
||||
import { AutomationPanel, SetupPanel } from "components/automation"
|
||||
</script>
|
||||
|
||||
<!-- routify:options index=3 -->
|
||||
<div class="root">
|
||||
<div class="nav">
|
||||
<AutomationPanel />
|
||||
</div>
|
||||
<div class="content">
|
||||
<slot />
|
||||
</div>
|
||||
{#if $automationStore.selectedAutomation}
|
||||
<div class="nav">
|
||||
<SetupPanel />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.content {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.root {
|
||||
height: calc(100% - 60px);
|
||||
display: grid;
|
||||
grid-template-columns: 300px minmax(510px, 1fr) 300px;
|
||||
background: var(--grey-1);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.nav {
|
||||
overflow-y: auto;
|
||||
background: var(--white);
|
||||
padding: var(--spacing-xl);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,5 @@
|
|||
<script>
|
||||
import { AutomationBuilder } from "components/automation"
|
||||
</script>
|
||||
|
||||
<AutomationBuilder />
|
|
@ -5,6 +5,7 @@
|
|||
import ModelNavigator from "components/nav/ModelNavigator/ModelNavigator.svelte"
|
||||
</script>
|
||||
|
||||
<!-- routify:options index=1 -->
|
||||
<div class="root">
|
||||
<div class="nav">
|
||||
<ModelNavigator />
|
||||
|
|
|
@ -37,6 +37,7 @@
|
|||
const lastPartOfName = c => (c ? last(c.split("/")) : "")
|
||||
</script>
|
||||
|
||||
<!-- routify:options index=1 -->
|
||||
<div class="root">
|
||||
|
||||
<div class="ui-nav">
|
||||
|
|
|
@ -1,51 +0,0 @@
|
|||
<script>
|
||||
import { workflowStore } from "builderStore"
|
||||
import { WorkflowPanel, SetupPanel } from "components/workflow"
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
<div class="nav">
|
||||
<div class="inner">
|
||||
<WorkflowPanel />
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<slot />
|
||||
</div>
|
||||
{#if $workflowStore.selectedWorkflow}
|
||||
<div class="nav">
|
||||
<div class="inner">
|
||||
<SetupPanel />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.content {
|
||||
position: relative;
|
||||
background: var(--grey-1);
|
||||
}
|
||||
|
||||
.root {
|
||||
height: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 300px minmax(0, 1fr) 300px;
|
||||
background: var(--grey-1);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.nav {
|
||||
overflow: auto;
|
||||
width: 300px;
|
||||
background: var(--white);
|
||||
}
|
||||
|
||||
.inner {
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
|
@ -1,5 +0,0 @@
|
|||
<script>
|
||||
import { WorkflowBuilder } from "components/workflow"
|
||||
</script>
|
||||
|
||||
<WorkflowBuilder />
|
|
@ -8,6 +8,13 @@
|
|||
dependencies:
|
||||
"@babel/highlight" "^7.8.3"
|
||||
|
||||
"@babel/code-frame@^7.10.4":
|
||||
version "7.10.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a"
|
||||
integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==
|
||||
dependencies:
|
||||
"@babel/highlight" "^7.10.4"
|
||||
|
||||
"@babel/compat-data@^7.9.6":
|
||||
version "7.9.6"
|
||||
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.9.6.tgz#3f604c40e420131affe6f2c8052e9a275ae2049b"
|
||||
|
@ -184,6 +191,11 @@
|
|||
dependencies:
|
||||
"@babel/types" "^7.8.3"
|
||||
|
||||
"@babel/helper-validator-identifier@^7.10.4":
|
||||
version "7.10.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2"
|
||||
integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==
|
||||
|
||||
"@babel/helper-validator-identifier@^7.9.0", "@babel/helper-validator-identifier@^7.9.5":
|
||||
version "7.9.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz#90977a8e6fbf6b431a7dc31752eee233bf052d80"
|
||||
|
@ -205,6 +217,15 @@
|
|||
"@babel/traverse" "^7.9.6"
|
||||
"@babel/types" "^7.9.6"
|
||||
|
||||
"@babel/highlight@^7.10.4":
|
||||
version "7.10.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143"
|
||||
integrity sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==
|
||||
dependencies:
|
||||
"@babel/helper-validator-identifier" "^7.10.4"
|
||||
chalk "^2.0.0"
|
||||
js-tokens "^4.0.0"
|
||||
|
||||
"@babel/highlight@^7.8.3":
|
||||
version "7.9.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.9.0.tgz#4e9b45ccb82b79607271b2979ad82c7b68163079"
|
||||
|
@ -760,6 +781,7 @@
|
|||
"@fortawesome/fontawesome-free@^5.14.0":
|
||||
version "5.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.14.0.tgz#a371e91029ebf265015e64f81bfbf7d228c9681f"
|
||||
integrity sha512-OfdMsF+ZQgdKHP9jUbmDcRrP0eX90XXrsXIdyjLbkmSBzmMXPABB8eobUJtivaupucYaByz6WNe1PI1JuYm3qA==
|
||||
|
||||
"@hapi/address@^2.1.2":
|
||||
version "2.1.4"
|
||||
|
@ -1137,10 +1159,6 @@
|
|||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
|
||||
|
||||
"@types/estree@*":
|
||||
version "0.0.44"
|
||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.44.tgz#980cc5a29a3ef3bea6ff1f7d021047d7ea575e21"
|
||||
|
||||
"@types/estree@0.0.39":
|
||||
version "0.0.39"
|
||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f"
|
||||
|
@ -1283,7 +1301,7 @@ acorn@^6.0.1:
|
|||
version "6.4.1"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.1.tgz#531e58ba3f51b9dacb9a6646ca4debf5b14ca474"
|
||||
|
||||
acorn@^7.1.0, acorn@^7.1.1:
|
||||
acorn@^7.1.1:
|
||||
version "7.2.0"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.2.0.tgz#17ea7e40d7c8640ff54a694c889c26f31704effe"
|
||||
|
||||
|
@ -1387,6 +1405,7 @@ array-equal@^1.0.0:
|
|||
array-filter@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-1.0.0.tgz#baf79e62e6ef4c2a4c0b831232daffec251f9d83"
|
||||
integrity sha1-uveeYubvTCpMC4MSMtr/7CUfnYM=
|
||||
|
||||
array-union@^2.1.0:
|
||||
version "2.1.0"
|
||||
|
@ -1445,6 +1464,7 @@ atob@^2.1.2:
|
|||
available-typed-arrays@^1.0.0, available-typed-arrays@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz#6b098ca9d8039079ee3f77f7b783c4480ba513f5"
|
||||
integrity sha512-XWX3OX8Onv97LMk/ftVyBibpGwY5a8SmuxZPzeOxqmuEqUCOM9ZE+uIaD1VNJ5QnvU2UQusvmKbuM1FR8QWGfQ==
|
||||
dependencies:
|
||||
array-filter "^1.0.0"
|
||||
|
||||
|
@ -1908,7 +1928,7 @@ combined-stream@^1.0.6, combined-stream@~1.0.6:
|
|||
dependencies:
|
||||
delayed-stream "~1.0.0"
|
||||
|
||||
commander@2, commander@^2.19.0:
|
||||
commander@2, commander@^2.20.0:
|
||||
version "2.20.3"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
|
||||
|
||||
|
@ -2400,6 +2420,7 @@ decode-uri-component@^0.2.0:
|
|||
deep-equal@^2.0.1:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.0.3.tgz#cad1c15277ad78a5c01c49c2dee0f54de8a6a7b0"
|
||||
integrity sha512-Spqdl4H+ky45I9ByyJtXteOm9CaIrPmnIPmOhrkKGNYWeDgCvJ8jNYVCTjChxW4FqGuZnLHADc8EKRMX6+CgvA==
|
||||
dependencies:
|
||||
es-abstract "^1.17.5"
|
||||
es-get-iterator "^1.1.0"
|
||||
|
@ -2588,6 +2609,7 @@ es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.5:
|
|||
es-abstract@^1.17.4:
|
||||
version "1.17.6"
|
||||
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.6.tgz#9142071707857b2cacc7b89ecb670316c3e2d52a"
|
||||
integrity sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==
|
||||
dependencies:
|
||||
es-to-primitive "^1.2.1"
|
||||
function-bind "^1.1.1"
|
||||
|
@ -2604,6 +2626,7 @@ es-abstract@^1.17.4:
|
|||
es-abstract@^1.18.0-next.0:
|
||||
version "1.18.0-next.0"
|
||||
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.0.tgz#b302834927e624d8e5837ed48224291f2c66e6fc"
|
||||
integrity sha512-elZXTZXKn51hUBdJjSZGYRujuzilgXo8vSPQzjGYXLvSlGiCo8VO8ZGV3kjo9a0WNJJ57hENagwbtlRuHuzkcQ==
|
||||
dependencies:
|
||||
es-to-primitive "^1.2.1"
|
||||
function-bind "^1.1.1"
|
||||
|
@ -2621,6 +2644,7 @@ es-abstract@^1.18.0-next.0:
|
|||
es-get-iterator@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.0.tgz#bb98ad9d6d63b31aacdc8f89d5d0ee57bcb5b4c8"
|
||||
integrity sha512-UfrmHuWQlNMTs35e1ypnvikg6jCz3SK8v8ImvmDsh36fCVUR1MqoFDiyn0/k52C8NqO3YsO8Oe0azeesNuqSsQ==
|
||||
dependencies:
|
||||
es-abstract "^1.17.4"
|
||||
has-symbols "^1.0.1"
|
||||
|
@ -3313,6 +3337,7 @@ is-accessor-descriptor@^1.0.0:
|
|||
is-arguments@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.0.4.tgz#3faf966c7cba0ff437fb31f6250082fcf0448cf3"
|
||||
integrity sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==
|
||||
|
||||
is-arrayish@^0.2.1:
|
||||
version "0.2.1"
|
||||
|
@ -3321,6 +3346,7 @@ is-arrayish@^0.2.1:
|
|||
is-bigint@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.0.tgz#73da8c33208d00f130e9b5e15d23eac9215601c4"
|
||||
integrity sha512-t5mGUXC/xRheCK431ylNiSkGGpBp8bHENBcENTkDT6ppwPzEVxNGZRvgvmOEfbWkFhA7D2GEuE2mmQTr78sl2g==
|
||||
|
||||
is-binary-path@~2.1.0:
|
||||
version "2.1.0"
|
||||
|
@ -3331,6 +3357,7 @@ is-binary-path@~2.1.0:
|
|||
is-boolean-object@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.0.1.tgz#10edc0900dd127697a92f6f9807c7617d68ac48e"
|
||||
integrity sha512-TqZuVwa/sppcrhUCAYkGBk7w0yxfQQnxq28fjkO53tnK9FQXmdwz2JS5+GjsWQ6RByES1K40nI+yDic5c9/aAQ==
|
||||
|
||||
is-buffer@^1.1.5:
|
||||
version "1.1.6"
|
||||
|
@ -3343,6 +3370,7 @@ is-callable@^1.1.4, is-callable@^1.1.5:
|
|||
is-callable@^1.2.0:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.1.tgz#4d1e21a4f437509d25ce55f8184350771421c96d"
|
||||
integrity sha512-wliAfSzx6V+6WfMOmus1xy0XvSgf/dlStkvTfq7F0g4bOIW0PSUbnyse3NhDwdyYS1ozfUtAAySqTws3z9Eqgg==
|
||||
|
||||
is-ci@^2.0.0:
|
||||
version "2.0.0"
|
||||
|
@ -3430,6 +3458,7 @@ is-installed-globally@^0.3.2:
|
|||
is-map@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.1.tgz#520dafc4307bb8ebc33b813de5ce7c9400d644a1"
|
||||
integrity sha512-T/S49scO8plUiAOA2DBTBG3JHpn1yiw0kRp6dgiZ0v2/6twi5eiB0rHtHFH9ZIrvlWc6+4O+m4zg5+Z833aXgw==
|
||||
|
||||
is-module@^1.0.0:
|
||||
version "1.0.0"
|
||||
|
@ -3438,10 +3467,12 @@ is-module@^1.0.0:
|
|||
is-negative-zero@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.0.tgz#9553b121b0fac28869da9ed459e20c7543788461"
|
||||
integrity sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE=
|
||||
|
||||
is-number-object@^1.0.3:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.4.tgz#36ac95e741cf18b283fc1ddf5e83da798e3ec197"
|
||||
integrity sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw==
|
||||
|
||||
is-number@^3.0.0:
|
||||
version "3.0.0"
|
||||
|
@ -3502,12 +3533,14 @@ is-regex@^1.0.5:
|
|||
is-regex@^1.1.0, is-regex@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9"
|
||||
integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==
|
||||
dependencies:
|
||||
has-symbols "^1.0.1"
|
||||
|
||||
is-set@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.1.tgz#d1604afdab1724986d30091575f54945da7e5f43"
|
||||
integrity sha512-eJEzOtVyenDs1TMzSQ3kU3K+E0GUS9sno+F0OBT97xsgcJsF9nXMBtkT9/kut5JEpM7oL7X/0qxR17K3mcwIAA==
|
||||
|
||||
is-stream@^1.1.0:
|
||||
version "1.1.0"
|
||||
|
@ -3520,6 +3553,7 @@ is-stream@^2.0.0:
|
|||
is-string@^1.0.4, is-string@^1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6"
|
||||
integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==
|
||||
|
||||
is-symbol@^1.0.2:
|
||||
version "1.0.3"
|
||||
|
@ -3530,6 +3564,7 @@ is-symbol@^1.0.2:
|
|||
is-typed-array@^1.1.3:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.3.tgz#a4ff5a5e672e1a55f99c7f54e59597af5c1df04d"
|
||||
integrity sha512-BSYUBOK/HJibQ30wWkWold5txYwMUXQct9YHAQJr8fSwvZoiglcqB0pd7vEN23+Tsi9IUEjztdOSzl4qLVYGTQ==
|
||||
dependencies:
|
||||
available-typed-arrays "^1.0.0"
|
||||
es-abstract "^1.17.4"
|
||||
|
@ -3543,10 +3578,12 @@ is-typedarray@~1.0.0:
|
|||
is-weakmap@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2"
|
||||
integrity sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==
|
||||
|
||||
is-weakset@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.1.tgz#e9a0af88dbd751589f5e50d80f4c98b780884f83"
|
||||
integrity sha512-pi4vhbhVHGLxohUw7PhGsueT4vRGFoXhP7+RGN0jKIv9+8PWYCQTqtADngrxOm2g46hoH0+g8uZZBzMrvVGDmw==
|
||||
|
||||
is-windows@^1.0.2:
|
||||
version "1.0.2"
|
||||
|
@ -3571,6 +3608,7 @@ isarray@1.0.0, isarray@~1.0.0:
|
|||
isarray@^2.0.5:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
|
||||
integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==
|
||||
|
||||
isbuffer@~0.0.0:
|
||||
version "0.0.0"
|
||||
|
@ -3972,13 +4010,22 @@ jest-watcher@^24.9.0:
|
|||
jest-util "^24.9.0"
|
||||
string-length "^2.0.0"
|
||||
|
||||
jest-worker@^24.0.0, jest-worker@^24.6.0, jest-worker@^24.9.0:
|
||||
jest-worker@^24.6.0, jest-worker@^24.9.0:
|
||||
version "24.9.0"
|
||||
resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-24.9.0.tgz#5dbfdb5b2d322e98567898238a9697bcce67b3e5"
|
||||
dependencies:
|
||||
merge-stream "^2.0.0"
|
||||
supports-color "^6.1.0"
|
||||
|
||||
jest-worker@^26.2.1:
|
||||
version "26.3.0"
|
||||
resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.3.0.tgz#7c8a97e4f4364b4f05ed8bca8ca0c24de091871f"
|
||||
integrity sha512-Vmpn2F6IASefL+DVBhPzI2J9/GJUsqzomdeN+P+dK8/jKxbh8R3BtFnx3FIta7wYlPU62cpJMJQo4kuOowcMnw==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
merge-stream "^2.0.0"
|
||||
supports-color "^7.0.0"
|
||||
|
||||
jest@^24.8.0:
|
||||
version "24.9.0"
|
||||
resolved "https://registry.yarnpkg.com/jest/-/jest-24.9.0.tgz#987d290c05a08b52c56188c1002e368edb007171"
|
||||
|
@ -4685,10 +4732,12 @@ object-inspect@^1.7.0:
|
|||
object-inspect@^1.8.0:
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0"
|
||||
integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==
|
||||
|
||||
object-is@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.2.tgz#c5d2e87ff9e119f78b7a088441519e2eec1573b6"
|
||||
integrity sha512-5lHCz+0uufF6wZ7CRFWJN3hp8Jqblpgve06U5CMQ3f//6iDjPr2PEo9MWCjEssDsa+UZEL4PkFpr+BMop6aKzQ==
|
||||
dependencies:
|
||||
define-properties "^1.1.3"
|
||||
es-abstract "^1.17.5"
|
||||
|
@ -5086,7 +5135,7 @@ ramda@~0.26.1:
|
|||
version "0.26.1"
|
||||
resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.26.1.tgz#8d41351eb8111c55353617fc3bbffad8e4d35d06"
|
||||
|
||||
randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5:
|
||||
randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
|
||||
dependencies:
|
||||
|
@ -5206,6 +5255,7 @@ regex-not@^1.0.0, regex-not@^1.0.2:
|
|||
regexp.prototype.flags@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz#7aba89b3c13a64509dabcf3ca8d9fbb9bdf5cb75"
|
||||
integrity sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==
|
||||
dependencies:
|
||||
define-properties "^1.1.3"
|
||||
es-abstract "^1.17.0-next.1"
|
||||
|
@ -5213,6 +5263,7 @@ regexp.prototype.flags@^1.3.0:
|
|||
regexparam@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/regexparam/-/regexparam-1.3.0.tgz#2fe42c93e32a40eff6235d635e0ffa344b92965f"
|
||||
integrity sha512-6IQpFBv6e5vz1QAqI+V4k8P2e/3gRrqfCJ9FI+O1FLQTO+Uz6RXZEZOPmTJ6hlGj7gkERzY5BRCv09whKP96/g==
|
||||
|
||||
regexpu-core@^4.7.0:
|
||||
version "4.7.0"
|
||||
|
@ -5450,14 +5501,15 @@ rollup-plugin-svelte@^5.0.3:
|
|||
rollup-pluginutils "^2.8.2"
|
||||
sourcemap-codec "^1.4.8"
|
||||
|
||||
rollup-plugin-terser@^4.0.4:
|
||||
version "4.0.4"
|
||||
resolved "https://registry.yarnpkg.com/rollup-plugin-terser/-/rollup-plugin-terser-4.0.4.tgz#6f661ef284fa7c27963d242601691dc3d23f994e"
|
||||
rollup-plugin-terser@^7.0.2:
|
||||
version "7.0.2"
|
||||
resolved "https://registry.yarnpkg.com/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz#e8fbba4869981b2dc35ae7e8a502d5c6c04d324d"
|
||||
integrity sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==
|
||||
dependencies:
|
||||
"@babel/code-frame" "^7.0.0"
|
||||
jest-worker "^24.0.0"
|
||||
serialize-javascript "^1.6.1"
|
||||
terser "^3.14.1"
|
||||
"@babel/code-frame" "^7.10.4"
|
||||
jest-worker "^26.2.1"
|
||||
serialize-javascript "^4.0.0"
|
||||
terser "^5.0.0"
|
||||
|
||||
rollup-plugin-url@^2.2.2:
|
||||
version "2.2.4"
|
||||
|
@ -5473,13 +5525,12 @@ rollup-pluginutils@^2.3.1, rollup-pluginutils@^2.8.1, rollup-pluginutils@^2.8.2:
|
|||
dependencies:
|
||||
estree-walker "^0.6.1"
|
||||
|
||||
rollup@^1.12.0:
|
||||
version "1.32.1"
|
||||
resolved "https://registry.yarnpkg.com/rollup/-/rollup-1.32.1.tgz#4480e52d9d9e2ae4b46ba0d9ddeaf3163940f9c4"
|
||||
dependencies:
|
||||
"@types/estree" "*"
|
||||
"@types/node" "*"
|
||||
acorn "^7.1.0"
|
||||
rollup@^2.11.2:
|
||||
version "2.27.0"
|
||||
resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.27.0.tgz#f2b70a8dd583bc3675b36686289aa9a51e27af4f"
|
||||
integrity sha512-1WlbhNdzhLjdhh2wsf6CDxmuBAYG+5O53fYqCcGv8aJOoX/ymCfCY6oZnvllXZzaC/Ng+lPPwq9EMbHOKc5ozA==
|
||||
optionalDependencies:
|
||||
fsevents "~2.1.2"
|
||||
|
||||
rsvp@^4.8.4:
|
||||
version "4.8.5"
|
||||
|
@ -5563,9 +5614,12 @@ semver@~2.3.1:
|
|||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-2.3.2.tgz#b9848f25d6cf36333073ec9ef8856d42f1233e52"
|
||||
|
||||
serialize-javascript@^1.6.1:
|
||||
version "1.9.1"
|
||||
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.9.1.tgz#cfc200aef77b600c47da9bb8149c943e798c2fdb"
|
||||
serialize-javascript@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa"
|
||||
integrity sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==
|
||||
dependencies:
|
||||
randombytes "^2.1.0"
|
||||
|
||||
set-blocking@^2.0.0:
|
||||
version "2.0.0"
|
||||
|
@ -5621,6 +5675,7 @@ shortid@^2.2.15:
|
|||
side-channel@^1.0.2:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.3.tgz#cdc46b057550bbab63706210838df5d4c19519c3"
|
||||
integrity sha512-A6+ByhlLkksFoUepsGxfj5x1gTSrs+OydsRptUxeNCabQpCFUvcwIczgOigI8vhY/OJCnPnyE9rGiwgvr9cS1g==
|
||||
dependencies:
|
||||
es-abstract "^1.18.0-next.0"
|
||||
object-inspect "^1.8.0"
|
||||
|
@ -5701,7 +5756,7 @@ source-map-resolve@^0.5.0, source-map-resolve@^0.5.2:
|
|||
source-map-url "^0.4.0"
|
||||
urix "^0.1.0"
|
||||
|
||||
source-map-support@^0.5.6, source-map-support@~0.5.10:
|
||||
source-map-support@^0.5.6, source-map-support@~0.5.12:
|
||||
version "0.5.19"
|
||||
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61"
|
||||
dependencies:
|
||||
|
@ -5955,6 +6010,13 @@ supports-color@^6.1.0:
|
|||
dependencies:
|
||||
has-flag "^3.0.0"
|
||||
|
||||
supports-color@^7.0.0:
|
||||
version "7.2.0"
|
||||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
|
||||
integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
|
||||
dependencies:
|
||||
has-flag "^4.0.0"
|
||||
|
||||
supports-color@^7.1.0:
|
||||
version "7.1.0"
|
||||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1"
|
||||
|
@ -5985,9 +6047,10 @@ svelte-simple-modal@^0.4.2:
|
|||
version "0.4.2"
|
||||
resolved "https://registry.yarnpkg.com/svelte-simple-modal/-/svelte-simple-modal-0.4.2.tgz#2cfe26ec8c0760b89813d65dfee836399620d6b2"
|
||||
|
||||
svelte@3.23.x:
|
||||
version "3.23.0"
|
||||
resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.23.0.tgz#bbcd6887cf588c24a975b14467455abfff9acd3f"
|
||||
svelte@^3.24.1:
|
||||
version "3.25.1"
|
||||
resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.25.1.tgz#218def1243fea5a97af6eb60f5e232315bb57ac4"
|
||||
integrity sha512-IbrVKTmuR0BvDw4ii8/gBNy8REu7nWTRy9uhUz+Yuae5lIjWgSGwKlWtJGC2Vg95s+UnXPqDu0Kk/sUwe0t2GQ==
|
||||
|
||||
symbol-observable@^1.1.0:
|
||||
version "1.2.0"
|
||||
|
@ -6001,13 +6064,14 @@ synchronous-promise@^2.0.13:
|
|||
version "2.0.13"
|
||||
resolved "https://registry.yarnpkg.com/synchronous-promise/-/synchronous-promise-2.0.13.tgz#9d8c165ddee69c5a6542862b405bc50095926702"
|
||||
|
||||
terser@^3.14.1:
|
||||
version "3.17.0"
|
||||
resolved "https://registry.yarnpkg.com/terser/-/terser-3.17.0.tgz#f88ffbeda0deb5637f9d24b0da66f4e15ab10cb2"
|
||||
terser@^5.0.0:
|
||||
version "5.3.1"
|
||||
resolved "https://registry.yarnpkg.com/terser/-/terser-5.3.1.tgz#f50fe20ab48b15234fe9bdd86b10148ad5fca787"
|
||||
integrity sha512-yD80f4hdwCWTH5mojzxe1q8bN1oJbsK/vfJGLcPZM/fl+/jItIVNKhFIHqqR71OipFWMLgj3Kc+GIp6CeIqfnA==
|
||||
dependencies:
|
||||
commander "^2.19.0"
|
||||
commander "^2.20.0"
|
||||
source-map "~0.6.1"
|
||||
source-map-support "~0.5.10"
|
||||
source-map-support "~0.5.12"
|
||||
|
||||
test-exclude@^5.2.3:
|
||||
version "5.2.3"
|
||||
|
@ -6323,6 +6387,7 @@ whatwg-url@^8.0.0:
|
|||
which-boxed-primitive@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.1.tgz#cbe8f838ebe91ba2471bb69e9edbda67ab5a5ec1"
|
||||
integrity sha512-7BT4TwISdDGBgaemWU0N0OU7FeAEJ9Oo2P1PHRm/FCWoEi2VLWC9b6xvxAA3C/NMpxg3HXVgi0sMmGbNUbNepQ==
|
||||
dependencies:
|
||||
is-bigint "^1.0.0"
|
||||
is-boolean-object "^1.0.0"
|
||||
|
@ -6333,6 +6398,7 @@ which-boxed-primitive@^1.0.1:
|
|||
which-collection@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.1.tgz#70eab71ebbbd2aefaf32f917082fc62cdcb70906"
|
||||
integrity sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==
|
||||
dependencies:
|
||||
is-map "^2.0.1"
|
||||
is-set "^2.0.1"
|
||||
|
@ -6346,6 +6412,7 @@ which-module@^2.0.0:
|
|||
which-typed-array@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.2.tgz#e5f98e56bda93e3dac196b01d47c1156679c00b2"
|
||||
integrity sha512-KT6okrd1tE6JdZAy3o2VhMoYPh3+J6EMZLyrxBQsZflI1QCZIxMrIYLkosd8Twf+YfknVIHmYQPgJt238p8dnQ==
|
||||
dependencies:
|
||||
available-typed-arrays "^1.0.2"
|
||||
es-abstract "^1.17.5"
|
||||
|
|
|
@ -50,6 +50,7 @@
|
|||
"bcryptjs": "^2.4.3",
|
||||
"chmodr": "^1.2.0",
|
||||
"dotenv": "^8.2.0",
|
||||
"download": "^8.0.0",
|
||||
"electron-is-dev": "^1.2.0",
|
||||
"electron-unhandled": "^3.0.2",
|
||||
"electron-updater": "^4.3.1",
|
||||
|
|
|
@ -0,0 +1,130 @@
|
|||
const CouchDB = require("../../db")
|
||||
const newid = require("../../db/newid")
|
||||
const actions = require("../../automations/actions")
|
||||
const logic = require("../../automations/logic")
|
||||
const triggers = require("../../automations/triggers")
|
||||
|
||||
/*************************
|
||||
* *
|
||||
* BUILDER FUNCTIONS *
|
||||
* *
|
||||
*************************/
|
||||
|
||||
function cleanAutomationInputs(automation) {
|
||||
if (automation == null) {
|
||||
return automation
|
||||
}
|
||||
let steps = automation.definition.steps
|
||||
let trigger = automation.definition.trigger
|
||||
let allSteps = [...steps, trigger]
|
||||
for (let step of allSteps) {
|
||||
if (step == null) {
|
||||
continue
|
||||
}
|
||||
for (let inputName of Object.keys(step.inputs)) {
|
||||
if (!step.inputs[inputName] || step.inputs[inputName] === "") {
|
||||
delete step.inputs[inputName]
|
||||
}
|
||||
}
|
||||
}
|
||||
return automation
|
||||
}
|
||||
|
||||
exports.create = async function(ctx) {
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
let automation = ctx.request.body
|
||||
|
||||
automation._id = newid()
|
||||
|
||||
automation.type = "automation"
|
||||
automation = cleanAutomationInputs(automation)
|
||||
const response = await db.post(automation)
|
||||
automation._rev = response.rev
|
||||
|
||||
ctx.status = 200
|
||||
ctx.body = {
|
||||
message: "Automation created successfully",
|
||||
automation: {
|
||||
...automation,
|
||||
...response,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
exports.update = async function(ctx) {
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
let automation = ctx.request.body
|
||||
|
||||
automation = cleanAutomationInputs(automation)
|
||||
const response = await db.put(automation)
|
||||
automation._rev = response.rev
|
||||
|
||||
ctx.status = 200
|
||||
ctx.body = {
|
||||
message: `Automation ${automation._id} updated successfully.`,
|
||||
automation: {
|
||||
...automation,
|
||||
_rev: response.rev,
|
||||
_id: response.id,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
exports.fetch = async function(ctx) {
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
const response = await db.query(`database/by_type`, {
|
||||
key: ["automation"],
|
||||
include_docs: true,
|
||||
})
|
||||
ctx.body = response.rows.map(row => row.doc)
|
||||
}
|
||||
|
||||
exports.find = async function(ctx) {
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
ctx.body = await db.get(ctx.params.id)
|
||||
}
|
||||
|
||||
exports.destroy = async function(ctx) {
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
ctx.body = await db.remove(ctx.params.id, ctx.params.rev)
|
||||
}
|
||||
|
||||
exports.getActionList = async function(ctx) {
|
||||
ctx.body = actions.DEFINITIONS
|
||||
}
|
||||
|
||||
exports.getTriggerList = async function(ctx) {
|
||||
ctx.body = triggers.BUILTIN_DEFINITIONS
|
||||
}
|
||||
|
||||
exports.getLogicList = async function(ctx) {
|
||||
ctx.body = logic.BUILTIN_DEFINITIONS
|
||||
}
|
||||
|
||||
module.exports.getDefinitionList = async function(ctx) {
|
||||
ctx.body = {
|
||||
logic: logic.BUILTIN_DEFINITIONS,
|
||||
trigger: triggers.BUILTIN_DEFINITIONS,
|
||||
action: actions.DEFINITIONS,
|
||||
}
|
||||
}
|
||||
|
||||
/*********************
|
||||
* *
|
||||
* API FUNCTIONS *
|
||||
* *
|
||||
*********************/
|
||||
|
||||
exports.trigger = async function(ctx) {
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
let automation = await db.get(ctx.params.id)
|
||||
await triggers.externalTrigger(automation, {
|
||||
...ctx.request.body,
|
||||
instanceId: ctx.user.instanceId,
|
||||
})
|
||||
ctx.status = 200
|
||||
ctx.body = {
|
||||
message: `Automation ${automation._id} has been triggered.`,
|
||||
automation,
|
||||
}
|
||||
}
|
|
@ -31,9 +31,9 @@ exports.create = async function(ctx) {
|
|||
emit([doc.type], doc._id)
|
||||
}.toString(),
|
||||
},
|
||||
by_workflow_trigger: {
|
||||
by_automation_trigger: {
|
||||
map: function(doc) {
|
||||
if (doc.type === "workflow") {
|
||||
if (doc.type === "automation") {
|
||||
const trigger = doc.definition.trigger
|
||||
if (trigger) {
|
||||
emit([trigger.event], trigger)
|
||||
|
|
|
@ -3,13 +3,18 @@ const validateJs = require("validate.js")
|
|||
const newid = require("../../db/newid")
|
||||
|
||||
function emitEvent(eventType, ctx, record) {
|
||||
ctx.eventEmitter &&
|
||||
ctx.eventEmitter.emit(eventType, {
|
||||
args: {
|
||||
record,
|
||||
},
|
||||
instanceId: ctx.user.instanceId,
|
||||
})
|
||||
let event = {
|
||||
record,
|
||||
instanceId: ctx.user.instanceId,
|
||||
}
|
||||
// add syntactic sugar for mustache later
|
||||
if (record._id) {
|
||||
event.id = record._id
|
||||
}
|
||||
if (record._rev) {
|
||||
event.revision = record._rev
|
||||
}
|
||||
ctx.eventEmitter && ctx.eventEmitter.emit(eventType, event)
|
||||
}
|
||||
|
||||
validateJs.extend(validateJs.validators.datetime, {
|
||||
|
@ -53,7 +58,6 @@ exports.patch = async function(ctx) {
|
|||
ctx.body = record
|
||||
ctx.status = 200
|
||||
ctx.message = `${model.name} updated successfully.`
|
||||
return
|
||||
}
|
||||
|
||||
exports.save = async function(ctx) {
|
||||
|
@ -179,10 +183,13 @@ exports.destroy = async function(ctx) {
|
|||
const db = new CouchDB(ctx.user.instanceId)
|
||||
const record = await db.get(ctx.params.recordId)
|
||||
if (record.modelId !== ctx.params.modelId) {
|
||||
ctx.throw(400, "Supplied modelId doe not match the record's modelId")
|
||||
ctx.throw(400, "Supplied modelId doesn't match the record's modelId")
|
||||
return
|
||||
}
|
||||
ctx.body = await db.remove(ctx.params.recordId, ctx.params.revId)
|
||||
ctx.status = 200
|
||||
// for automations
|
||||
ctx.record = record
|
||||
emitEvent(`record:delete`, ctx, record)
|
||||
}
|
||||
|
||||
|
|
|
@ -56,6 +56,7 @@ exports.create = async function(ctx) {
|
|||
|
||||
ctx.status = 200
|
||||
ctx.message = "User created successfully."
|
||||
ctx.userId = response._id
|
||||
ctx.body = {
|
||||
_rev: response.rev,
|
||||
username,
|
||||
|
|
|
@ -12,7 +12,7 @@ const controller = {
|
|||
!name.startsWith("all") &&
|
||||
name !== "by_type" &&
|
||||
name !== "by_username" &&
|
||||
name !== "by_workflow_trigger"
|
||||
name !== "by_automation_trigger"
|
||||
) {
|
||||
response.push({
|
||||
name,
|
||||
|
|
|
@ -1,113 +0,0 @@
|
|||
const ACTION = {
|
||||
SAVE_RECORD: {
|
||||
name: "Save Record",
|
||||
tagline: "<b>Save</b> a <b>{{record.model.name}}</b> record",
|
||||
icon: "ri-save-3-fill",
|
||||
description: "Save a record to your database.",
|
||||
params: {
|
||||
record: "record",
|
||||
},
|
||||
args: {
|
||||
record: {},
|
||||
},
|
||||
type: "ACTION",
|
||||
},
|
||||
DELETE_RECORD: {
|
||||
description: "Delete a record from your database.",
|
||||
icon: "ri-delete-bin-line",
|
||||
name: "Delete Record",
|
||||
tagline: "<b>Delete</b> a <b>{{record.model.name}}</b> record",
|
||||
params: {},
|
||||
args: {},
|
||||
type: "ACTION",
|
||||
},
|
||||
CREATE_USER: {
|
||||
description: "Create a new user.",
|
||||
tagline: "Create user <b>{{username}}</b>",
|
||||
icon: "ri-user-add-fill",
|
||||
name: "Create User",
|
||||
params: {
|
||||
username: "string",
|
||||
password: "password",
|
||||
accessLevelId: "accessLevel",
|
||||
},
|
||||
args: {
|
||||
accessLevelId: "POWER_USER",
|
||||
},
|
||||
type: "ACTION",
|
||||
},
|
||||
SEND_EMAIL: {
|
||||
description: "Send an email.",
|
||||
tagline: "Send email to <b>{{to}}</b>",
|
||||
icon: "ri-mail-open-fill",
|
||||
name: "Send Email",
|
||||
params: {
|
||||
to: "string",
|
||||
from: "string",
|
||||
subject: "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",
|
||||
},
|
||||
}
|
||||
|
||||
const TRIGGER = {
|
||||
RECORD_SAVED: {
|
||||
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",
|
||||
},
|
||||
RECORD_DELETED: {
|
||||
name: "Record Deleted",
|
||||
event: "record:delete",
|
||||
icon: "ri-delete-bin-line",
|
||||
tagline: "Record is deleted from <b>{{model.name}}</b>",
|
||||
description: "Fired when a record is deleted from your database.",
|
||||
params: {
|
||||
model: "model",
|
||||
},
|
||||
type: "TRIGGER",
|
||||
},
|
||||
}
|
||||
|
||||
// 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,
|
||||
LOGIC,
|
||||
TRIGGER,
|
||||
}
|
|
@ -1,107 +0,0 @@
|
|||
const CouchDB = require("../../../db")
|
||||
const newid = require("../../../db/newid")
|
||||
const blockDefinitions = require("./blockDefinitions")
|
||||
const triggers = require("../../../workflows/triggers")
|
||||
|
||||
/*************************
|
||||
* *
|
||||
* BUILDER FUNCTIONS *
|
||||
* *
|
||||
*************************/
|
||||
|
||||
exports.create = async function(ctx) {
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
const workflow = ctx.request.body
|
||||
|
||||
workflow._id = newid()
|
||||
|
||||
workflow.type = "workflow"
|
||||
const response = await db.post(workflow)
|
||||
workflow._rev = response.rev
|
||||
|
||||
ctx.status = 200
|
||||
ctx.body = {
|
||||
message: "Workflow created successfully",
|
||||
workflow: {
|
||||
...workflow,
|
||||
...response,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
exports.update = async function(ctx) {
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
const workflow = ctx.request.body
|
||||
|
||||
const response = await db.put(workflow)
|
||||
workflow._rev = response.rev
|
||||
|
||||
ctx.status = 200
|
||||
ctx.body = {
|
||||
message: `Workflow ${workflow._id} updated successfully.`,
|
||||
workflow: {
|
||||
...workflow,
|
||||
_rev: response.rev,
|
||||
_id: response.id,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
exports.fetch = async function(ctx) {
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
const response = await db.query(`database/by_type`, {
|
||||
key: ["workflow"],
|
||||
include_docs: true,
|
||||
})
|
||||
ctx.body = response.rows.map(row => row.doc)
|
||||
}
|
||||
|
||||
exports.find = async function(ctx) {
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
ctx.body = await db.get(ctx.params.id)
|
||||
}
|
||||
|
||||
exports.destroy = async function(ctx) {
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
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,
|
||||
}
|
||||
}
|
|
@ -16,7 +16,7 @@ const {
|
|||
viewRoutes,
|
||||
staticRoutes,
|
||||
componentRoutes,
|
||||
workflowRoutes,
|
||||
automationRoutes,
|
||||
accesslevelRoutes,
|
||||
apiKeysRoutes,
|
||||
} = require("./routes")
|
||||
|
@ -84,8 +84,8 @@ router.use(userRoutes.allowedMethods())
|
|||
router.use(instanceRoutes.routes())
|
||||
router.use(instanceRoutes.allowedMethods())
|
||||
|
||||
router.use(workflowRoutes.routes())
|
||||
router.use(workflowRoutes.allowedMethods())
|
||||
router.use(automationRoutes.routes())
|
||||
router.use(automationRoutes.allowedMethods())
|
||||
|
||||
router.use(deployRoutes.routes())
|
||||
router.use(deployRoutes.allowedMethods())
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
const Router = require("@koa/router")
|
||||
const controller = require("../controllers/workflow")
|
||||
const controller = require("../controllers/automation")
|
||||
const authorized = require("../../middleware/authorized")
|
||||
const joiValidator = require("../../middleware/joi-validator")
|
||||
const { BUILDER } = require("../../utilities/accessLevels")
|
||||
|
@ -23,55 +23,57 @@ function generateStepSchema(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))
|
||||
function generateValidator(existing = false) {
|
||||
// prettier-ignore
|
||||
return joiValidator.body(Joi.object({
|
||||
live: Joi.bool(),
|
||||
_id: existing ? Joi.string().required() : Joi.string(),
|
||||
_rev: existing ? Joi.string().required() : Joi.string(),
|
||||
name: Joi.string().required(),
|
||||
type: Joi.string().valid("automation").required(),
|
||||
definition: Joi.object({
|
||||
steps: Joi.array().required().items(generateStepSchema(["ACTION", "LOGIC"])),
|
||||
trigger: generateStepSchema(["TRIGGER"]),
|
||||
}).required().unknown(true),
|
||||
}).unknown(true))
|
||||
}
|
||||
|
||||
router
|
||||
.get(
|
||||
"/api/workflows/trigger/list",
|
||||
"/api/automations/trigger/list",
|
||||
authorized(BUILDER),
|
||||
controller.getTriggerList
|
||||
)
|
||||
.get(
|
||||
"/api/workflows/action/list",
|
||||
"/api/automations/action/list",
|
||||
authorized(BUILDER),
|
||||
controller.getActionList
|
||||
)
|
||||
.get(
|
||||
"/api/workflows/logic/list",
|
||||
"/api/automations/logic/list",
|
||||
authorized(BUILDER),
|
||||
controller.getLogicList
|
||||
)
|
||||
.get(
|
||||
"/api/workflows/definitions/list",
|
||||
"/api/automations/definitions/list",
|
||||
authorized(BUILDER),
|
||||
controller.getDefinitionList
|
||||
)
|
||||
.get("/api/workflows", authorized(BUILDER), controller.fetch)
|
||||
.get("/api/workflows/:id", authorized(BUILDER), controller.find)
|
||||
.get("/api/automations", authorized(BUILDER), controller.fetch)
|
||||
.get("/api/automations/:id", authorized(BUILDER), controller.find)
|
||||
.put(
|
||||
"/api/workflows",
|
||||
"/api/automations",
|
||||
authorized(BUILDER),
|
||||
workflowValidator,
|
||||
generateValidator(true),
|
||||
controller.update
|
||||
)
|
||||
.post(
|
||||
"/api/workflows",
|
||||
"/api/automations",
|
||||
authorized(BUILDER),
|
||||
workflowValidator,
|
||||
generateValidator(false),
|
||||
controller.create
|
||||
)
|
||||
.post("/api/workflows/:id/trigger", controller.trigger)
|
||||
.delete("/api/workflows/:id/:rev", authorized(BUILDER), controller.destroy)
|
||||
.post("/api/automations/:id/trigger", controller.trigger)
|
||||
.delete("/api/automations/:id/:rev", authorized(BUILDER), controller.destroy)
|
||||
|
||||
module.exports = router
|
|
@ -9,7 +9,7 @@ const recordRoutes = require("./record")
|
|||
const viewRoutes = require("./view")
|
||||
const staticRoutes = require("./static")
|
||||
const componentRoutes = require("./component")
|
||||
const workflowRoutes = require("./workflow")
|
||||
const automationRoutes = require("./automation")
|
||||
const accesslevelRoutes = require("./accesslevel")
|
||||
const deployRoutes = require("./deploy")
|
||||
const apiKeysRoutes = require("./apikeys")
|
||||
|
@ -27,7 +27,7 @@ module.exports = {
|
|||
viewRoutes,
|
||||
staticRoutes,
|
||||
componentRoutes,
|
||||
workflowRoutes,
|
||||
automationRoutes,
|
||||
accesslevelRoutes,
|
||||
apiKeysRoutes,
|
||||
}
|
||||
|
|
|
@ -0,0 +1,260 @@
|
|||
const {
|
||||
createClientDatabase,
|
||||
createApplication,
|
||||
createInstance,
|
||||
createModel,
|
||||
getAllFromModel,
|
||||
defaultHeaders,
|
||||
supertest,
|
||||
insertDocument,
|
||||
destroyDocument,
|
||||
builderEndpointShouldBlockNormalUsers
|
||||
} = require("./couchTestUtils")
|
||||
|
||||
const { delay } = require("./testUtils")
|
||||
|
||||
const MAX_RETRIES = 4
|
||||
const TEST_AUTOMATION = {
|
||||
_id: "Test Automation",
|
||||
name: "My Automation",
|
||||
pageId: "123123123",
|
||||
screenId: "kasdkfldsafkl",
|
||||
live: true,
|
||||
uiTree: {
|
||||
|
||||
},
|
||||
definition: {
|
||||
trigger: {},
|
||||
steps: [
|
||||
],
|
||||
},
|
||||
type: "automation",
|
||||
}
|
||||
|
||||
let ACTION_DEFINITIONS = {}
|
||||
let TRIGGER_DEFINITIONS = {}
|
||||
let LOGIC_DEFINITIONS = {}
|
||||
|
||||
describe("/automations", () => {
|
||||
let request
|
||||
let server
|
||||
let app
|
||||
let instance
|
||||
let automation
|
||||
let automationId
|
||||
|
||||
beforeAll(async () => {
|
||||
({ request, server } = await supertest())
|
||||
await createClientDatabase(request)
|
||||
app = await createApplication(request)
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
if (automation) await destroyDocument(automation.id)
|
||||
instance = await createInstance(request, app._id)
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
server.close()
|
||||
})
|
||||
|
||||
const createAutomation = async () => {
|
||||
automation = await insertDocument(instance._id, {
|
||||
type: "automation",
|
||||
...TEST_AUTOMATION
|
||||
})
|
||||
automation = { ...automation, ...TEST_AUTOMATION }
|
||||
}
|
||||
|
||||
const triggerWorkflow = async (automationId) => {
|
||||
return await request
|
||||
.post(`/api/automations/${automationId}/trigger`)
|
||||
.send({ name: "Test", description: "TEST" })
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
}
|
||||
|
||||
describe("get definitions", () => {
|
||||
it("returns a list of definitions for actions", async () => {
|
||||
const res = await request
|
||||
.get(`/api/automations/action/list`)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
expect(Object.keys(res.body).length).not.toEqual(0)
|
||||
ACTION_DEFINITIONS = res.body
|
||||
})
|
||||
|
||||
it("returns a list of definitions for triggers", async () => {
|
||||
const res = await request
|
||||
.get(`/api/automations/trigger/list`)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
expect(Object.keys(res.body).length).not.toEqual(0)
|
||||
TRIGGER_DEFINITIONS = res.body
|
||||
})
|
||||
|
||||
it("returns a list of definitions for actions", async () => {
|
||||
const res = await request
|
||||
.get(`/api/automations/logic/list`)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
expect(Object.keys(res.body).length).not.toEqual(0)
|
||||
LOGIC_DEFINITIONS = res.body
|
||||
})
|
||||
|
||||
it("returns all of the definitions in one", async () => {
|
||||
const res = await request
|
||||
.get(`/api/automations/definitions/list`)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
expect(Object.keys(res.body.action).length).toEqual(Object.keys(ACTION_DEFINITIONS).length)
|
||||
expect(Object.keys(res.body.trigger).length).toEqual(Object.keys(TRIGGER_DEFINITIONS).length)
|
||||
expect(Object.keys(res.body.logic).length).toEqual(Object.keys(LOGIC_DEFINITIONS).length)
|
||||
})
|
||||
})
|
||||
|
||||
describe("create", () => {
|
||||
it("should setup the automation fully", () => {
|
||||
let trigger = TRIGGER_DEFINITIONS["RECORD_SAVED"]
|
||||
trigger.id = "wadiawdo34"
|
||||
let saveAction = ACTION_DEFINITIONS["SAVE_RECORD"]
|
||||
saveAction.inputs.record = {
|
||||
name: "{{trigger.name}}",
|
||||
description: "{{trigger.description}}"
|
||||
}
|
||||
saveAction.id = "awde444wk"
|
||||
|
||||
TEST_AUTOMATION.definition.steps.push(saveAction)
|
||||
TEST_AUTOMATION.definition.trigger = trigger
|
||||
})
|
||||
|
||||
it("returns a success message when the automation is successfully created", async () => {
|
||||
const res = await request
|
||||
.post(`/api/automations`)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.send(TEST_AUTOMATION)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
expect(res.body.message).toEqual("Automation created successfully")
|
||||
expect(res.body.automation.name).toEqual("My Automation")
|
||||
expect(res.body.automation._id).not.toEqual(null)
|
||||
automationId = res.body.automation._id
|
||||
})
|
||||
|
||||
it("should apply authorization to endpoint", async () => {
|
||||
await builderEndpointShouldBlockNormalUsers({
|
||||
request,
|
||||
method: "POST",
|
||||
url: `/api/automations`,
|
||||
instanceId: instance._id,
|
||||
appId: app._id,
|
||||
body: TEST_AUTOMATION
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("trigger", () => {
|
||||
it("trigger the automation successfully", async () => {
|
||||
let model = await createModel(request, app._id, instance._id)
|
||||
TEST_AUTOMATION.definition.trigger.inputs.modelId = model._id
|
||||
TEST_AUTOMATION.definition.steps[0].inputs.record.modelId = model._id
|
||||
await createAutomation()
|
||||
// this looks a bit mad but we don't actually have a way to wait for a response from the automation to
|
||||
// know that it has finished all of its actions - this is currently the best way
|
||||
// also when this runs in CI it is very temper-mental so for now trying to make run stable by repeating until it works
|
||||
// TODO: update when workflow logs are a thing
|
||||
for (let tries = 0; tries < MAX_RETRIES; tries++) {
|
||||
const res = await triggerWorkflow(automation._id)
|
||||
expect(res.body.message).toEqual(`Automation ${automation._id} has been triggered.`)
|
||||
expect(res.body.automation.name).toEqual(TEST_AUTOMATION.name)
|
||||
await delay(500)
|
||||
let elements = await getAllFromModel(request, app._id, instance._id, model._id)
|
||||
// don't test it unless there are values to test
|
||||
if (elements.length === 1) {
|
||||
expect(elements.length).toEqual(1)
|
||||
expect(elements[0].name).toEqual("Test")
|
||||
expect(elements[0].description).toEqual("TEST")
|
||||
return
|
||||
}
|
||||
}
|
||||
throw "Failed to find the records"
|
||||
})
|
||||
})
|
||||
|
||||
describe("update", () => {
|
||||
it("updates a automations data", async () => {
|
||||
await createAutomation()
|
||||
automation._id = automation.id
|
||||
automation._rev = automation.rev
|
||||
automation.name = "Updated Name"
|
||||
automation.type = "automation"
|
||||
|
||||
const res = await request
|
||||
.put(`/api/automations`)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.send(automation)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
expect(res.body.message).toEqual("Automation Test Automation updated successfully.")
|
||||
expect(res.body.automation.name).toEqual("Updated Name")
|
||||
})
|
||||
})
|
||||
|
||||
describe("fetch", () => {
|
||||
it("return all the automations for an instance", async () => {
|
||||
await createAutomation()
|
||||
const res = await request
|
||||
.get(`/api/automations`)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
expect(res.body[0]).toEqual(expect.objectContaining(TEST_AUTOMATION))
|
||||
})
|
||||
|
||||
it("should apply authorization to endpoint", async () => {
|
||||
await builderEndpointShouldBlockNormalUsers({
|
||||
request,
|
||||
method: "GET",
|
||||
url: `/api/automations`,
|
||||
instanceId: instance._id,
|
||||
appId: app._id,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("destroy", () => {
|
||||
it("deletes a automation by its ID", async () => {
|
||||
await createAutomation()
|
||||
const res = await request
|
||||
.delete(`/api/automations/${automation.id}/${automation.rev}`)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
expect(res.body.id).toEqual(TEST_AUTOMATION._id)
|
||||
})
|
||||
|
||||
it("should apply authorization to endpoint", async () => {
|
||||
await createAutomation()
|
||||
await builderEndpointShouldBlockNormalUsers({
|
||||
request,
|
||||
method: "DELETE",
|
||||
url: `/api/automations/${automation.id}/${automation._rev}`,
|
||||
instanceId: instance._id,
|
||||
appId: app._id,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -67,6 +67,13 @@ exports.createModel = async (request, appId, instanceId, model) => {
|
|||
return res.body
|
||||
}
|
||||
|
||||
exports.getAllFromModel = async (request, appId, instanceId, modelId) => {
|
||||
const res = await request
|
||||
.get(`/api/${modelId}/records`)
|
||||
.set(exports.defaultHeaders(appId, instanceId))
|
||||
return res.body
|
||||
}
|
||||
|
||||
exports.createView = async (request, appId, instanceId, modelId, view) => {
|
||||
view = view || {
|
||||
map: "function(doc) { emit(doc[doc.key], doc._id); } ",
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
module.exports.delay = ms => new Promise(resolve => setTimeout(resolve, ms))
|
|
@ -1,153 +0,0 @@
|
|||
const {
|
||||
createClientDatabase,
|
||||
createApplication,
|
||||
createInstance,
|
||||
defaultHeaders,
|
||||
supertest,
|
||||
insertDocument,
|
||||
destroyDocument,
|
||||
builderEndpointShouldBlockNormalUsers
|
||||
} = require("./couchTestUtils")
|
||||
|
||||
const TEST_WORKFLOW = {
|
||||
_id: "Test Workflow",
|
||||
name: "My Workflow",
|
||||
pageId: "123123123",
|
||||
screenId: "kasdkfldsafkl",
|
||||
live: true,
|
||||
uiTree: {
|
||||
|
||||
},
|
||||
definition: {
|
||||
triggers: [
|
||||
|
||||
],
|
||||
next: {
|
||||
stepId: "abc123",
|
||||
type: "SERVER",
|
||||
conditions: {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe("/workflows", () => {
|
||||
let request
|
||||
let server
|
||||
let app
|
||||
let instance
|
||||
let workflow
|
||||
|
||||
beforeAll(async () => {
|
||||
({ request, server } = await supertest())
|
||||
await createClientDatabase(request)
|
||||
app = await createApplication(request)
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
instance = await createInstance(request, app._id)
|
||||
if (workflow) await destroyDocument(workflow.id);
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
server.close()
|
||||
})
|
||||
|
||||
const createWorkflow = async () => {
|
||||
workflow = await insertDocument(instance._id, {
|
||||
type: "workflow",
|
||||
...TEST_WORKFLOW
|
||||
});
|
||||
}
|
||||
|
||||
describe("create", () => {
|
||||
it("returns a success message when the workflow is successfully created", async () => {
|
||||
const res = await request
|
||||
.post(`/api/workflows`)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.send(TEST_WORKFLOW)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
expect(res.body.message).toEqual("Workflow created successfully");
|
||||
expect(res.body.workflow.name).toEqual("My Workflow");
|
||||
})
|
||||
|
||||
it("should apply authorization to endpoint", async () => {
|
||||
await builderEndpointShouldBlockNormalUsers({
|
||||
request,
|
||||
method: "POST",
|
||||
url: `/api/workflows`,
|
||||
instanceId: instance._id,
|
||||
appId: app._id,
|
||||
body: TEST_WORKFLOW
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("update", () => {
|
||||
it("updates a workflows data", async () => {
|
||||
await createWorkflow();
|
||||
workflow._id = workflow.id
|
||||
workflow._rev = workflow.rev
|
||||
workflow.name = "Updated Name";
|
||||
|
||||
const res = await request
|
||||
.put(`/api/workflows`)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.send(workflow)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
expect(res.body.message).toEqual("Workflow Test Workflow updated successfully.");
|
||||
expect(res.body.workflow.name).toEqual("Updated Name");
|
||||
})
|
||||
})
|
||||
|
||||
describe("fetch", () => {
|
||||
it("return all the workflows for an instance", async () => {
|
||||
await createWorkflow();
|
||||
const res = await request
|
||||
.get(`/api/workflows`)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
expect(res.body[0]).toEqual(expect.objectContaining(TEST_WORKFLOW));
|
||||
})
|
||||
|
||||
it("should apply authorization to endpoint", async () => {
|
||||
await builderEndpointShouldBlockNormalUsers({
|
||||
request,
|
||||
method: "GET",
|
||||
url: `/api/workflows`,
|
||||
instanceId: instance._id,
|
||||
appId: app._id,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("destroy", () => {
|
||||
it("deletes a workflow by its ID", async () => {
|
||||
await createWorkflow();
|
||||
const res = await request
|
||||
.delete(`/api/workflows/${workflow.id}/${workflow.rev}`)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
expect(res.body.id).toEqual(TEST_WORKFLOW._id);
|
||||
})
|
||||
|
||||
it("should apply authorization to endpoint", async () => {
|
||||
await createWorkflow();
|
||||
await builderEndpointShouldBlockNormalUsers({
|
||||
request,
|
||||
method: "DELETE",
|
||||
url: `/api/workflows/${workflow.id}/${workflow._rev}`,
|
||||
instanceId: instance._id,
|
||||
appId: app._id,
|
||||
})
|
||||
})
|
||||
})
|
||||
});
|
|
@ -6,7 +6,7 @@ const http = require("http")
|
|||
const api = require("./api")
|
||||
const env = require("./environment")
|
||||
const eventEmitter = require("./events")
|
||||
const workflows = require("./workflows/index")
|
||||
const automations = require("./automations/index")
|
||||
const Sentry = require("@sentry/node")
|
||||
|
||||
const app = new Koa()
|
||||
|
@ -50,5 +50,5 @@ process.on("SIGINT", () => process.exit(1))
|
|||
|
||||
module.exports = server.listen(env.PORT || 4001, () => {
|
||||
console.log(`Budibase running on ${JSON.stringify(server.address())}`)
|
||||
workflows.init()
|
||||
automations.init()
|
||||
})
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
const sendEmail = require("./steps/sendEmail")
|
||||
const saveRecord = require("./steps/saveRecord")
|
||||
const updateRecord = require("./steps/updateRecord")
|
||||
const deleteRecord = require("./steps/deleteRecord")
|
||||
const createUser = require("./steps/createUser")
|
||||
const environment = require("../environment")
|
||||
const download = require("download")
|
||||
const fetch = require("node-fetch")
|
||||
const path = require("path")
|
||||
const os = require("os")
|
||||
const fs = require("fs")
|
||||
const Sentry = require("@sentry/node")
|
||||
|
||||
const DEFAULT_BUCKET =
|
||||
"https://prod-budi-automations.s3-eu-west-1.amazonaws.com"
|
||||
const DEFAULT_DIRECTORY = ".budibase-automations"
|
||||
const AUTOMATION_MANIFEST = "manifest.json"
|
||||
const BUILTIN_ACTIONS = {
|
||||
SEND_EMAIL: sendEmail.run,
|
||||
SAVE_RECORD: saveRecord.run,
|
||||
UPDATE_RECORD: updateRecord.run,
|
||||
DELETE_RECORD: deleteRecord.run,
|
||||
CREATE_USER: createUser.run,
|
||||
}
|
||||
const BUILTIN_DEFINITIONS = {
|
||||
SEND_EMAIL: sendEmail.definition,
|
||||
SAVE_RECORD: saveRecord.definition,
|
||||
UPDATE_RECORD: updateRecord.definition,
|
||||
DELETE_RECORD: deleteRecord.definition,
|
||||
CREATE_USER: createUser.definition,
|
||||
}
|
||||
|
||||
let AUTOMATION_BUCKET = environment.AUTOMATION_BUCKET
|
||||
let AUTOMATION_DIRECTORY = environment.AUTOMATION_DIRECTORY
|
||||
let MANIFEST = null
|
||||
|
||||
function buildBundleName(pkgName, version) {
|
||||
return `${pkgName}@${version}.min.js`
|
||||
}
|
||||
|
||||
async function downloadPackage(name, version, bundleName) {
|
||||
await download(
|
||||
`${AUTOMATION_BUCKET}/${name}/${version}/${bundleName}`,
|
||||
AUTOMATION_DIRECTORY
|
||||
)
|
||||
return require(path.join(AUTOMATION_DIRECTORY, bundleName))
|
||||
}
|
||||
|
||||
module.exports.getAction = async function(actionName) {
|
||||
if (BUILTIN_ACTIONS[actionName] != null) {
|
||||
return BUILTIN_ACTIONS[actionName]
|
||||
}
|
||||
// env setup to get async packages
|
||||
if (!MANIFEST || !MANIFEST.packages || !MANIFEST.packages[actionName]) {
|
||||
return null
|
||||
}
|
||||
const pkg = MANIFEST.packages[actionName]
|
||||
const bundleName = buildBundleName(pkg.stepId, pkg.version)
|
||||
try {
|
||||
return require(path.join(AUTOMATION_DIRECTORY, bundleName))
|
||||
} catch (err) {
|
||||
return downloadPackage(pkg.stepId, pkg.version, bundleName)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.init = async function() {
|
||||
// set defaults
|
||||
if (!AUTOMATION_DIRECTORY) {
|
||||
AUTOMATION_DIRECTORY = path.join(os.homedir(), DEFAULT_DIRECTORY)
|
||||
}
|
||||
if (!AUTOMATION_BUCKET) {
|
||||
AUTOMATION_BUCKET = DEFAULT_BUCKET
|
||||
}
|
||||
if (!fs.existsSync(AUTOMATION_DIRECTORY)) {
|
||||
fs.mkdirSync(AUTOMATION_DIRECTORY, { recursive: true })
|
||||
}
|
||||
// env setup to get async packages
|
||||
try {
|
||||
let response = await fetch(`${AUTOMATION_BUCKET}/${AUTOMATION_MANIFEST}`)
|
||||
MANIFEST = await response.json()
|
||||
module.exports.DEFINITIONS =
|
||||
MANIFEST && MANIFEST.packages
|
||||
? Object.assign(MANIFEST.packages, BUILTIN_DEFINITIONS)
|
||||
: BUILTIN_DEFINITIONS
|
||||
} catch (err) {
|
||||
Sentry.captureException(err)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.DEFINITIONS = BUILTIN_DEFINITIONS
|
||||
module.exports.BUILTIN_DEFINITIONS = BUILTIN_DEFINITIONS
|
|
@ -0,0 +1,116 @@
|
|||
const CouchDB = require("../db")
|
||||
|
||||
/**
|
||||
* When running mustache statements to execute on the context of the automation it possible user's may input mustache
|
||||
* in a few different forms, some of which are invalid but are logically valid. An example of this would be the mustache
|
||||
* statement "{{steps[0].revision}}" here it is obvious the user is attempting to access an array or object using array
|
||||
* like operators. These are not supported by Mustache and therefore the statement will fail. This function will clean up
|
||||
* the mustache statement so it instead reads as "{{steps.0.revision}}" which is valid and will work. It may also be expanded
|
||||
* to include any other mustache statement cleanup that has been deemed necessary for the system.
|
||||
*
|
||||
* @param {string} string The string which *may* contain mustache statements, it is OK if it does not contain any.
|
||||
* @returns {string} The string that was input with cleaned up mustache statements as required.
|
||||
*/
|
||||
module.exports.cleanMustache = string => {
|
||||
let charToReplace = {
|
||||
"[": ".",
|
||||
"]": "",
|
||||
}
|
||||
let regex = new RegExp(/{{[^}}]*}}/g)
|
||||
let matches = string.match(regex)
|
||||
if (matches == null) {
|
||||
return string
|
||||
}
|
||||
for (let match of matches) {
|
||||
let baseIdx = string.indexOf(match)
|
||||
for (let key of Object.keys(charToReplace)) {
|
||||
let idxChar = match.indexOf(key)
|
||||
if (idxChar !== -1) {
|
||||
string =
|
||||
string.slice(baseIdx, baseIdx + idxChar) +
|
||||
charToReplace[key] +
|
||||
string.slice(baseIdx + idxChar + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
return string
|
||||
}
|
||||
|
||||
/**
|
||||
* When values are input to the system generally they will be of type string as this is required for mustache. This can
|
||||
* generate some odd scenarios as the Schema of the automation requires a number but the builder might supply a string
|
||||
* with mustache syntax to get the number from the rest of the context. To support this the server has to make sure that
|
||||
* the post mustache statement can be cast into the correct type, this function does this for numbers and booleans.
|
||||
*
|
||||
* @param {object} inputs An object of inputs, please note this will not recurse down into any objects within, it simply
|
||||
* cleanses the top level inputs, however it can be used by recursively calling it deeper into the object structures if
|
||||
* the schema is known.
|
||||
* @param {object} schema The defined schema of the inputs, in the form of JSON schema. The schema definition of an
|
||||
* automation is the likely use case of this, however validate.js syntax can be converted closely enough to use this by
|
||||
* wrapping the schema properties in a top level "properties" object.
|
||||
* @returns {object} The inputs object which has had all the various types supported by this function converted to their
|
||||
* primitive types.
|
||||
*/
|
||||
module.exports.cleanInputValues = (inputs, schema) => {
|
||||
if (schema == null) {
|
||||
return inputs
|
||||
}
|
||||
for (let inputKey of Object.keys(inputs)) {
|
||||
let input = inputs[inputKey]
|
||||
if (typeof input !== "string") {
|
||||
continue
|
||||
}
|
||||
let propSchema = schema.properties[inputKey]
|
||||
if (!propSchema) {
|
||||
continue
|
||||
}
|
||||
if (propSchema.type === "boolean") {
|
||||
let lcInput = input.toLowerCase()
|
||||
if (lcInput === "true") {
|
||||
inputs[inputKey] = true
|
||||
}
|
||||
if (lcInput === "false") {
|
||||
inputs[inputKey] = false
|
||||
}
|
||||
}
|
||||
if (propSchema.type === "number") {
|
||||
let floatInput = parseFloat(input)
|
||||
if (!isNaN(floatInput)) {
|
||||
inputs[inputKey] = floatInput
|
||||
}
|
||||
}
|
||||
}
|
||||
return inputs
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a record input like a save or update record we need to clean the inputs against a schema that is not part of
|
||||
* the automation but is instead part of the Table/Model. This function will get the model schema and use it to instead
|
||||
* perform the cleanInputValues function on the input record.
|
||||
*
|
||||
* @param {string} instanceId The instance which the Table/Model is contained under.
|
||||
* @param {string} modelId The ID of the Table/Model which the schema is to be retrieved for.
|
||||
* @param {object} record The input record structure which requires clean-up after having been through mustache statements.
|
||||
* @returns {Promise<Object>} The cleaned up records object, will should now have all the required primitive types.
|
||||
*/
|
||||
module.exports.cleanUpRecord = async (instanceId, modelId, record) => {
|
||||
const db = new CouchDB(instanceId)
|
||||
const model = await db.get(modelId)
|
||||
|
||||
return module.exports.cleanInputValues(record, { properties: model.schema })
|
||||
}
|
||||
|
||||
/**
|
||||
* A utility function for the cleanUpRecord, which can be used if only the record ID is known (not the model ID) to clean
|
||||
* up a record after mustache statements have been replaced. This is specifically useful for the update record action.
|
||||
*
|
||||
* @param {string} instanceId The instance which the Table/Model is contained under.
|
||||
* @param {string} recordId The ID of the record from which the modelId will be extracted, to get the Table/Model schema.
|
||||
* @param {object} record The input record structure which requires clean-up after having been through mustache statements.
|
||||
* @returns {Promise<Object>} The cleaned up records object, which will now have all the required primitive types.
|
||||
*/
|
||||
module.exports.cleanUpRecordById = async (instanceId, recordId, record) => {
|
||||
const db = new CouchDB(instanceId)
|
||||
const foundRecord = await db.get(recordId)
|
||||
return module.exports.cleanUpRecord(instanceId, foundRecord.modelId, record)
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
const triggers = require("./triggers")
|
||||
const actions = require("./actions")
|
||||
const environment = require("../environment")
|
||||
const workerFarm = require("worker-farm")
|
||||
const singleThread = require("./thread")
|
||||
|
@ -21,11 +22,13 @@ function runWorker(job) {
|
|||
* 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)
|
||||
}
|
||||
actions.init().then(() => {
|
||||
triggers.automationQueue.process(async job => {
|
||||
if (environment.BUDIBASE_ENVIRONMENT === "PRODUCTION") {
|
||||
await runWorker(job)
|
||||
} else {
|
||||
await singleThread(job)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
let filter = require("./steps/filter")
|
||||
let delay = require("./steps/delay")
|
||||
|
||||
let BUILTIN_LOGIC = {
|
||||
DELAY: delay.run,
|
||||
FILTER: filter.run,
|
||||
}
|
||||
|
||||
let BUILTIN_DEFINITIONS = {
|
||||
DELAY: delay.definition,
|
||||
FILTER: filter.definition,
|
||||
}
|
||||
|
||||
module.exports.getLogic = function(logicName) {
|
||||
if (BUILTIN_LOGIC[logicName] != null) {
|
||||
return BUILTIN_LOGIC[logicName]
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.BUILTIN_DEFINITIONS = BUILTIN_DEFINITIONS
|
|
@ -0,0 +1,86 @@
|
|||
const accessLevels = require("../../utilities/accessLevels")
|
||||
const userController = require("../../api/controllers/user")
|
||||
|
||||
module.exports.definition = {
|
||||
description: "Create a new user",
|
||||
tagline: "Create user {{inputs.username}}",
|
||||
icon: "ri-user-add-fill",
|
||||
name: "Create User",
|
||||
type: "ACTION",
|
||||
stepId: "CREATE_USER",
|
||||
inputs: {
|
||||
accessLevelId: accessLevels.POWERUSER_LEVEL_ID,
|
||||
},
|
||||
schema: {
|
||||
inputs: {
|
||||
properties: {
|
||||
username: {
|
||||
type: "string",
|
||||
title: "Username",
|
||||
},
|
||||
password: {
|
||||
type: "string",
|
||||
customType: "password",
|
||||
title: "Password",
|
||||
},
|
||||
accessLevelId: {
|
||||
type: "string",
|
||||
title: "Access Level",
|
||||
enum: accessLevels.ACCESS_LEVELS,
|
||||
pretty: Object.values(accessLevels.PRETTY_ACCESS_LEVELS),
|
||||
},
|
||||
},
|
||||
required: ["username", "password", "accessLevelId"],
|
||||
},
|
||||
outputs: {
|
||||
properties: {
|
||||
id: {
|
||||
type: "string",
|
||||
description: "The identifier of the new user",
|
||||
},
|
||||
revision: {
|
||||
type: "string",
|
||||
description: "The revision of the new user",
|
||||
},
|
||||
response: {
|
||||
type: "object",
|
||||
description: "The response from the user table",
|
||||
},
|
||||
success: {
|
||||
type: "boolean",
|
||||
description: "Whether the action was successful",
|
||||
},
|
||||
},
|
||||
required: ["id", "revision", "success"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
module.exports.run = async function({ inputs, instanceId }) {
|
||||
const { username, password, accessLevelId } = inputs
|
||||
const ctx = {
|
||||
user: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
request: {
|
||||
body: { username, password, accessLevelId },
|
||||
},
|
||||
}
|
||||
|
||||
try {
|
||||
await userController.create(ctx)
|
||||
return {
|
||||
response: ctx.body,
|
||||
// internal property not returned through the API
|
||||
id: ctx.userId,
|
||||
revision: ctx.body._rev,
|
||||
success: ctx.status === 200,
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return {
|
||||
success: false,
|
||||
response: err,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
const wait = ms => new Promise(resolve => setTimeout(resolve, ms))
|
||||
|
||||
module.exports.definition = {
|
||||
name: "Delay",
|
||||
icon: "ri-time-fill",
|
||||
tagline: "Delay for {{inputs.time}} milliseconds",
|
||||
description: "Delay the automation until an amount of time has passed",
|
||||
stepId: "DELAY",
|
||||
inputs: {},
|
||||
schema: {
|
||||
inputs: {
|
||||
properties: {
|
||||
time: {
|
||||
type: "number",
|
||||
title: "Delay in milliseconds",
|
||||
},
|
||||
},
|
||||
required: ["time"],
|
||||
},
|
||||
},
|
||||
type: "LOGIC",
|
||||
}
|
||||
|
||||
module.exports.run = async function delay({ inputs }) {
|
||||
await wait(inputs.time)
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
const recordController = require("../../api/controllers/record")
|
||||
|
||||
module.exports.definition = {
|
||||
description: "Delete a record from your database",
|
||||
icon: "ri-delete-bin-line",
|
||||
name: "Delete Record",
|
||||
tagline: "Delete a {{inputs.enriched.model.name}} record",
|
||||
type: "ACTION",
|
||||
stepId: "DELETE_RECORD",
|
||||
inputs: {},
|
||||
schema: {
|
||||
inputs: {
|
||||
properties: {
|
||||
modelId: {
|
||||
type: "string",
|
||||
customType: "model",
|
||||
title: "Table",
|
||||
},
|
||||
id: {
|
||||
type: "string",
|
||||
title: "Record ID",
|
||||
},
|
||||
revision: {
|
||||
type: "string",
|
||||
title: "Record Revision",
|
||||
},
|
||||
},
|
||||
required: ["modelId", "id", "revision"],
|
||||
},
|
||||
outputs: {
|
||||
properties: {
|
||||
record: {
|
||||
type: "object",
|
||||
customType: "record",
|
||||
description: "The deleted record",
|
||||
},
|
||||
response: {
|
||||
type: "object",
|
||||
description: "The response from the table",
|
||||
},
|
||||
success: {
|
||||
type: "boolean",
|
||||
description: "Whether the action was successful",
|
||||
},
|
||||
},
|
||||
required: ["record", "success"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
module.exports.run = async function({ inputs, instanceId }) {
|
||||
// TODO: better logging of when actions are missed due to missing parameters
|
||||
if (inputs.id == null || inputs.revision == null) {
|
||||
return
|
||||
}
|
||||
let ctx = {
|
||||
params: {
|
||||
modelId: inputs.modelId,
|
||||
recordId: inputs.id,
|
||||
revId: inputs.revision,
|
||||
},
|
||||
user: { instanceId },
|
||||
}
|
||||
|
||||
try {
|
||||
await recordController.destroy(ctx)
|
||||
return {
|
||||
response: ctx.body,
|
||||
record: ctx.record,
|
||||
success: ctx.status === 200,
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return {
|
||||
success: false,
|
||||
response: err,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
const LogicConditions = {
|
||||
EQUAL: "EQUAL",
|
||||
NOT_EQUAL: "NOT_EQUAL",
|
||||
GREATER_THAN: "GREATER_THAN",
|
||||
LESS_THAN: "LESS_THAN",
|
||||
}
|
||||
|
||||
const PrettyLogicConditions = {
|
||||
[LogicConditions.EQUAL]: "Equals",
|
||||
[LogicConditions.NOT_EQUAL]: "Not equals",
|
||||
[LogicConditions.GREATER_THAN]: "Greater than",
|
||||
[LogicConditions.LESS_THAN]: "Less than",
|
||||
}
|
||||
|
||||
module.exports.definition = {
|
||||
name: "Filter",
|
||||
tagline: "{{inputs.field}} {{inputs.condition}} {{inputs.value}}",
|
||||
icon: "ri-git-branch-line",
|
||||
description: "Filter any automations which do not meet certain conditions",
|
||||
type: "LOGIC",
|
||||
stepId: "FILTER",
|
||||
inputs: {
|
||||
condition: LogicConditions.EQUALS,
|
||||
},
|
||||
schema: {
|
||||
inputs: {
|
||||
properties: {
|
||||
field: {
|
||||
type: "string",
|
||||
title: "Reference Value",
|
||||
},
|
||||
condition: {
|
||||
type: "string",
|
||||
title: "Condition",
|
||||
enum: Object.values(LogicConditions),
|
||||
pretty: Object.values(PrettyLogicConditions),
|
||||
},
|
||||
value: {
|
||||
type: "string",
|
||||
title: "Comparison Value",
|
||||
},
|
||||
},
|
||||
required: ["field", "condition", "value"],
|
||||
},
|
||||
outputs: {
|
||||
properties: {
|
||||
success: {
|
||||
type: "boolean",
|
||||
description: "Whether the logic block passed",
|
||||
},
|
||||
},
|
||||
required: ["success"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
module.exports.run = async function filter({ inputs }) {
|
||||
const { field, condition, value } = inputs
|
||||
let success
|
||||
if (typeof field !== "object" && typeof value !== "object") {
|
||||
switch (condition) {
|
||||
case LogicConditions.EQUAL:
|
||||
success = field === value
|
||||
break
|
||||
case LogicConditions.NOT_EQUAL:
|
||||
success = field !== value
|
||||
break
|
||||
case LogicConditions.GREATER_THAN:
|
||||
success = field > value
|
||||
break
|
||||
case LogicConditions.LESS_THAN:
|
||||
success = field < value
|
||||
break
|
||||
default:
|
||||
return
|
||||
}
|
||||
} else {
|
||||
success = false
|
||||
}
|
||||
return { success }
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
const recordController = require("../../api/controllers/record")
|
||||
const automationUtils = require("../automationUtils")
|
||||
|
||||
module.exports.definition = {
|
||||
name: "Save Record",
|
||||
tagline: "Save a {{inputs.enriched.model.name}} record",
|
||||
icon: "ri-save-3-fill",
|
||||
description: "Save a record to your database",
|
||||
type: "ACTION",
|
||||
stepId: "SAVE_RECORD",
|
||||
inputs: {},
|
||||
schema: {
|
||||
inputs: {
|
||||
properties: {
|
||||
record: {
|
||||
type: "object",
|
||||
properties: {
|
||||
modelId: {
|
||||
type: "string",
|
||||
customType: "model",
|
||||
},
|
||||
},
|
||||
customType: "record",
|
||||
title: "Table",
|
||||
required: ["modelId"],
|
||||
},
|
||||
},
|
||||
required: ["record"],
|
||||
},
|
||||
outputs: {
|
||||
properties: {
|
||||
record: {
|
||||
type: "object",
|
||||
customType: "record",
|
||||
description: "The new record",
|
||||
},
|
||||
response: {
|
||||
type: "object",
|
||||
description: "The response from the table",
|
||||
},
|
||||
success: {
|
||||
type: "boolean",
|
||||
description: "Whether the action was successful",
|
||||
},
|
||||
id: {
|
||||
type: "string",
|
||||
description: "The identifier of the new record",
|
||||
},
|
||||
revision: {
|
||||
type: "string",
|
||||
description: "The revision of the new record",
|
||||
},
|
||||
},
|
||||
required: ["success", "id", "revision"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
module.exports.run = async function({ inputs, instanceId }) {
|
||||
// TODO: better logging of when actions are missed due to missing parameters
|
||||
if (inputs.record == null || inputs.record.modelId == null) {
|
||||
return
|
||||
}
|
||||
inputs.record = await automationUtils.cleanUpRecord(
|
||||
instanceId,
|
||||
inputs.record.modelId,
|
||||
inputs.record
|
||||
)
|
||||
// have to clean up the record, remove the model from it
|
||||
const ctx = {
|
||||
params: {
|
||||
modelId: inputs.record.modelId,
|
||||
},
|
||||
request: {
|
||||
body: inputs.record,
|
||||
},
|
||||
user: { instanceId },
|
||||
}
|
||||
|
||||
try {
|
||||
await recordController.save(ctx)
|
||||
return {
|
||||
record: inputs.record,
|
||||
response: ctx.body,
|
||||
id: ctx.body._id,
|
||||
revision: ctx.body._rev,
|
||||
success: ctx.status === 200,
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return {
|
||||
success: false,
|
||||
response: err,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
const environment = require("../../environment")
|
||||
const sgMail = require("@sendgrid/mail")
|
||||
sgMail.setApiKey(environment.SENDGRID_API_KEY)
|
||||
|
||||
module.exports.definition = {
|
||||
description: "Send an email",
|
||||
tagline: "Send email to {{inputs.to}}",
|
||||
icon: "ri-mail-open-fill",
|
||||
name: "Send Email",
|
||||
type: "ACTION",
|
||||
stepId: "SEND_EMAIL",
|
||||
inputs: {},
|
||||
schema: {
|
||||
inputs: {
|
||||
properties: {
|
||||
to: {
|
||||
type: "string",
|
||||
title: "Send To",
|
||||
},
|
||||
from: {
|
||||
type: "string",
|
||||
title: "Send From",
|
||||
},
|
||||
subject: {
|
||||
type: "string",
|
||||
title: "Email Subject",
|
||||
},
|
||||
contents: {
|
||||
type: "string",
|
||||
title: "Email Contents",
|
||||
},
|
||||
},
|
||||
required: ["to", "from", "subject", "contents"],
|
||||
},
|
||||
outputs: {
|
||||
properties: {
|
||||
success: {
|
||||
type: "boolean",
|
||||
description: "Whether the email was sent",
|
||||
},
|
||||
response: {
|
||||
type: "object",
|
||||
description: "A response from the email client, this may be an error",
|
||||
},
|
||||
},
|
||||
required: ["success"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
module.exports.run = async function({ inputs }) {
|
||||
const msg = {
|
||||
to: inputs.to,
|
||||
from: inputs.from,
|
||||
subject: inputs.subject,
|
||||
text: inputs.contents ? inputs.contents : "Empty",
|
||||
}
|
||||
|
||||
try {
|
||||
let response = await sgMail.send(msg)
|
||||
return {
|
||||
success: true,
|
||||
response,
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return {
|
||||
success: false,
|
||||
response: err,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
const recordController = require("../../api/controllers/record")
|
||||
const automationUtils = require("../automationUtils")
|
||||
|
||||
module.exports.definition = {
|
||||
name: "Update Record",
|
||||
tagline: "Update a {{inputs.enriched.model.name}} record",
|
||||
icon: "ri-refresh-fill",
|
||||
description: "Update a record to your database",
|
||||
type: "ACTION",
|
||||
stepId: "UPDATE_RECORD",
|
||||
inputs: {},
|
||||
schema: {
|
||||
inputs: {
|
||||
properties: {
|
||||
record: {
|
||||
type: "object",
|
||||
customType: "record",
|
||||
title: "Record",
|
||||
},
|
||||
recordId: {
|
||||
type: "string",
|
||||
title: "Record ID",
|
||||
},
|
||||
},
|
||||
required: ["record", "recordId"],
|
||||
},
|
||||
outputs: {
|
||||
properties: {
|
||||
record: {
|
||||
type: "object",
|
||||
customType: "record",
|
||||
description: "The updated record",
|
||||
},
|
||||
response: {
|
||||
type: "object",
|
||||
description: "The response from the table",
|
||||
},
|
||||
success: {
|
||||
type: "boolean",
|
||||
description: "Whether the action was successful",
|
||||
},
|
||||
id: {
|
||||
type: "string",
|
||||
description: "The identifier of the updated record",
|
||||
},
|
||||
revision: {
|
||||
type: "string",
|
||||
description: "The revision of the updated record",
|
||||
},
|
||||
},
|
||||
required: ["success", "id", "revision"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
module.exports.run = async function({ inputs, instanceId }) {
|
||||
if (inputs.recordId == null || inputs.record == null) {
|
||||
return
|
||||
}
|
||||
|
||||
inputs.record = await automationUtils.cleanUpRecordById(
|
||||
instanceId,
|
||||
inputs.recordId,
|
||||
inputs.record
|
||||
)
|
||||
// clear any falsy properties so that they aren't updated
|
||||
for (let propKey of Object.keys(inputs.record)) {
|
||||
if (!inputs.record[propKey] || inputs.record[propKey] === "") {
|
||||
delete inputs.record[propKey]
|
||||
}
|
||||
}
|
||||
|
||||
// have to clean up the record, remove the model from it
|
||||
const ctx = {
|
||||
params: {
|
||||
id: inputs.recordId,
|
||||
},
|
||||
request: {
|
||||
body: inputs.record,
|
||||
},
|
||||
user: { instanceId },
|
||||
}
|
||||
|
||||
try {
|
||||
await recordController.patch(ctx)
|
||||
return {
|
||||
record: ctx.body,
|
||||
response: ctx.message,
|
||||
id: ctx.body._id,
|
||||
revision: ctx.body._rev,
|
||||
success: ctx.status === 200,
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return {
|
||||
success: false,
|
||||
response: err,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
const mustache = require("mustache")
|
||||
const actions = require("./actions")
|
||||
const logic = require("./logic")
|
||||
const automationUtils = require("./automationUtils")
|
||||
|
||||
const FILTER_STEP_ID = logic.BUILTIN_DEFINITIONS.FILTER.stepId
|
||||
|
||||
function recurseMustache(inputs, context) {
|
||||
for (let key of Object.keys(inputs)) {
|
||||
let val = inputs[key]
|
||||
if (typeof val === "string") {
|
||||
val = automationUtils.cleanMustache(inputs[key])
|
||||
inputs[key] = mustache.render(val, context)
|
||||
}
|
||||
// this covers objects and arrays
|
||||
else if (typeof val === "object") {
|
||||
inputs[key] = recurseMustache(inputs[key], context)
|
||||
}
|
||||
}
|
||||
return inputs
|
||||
}
|
||||
|
||||
/**
|
||||
* The automation orchestrator is a class responsible for executing automations.
|
||||
* It handles the context of the automation and makes sure each step gets the correct
|
||||
* inputs and handles any outputs.
|
||||
*/
|
||||
class Orchestrator {
|
||||
constructor(automation, triggerOutput) {
|
||||
this._instanceId = triggerOutput.instanceId
|
||||
// remove from context
|
||||
delete triggerOutput.instanceId
|
||||
// step zero is never used as the mustache is zero indexed for customer facing
|
||||
this._context = { steps: [{}], trigger: triggerOutput }
|
||||
this._automation = automation
|
||||
}
|
||||
|
||||
async getStepFunctionality(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 automation step by name ${stepId}`
|
||||
}
|
||||
return step
|
||||
}
|
||||
|
||||
async execute() {
|
||||
let automation = this._automation
|
||||
for (let step of automation.definition.steps) {
|
||||
let stepFn = await this.getStepFunctionality(step.type, step.stepId)
|
||||
step.inputs = recurseMustache(step.inputs, this._context)
|
||||
step.inputs = automationUtils.cleanInputValues(
|
||||
step.inputs,
|
||||
step.schema.inputs
|
||||
)
|
||||
// instanceId is always passed
|
||||
try {
|
||||
const outputs = await stepFn({
|
||||
inputs: step.inputs,
|
||||
instanceId: this._instanceId,
|
||||
})
|
||||
if (step.stepId === FILTER_STEP_ID && !outputs.success) {
|
||||
break
|
||||
}
|
||||
this._context.steps.push(outputs)
|
||||
} catch (err) {
|
||||
console.error(`Automation error - ${step.stepId} - ${err}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// callback is required for worker-farm to state that the worker thread has completed
|
||||
module.exports = async (job, cb = null) => {
|
||||
try {
|
||||
const automationOrchestrator = new Orchestrator(
|
||||
job.data.automation,
|
||||
job.data.event
|
||||
)
|
||||
await automationOrchestrator.execute()
|
||||
if (cb) {
|
||||
cb()
|
||||
}
|
||||
} catch (err) {
|
||||
if (cb) {
|
||||
cb(err)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,178 @@
|
|||
const CouchDB = require("../db")
|
||||
const emitter = require("../events/index")
|
||||
const InMemoryQueue = require("./queue/inMemoryQueue")
|
||||
|
||||
let automationQueue = new InMemoryQueue()
|
||||
|
||||
const FAKE_STRING = "TEST"
|
||||
const FAKE_BOOL = false
|
||||
const FAKE_NUMBER = 1
|
||||
const FAKE_DATETIME = "1970-01-01T00:00:00.000Z"
|
||||
|
||||
const BUILTIN_DEFINITIONS = {
|
||||
RECORD_SAVED: {
|
||||
name: "Record Saved",
|
||||
event: "record:save",
|
||||
icon: "ri-save-line",
|
||||
tagline: "Record is added to {{inputs.enriched.model.name}}",
|
||||
description: "Fired when a record is saved to your database",
|
||||
stepId: "RECORD_SAVED",
|
||||
inputs: {},
|
||||
schema: {
|
||||
inputs: {
|
||||
properties: {
|
||||
modelId: {
|
||||
type: "string",
|
||||
customType: "model",
|
||||
title: "Table",
|
||||
},
|
||||
},
|
||||
required: ["modelId"],
|
||||
},
|
||||
outputs: {
|
||||
properties: {
|
||||
record: {
|
||||
type: "object",
|
||||
customType: "record",
|
||||
description: "The new record that was saved",
|
||||
},
|
||||
id: {
|
||||
type: "string",
|
||||
description: "Record ID - can be used for updating",
|
||||
},
|
||||
revision: {
|
||||
type: "string",
|
||||
description: "Revision of record",
|
||||
},
|
||||
},
|
||||
required: ["record", "id"],
|
||||
},
|
||||
},
|
||||
type: "TRIGGER",
|
||||
},
|
||||
RECORD_DELETED: {
|
||||
name: "Record Deleted",
|
||||
event: "record:delete",
|
||||
icon: "ri-delete-bin-line",
|
||||
tagline: "Record is deleted from {{inputs.enriched.model.name}}",
|
||||
description: "Fired when a record is deleted from your database",
|
||||
stepId: "RECORD_DELETED",
|
||||
inputs: {},
|
||||
schema: {
|
||||
inputs: {
|
||||
properties: {
|
||||
modelId: {
|
||||
type: "string",
|
||||
customType: "model",
|
||||
title: "Table",
|
||||
},
|
||||
},
|
||||
required: ["modelId"],
|
||||
},
|
||||
outputs: {
|
||||
properties: {
|
||||
record: {
|
||||
type: "object",
|
||||
customType: "record",
|
||||
description: "The record that was deleted",
|
||||
},
|
||||
},
|
||||
required: ["record", "id"],
|
||||
},
|
||||
},
|
||||
type: "TRIGGER",
|
||||
},
|
||||
}
|
||||
|
||||
async function queueRelevantRecordAutomations(event, eventType) {
|
||||
if (event.instanceId == null) {
|
||||
throw `No instanceId specified for ${eventType} - check event emitters.`
|
||||
}
|
||||
const db = new CouchDB(event.instanceId)
|
||||
const automationsToTrigger = await db.query(
|
||||
"database/by_automation_trigger",
|
||||
{
|
||||
key: [eventType],
|
||||
include_docs: true,
|
||||
}
|
||||
)
|
||||
|
||||
const automations = automationsToTrigger.rows.map(wf => wf.doc)
|
||||
for (let automation of automations) {
|
||||
let automationDef = automation.definition
|
||||
let automationTrigger = automationDef ? automationDef.trigger : {}
|
||||
if (
|
||||
!automation.live ||
|
||||
!automationTrigger.inputs ||
|
||||
automationTrigger.inputs.modelId !== event.record.modelId
|
||||
) {
|
||||
continue
|
||||
}
|
||||
automationQueue.add({ automation, event })
|
||||
}
|
||||
}
|
||||
|
||||
emitter.on("record:save", async function(event) {
|
||||
if (!event || !event.record || !event.record.modelId) {
|
||||
return
|
||||
}
|
||||
await queueRelevantRecordAutomations(event, "record:save")
|
||||
})
|
||||
|
||||
emitter.on("record:delete", async function(event) {
|
||||
if (!event || !event.record || !event.record.modelId) {
|
||||
return
|
||||
}
|
||||
await queueRelevantRecordAutomations(event, "record:delete")
|
||||
})
|
||||
|
||||
async function fillRecordOutput(automation, params) {
|
||||
let triggerSchema = automation.definition.trigger
|
||||
let modelId = triggerSchema.inputs.modelId
|
||||
const db = new CouchDB(params.instanceId)
|
||||
try {
|
||||
let model = await db.get(modelId)
|
||||
let record = {}
|
||||
for (let schemaKey of Object.keys(model.schema)) {
|
||||
if (params[schemaKey] != null) {
|
||||
continue
|
||||
}
|
||||
let propSchema = model.schema[schemaKey]
|
||||
switch (propSchema.constraints.type) {
|
||||
case "string":
|
||||
record[schemaKey] = FAKE_STRING
|
||||
break
|
||||
case "boolean":
|
||||
record[schemaKey] = FAKE_BOOL
|
||||
break
|
||||
case "number":
|
||||
record[schemaKey] = FAKE_NUMBER
|
||||
break
|
||||
case "datetime":
|
||||
record[schemaKey] = FAKE_DATETIME
|
||||
break
|
||||
}
|
||||
}
|
||||
params.record = record
|
||||
} catch (err) {
|
||||
throw "Failed to find model for trigger"
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
module.exports.externalTrigger = async function(automation, params) {
|
||||
// TODO: replace this with allowing user in builder to input values in future
|
||||
if (
|
||||
automation.definition != null &&
|
||||
automation.definition.trigger != null &&
|
||||
automation.definition.trigger.inputs.modelId != null
|
||||
) {
|
||||
params = await fillRecordOutput(automation, params)
|
||||
}
|
||||
|
||||
automationQueue.add({ automation, event: params })
|
||||
}
|
||||
|
||||
module.exports.automationQueue = automationQueue
|
||||
|
||||
module.exports.BUILTIN_DEFINITIONS = BUILTIN_DEFINITIONS
|
|
@ -7,5 +7,8 @@ module.exports = {
|
|||
COUCH_DB_URL: process.env.COUCH_DB_URL,
|
||||
SALT_ROUNDS: process.env.SALT_ROUNDS,
|
||||
LOGGER: process.env.LOGGER,
|
||||
AUTOMATION_DIRECTORY: process.env.AUTOMATION_DIRECTORY,
|
||||
AUTOMATION_BUCKET: process.env.AUTOMATION_BUCKET,
|
||||
BUDIBASE_ENVIRONMENT: process.env.BUDIBASE_ENVIRONMENT,
|
||||
SENDGRID_API_KEY: process.env.SENDGRID_API_KEY,
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ const EventEmitter = require("events").EventEmitter
|
|||
|
||||
/**
|
||||
* keeping event emitter in one central location as it might be used for things other than
|
||||
* workflows (what it was for originally) - having a central emitter will be useful in the
|
||||
* automations (what it was for originally) - having a central emitter will be useful in the
|
||||
* future.
|
||||
*/
|
||||
|
||||
|
|
|
@ -1,11 +1,19 @@
|
|||
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}`)
|
||||
}
|
||||
if (!schema) {
|
||||
return next()
|
||||
}
|
||||
let params = null
|
||||
if (ctx[property] != null) {
|
||||
params = ctx[property]
|
||||
} else if (ctx.request[property] != null) {
|
||||
params = ctx.request[property]
|
||||
}
|
||||
const { error } = schema.validate(params)
|
||||
if (error) {
|
||||
ctx.throw(400, `Invalid ${property} - ${error.message}`)
|
||||
return
|
||||
}
|
||||
return next()
|
||||
}
|
||||
|
|
|
@ -1,100 +1,36 @@
|
|||
const viewController = require("../api/controllers/view")
|
||||
const modelController = require("../api/controllers/model")
|
||||
const workflowController = require("../api/controllers/workflow")
|
||||
|
||||
// Access Level IDs
|
||||
const ADMIN_LEVEL_ID = "ADMIN"
|
||||
const POWERUSER_LEVEL_ID = "POWER_USER"
|
||||
const BUILDER_LEVEL_ID = "BUILDER"
|
||||
const ANON_LEVEL_ID = "ANON"
|
||||
|
||||
// Permissions
|
||||
const READ_MODEL = "read-model"
|
||||
const WRITE_MODEL = "write-model"
|
||||
const READ_VIEW = "read-view"
|
||||
const EXECUTE_WORKFLOW = "execute-workflow"
|
||||
const USER_MANAGEMENT = "user-management"
|
||||
const BUILDER = "builder"
|
||||
const LIST_USERS = "list-users"
|
||||
|
||||
const adminPermissions = [
|
||||
module.exports.READ_MODEL = "read-model"
|
||||
module.exports.WRITE_MODEL = "write-model"
|
||||
module.exports.READ_VIEW = "read-view"
|
||||
module.exports.EXECUTE_AUTOMATION = "execute-automation"
|
||||
module.exports.USER_MANAGEMENT = "user-management"
|
||||
module.exports.BUILDER = "builder"
|
||||
module.exports.LIST_USERS = "list-users"
|
||||
// Access Level IDs
|
||||
module.exports.ADMIN_LEVEL_ID = "ADMIN"
|
||||
module.exports.POWERUSER_LEVEL_ID = "POWER_USER"
|
||||
module.exports.BUILDER_LEVEL_ID = "BUILDER"
|
||||
module.exports.ANON_LEVEL_ID = "ANON"
|
||||
module.exports.ACCESS_LEVELS = [
|
||||
module.exports.ADMIN_LEVEL_ID,
|
||||
module.exports.POWERUSER_LEVEL_ID,
|
||||
module.exports.BUILDER_LEVEL_ID,
|
||||
module.exports.ANON_LEVEL_ID,
|
||||
]
|
||||
module.exports.PRETTY_ACCESS_LEVELS = {
|
||||
[module.exports.ADMIN_LEVEL_ID]: "Admin",
|
||||
[module.exports.POWERUSER_LEVEL_ID]: "Power user",
|
||||
[module.exports.BUILDER_LEVEL_ID]: "Builder",
|
||||
[module.exports.ANON_LEVEL_ID]: "Anonymous",
|
||||
}
|
||||
module.exports.adminPermissions = [
|
||||
{
|
||||
name: USER_MANAGEMENT,
|
||||
name: module.exports.USER_MANAGEMENT,
|
||||
},
|
||||
]
|
||||
|
||||
const generateAdminPermissions = async instanceId => [
|
||||
...adminPermissions,
|
||||
...(await generatePowerUserPermissions(instanceId)),
|
||||
]
|
||||
|
||||
const generatePowerUserPermissions = async instanceId => {
|
||||
const fetchModelsCtx = {
|
||||
user: {
|
||||
instanceId,
|
||||
},
|
||||
}
|
||||
await modelController.fetch(fetchModelsCtx)
|
||||
const models = fetchModelsCtx.body
|
||||
|
||||
const fetchViewsCtx = {
|
||||
user: {
|
||||
instanceId,
|
||||
},
|
||||
}
|
||||
await viewController.fetch(fetchViewsCtx)
|
||||
const views = fetchViewsCtx.body
|
||||
|
||||
const fetchWorkflowsCtx = {
|
||||
user: {
|
||||
instanceId,
|
||||
},
|
||||
}
|
||||
await workflowController.fetch(fetchWorkflowsCtx)
|
||||
const workflows = fetchWorkflowsCtx.body
|
||||
|
||||
const readModelPermissions = models.map(m => ({
|
||||
itemId: m._id,
|
||||
name: READ_MODEL,
|
||||
}))
|
||||
|
||||
const writeModelPermissions = models.map(m => ({
|
||||
itemId: m._id,
|
||||
name: WRITE_MODEL,
|
||||
}))
|
||||
|
||||
const viewPermissions = views.map(v => ({
|
||||
itemId: v.name,
|
||||
name: READ_VIEW,
|
||||
}))
|
||||
|
||||
const executeWorkflowPermissions = workflows.map(w => ({
|
||||
itemId: w._id,
|
||||
name: EXECUTE_WORKFLOW,
|
||||
}))
|
||||
|
||||
return [
|
||||
...readModelPermissions,
|
||||
...writeModelPermissions,
|
||||
...viewPermissions,
|
||||
...executeWorkflowPermissions,
|
||||
{ name: LIST_USERS },
|
||||
]
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ADMIN_LEVEL_ID,
|
||||
POWERUSER_LEVEL_ID,
|
||||
BUILDER_LEVEL_ID,
|
||||
ANON_LEVEL_ID,
|
||||
READ_MODEL,
|
||||
WRITE_MODEL,
|
||||
READ_VIEW,
|
||||
EXECUTE_WORKFLOW,
|
||||
USER_MANAGEMENT,
|
||||
BUILDER,
|
||||
LIST_USERS,
|
||||
adminPermissions,
|
||||
generateAdminPermissions,
|
||||
generatePowerUserPermissions,
|
||||
}
|
||||
// to avoid circular dependencies this is included later, after exporting all enums
|
||||
const permissions = require("./permissions")
|
||||
module.exports.generateAdminPermissions = permissions.generateAdminPermissions
|
||||
module.exports.generatePowerUserPermissions =
|
||||
permissions.generatePowerUserPermissions
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
const viewController = require("../api/controllers/view")
|
||||
const modelController = require("../api/controllers/model")
|
||||
const automationController = require("../api/controllers/automation")
|
||||
const accessLevels = require("./accessLevels")
|
||||
|
||||
// this has been broken out to reduce risk of circular dependency from utilities, no enums defined here
|
||||
const generateAdminPermissions = async instanceId => [
|
||||
...accessLevels.adminPermissions,
|
||||
...(await generatePowerUserPermissions(instanceId)),
|
||||
]
|
||||
|
||||
const generatePowerUserPermissions = async instanceId => {
|
||||
const fetchModelsCtx = {
|
||||
user: {
|
||||
instanceId,
|
||||
},
|
||||
}
|
||||
await modelController.fetch(fetchModelsCtx)
|
||||
const models = fetchModelsCtx.body
|
||||
|
||||
const fetchViewsCtx = {
|
||||
user: {
|
||||
instanceId,
|
||||
},
|
||||
}
|
||||
await viewController.fetch(fetchViewsCtx)
|
||||
const views = fetchViewsCtx.body
|
||||
|
||||
const fetchAutomationsCtx = {
|
||||
user: {
|
||||
instanceId,
|
||||
},
|
||||
}
|
||||
await automationController.fetch(fetchAutomationsCtx)
|
||||
const automations = fetchAutomationsCtx.body
|
||||
|
||||
const readModelPermissions = models.map(m => ({
|
||||
itemId: m._id,
|
||||
name: accessLevels.READ_MODEL,
|
||||
}))
|
||||
|
||||
const writeModelPermissions = models.map(m => ({
|
||||
itemId: m._id,
|
||||
name: accessLevels.WRITE_MODEL,
|
||||
}))
|
||||
|
||||
const viewPermissions = views.map(v => ({
|
||||
itemId: v.name,
|
||||
name: accessLevels.READ_VIEW,
|
||||
}))
|
||||
|
||||
const executeAutomationPermissions = automations.map(w => ({
|
||||
itemId: w._id,
|
||||
name: accessLevels.EXECUTE_AUTOMATION,
|
||||
}))
|
||||
|
||||
return [
|
||||
...readModelPermissions,
|
||||
...writeModelPermissions,
|
||||
...viewPermissions,
|
||||
...executeAutomationPermissions,
|
||||
{ name: accessLevels.LIST_USERS },
|
||||
]
|
||||
}
|
||||
module.exports.generateAdminPermissions = generateAdminPermissions
|
||||
module.exports.generatePowerUserPermissions = generatePowerUserPermissions
|
|
@ -1,112 +0,0 @@
|
|||
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
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue