Merge branch 'rename-workflow-automation' of github.com:Budibase/budibase into async-workflow-blocks
This commit is contained in:
commit
eb494b4698
|
@ -28,5 +28,8 @@
|
|||
"format": "prettier --write \"{,!(node_modules)/**/}*.{js,jsx,svelte}\"",
|
||||
"test:e2e": "lerna run cy:test",
|
||||
"test:e2e:ci": "lerna run cy:ci"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome": "^1.1.8"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,6 @@ node_modules_win
|
|||
package-lock.json
|
||||
release/
|
||||
dist/
|
||||
cypress/screenshots
|
||||
cypress/videos
|
||||
routify
|
||||
cypress/videos
|
||||
cypress/screenshots
|
||||
|
|
|
@ -1,27 +1,27 @@
|
|||
context("Create a workflow", () => {
|
||||
context("Create a automation", () => {
|
||||
before(() => {
|
||||
cy.server()
|
||||
cy.visit("localhost:4001/_builder")
|
||||
|
||||
cy.createApp(
|
||||
"Workflow Test App",
|
||||
"This app is used to test that workflows do in fact work!"
|
||||
"Automation Test App",
|
||||
"This app is used to test that automations do in fact work!"
|
||||
)
|
||||
})
|
||||
|
||||
// https://on.cypress.io/interacting-with-elements
|
||||
it("should create a workflow", () => {
|
||||
it("should create a automation", () => {
|
||||
cy.createTestTableWithData()
|
||||
|
||||
cy.contains("workflow").click()
|
||||
cy.contains("Create New Workflow").click()
|
||||
cy.contains("automate").click()
|
||||
cy.contains("Create New Automation").click()
|
||||
cy.get("input").type("Add Record")
|
||||
cy.contains("Save").click()
|
||||
|
||||
// Add trigger
|
||||
cy.get("[data-cy=add-workflow-component]").click()
|
||||
cy.get("[data-cy=add-automation-component]").click()
|
||||
cy.get("[data-cy=RECORD_SAVED]").click()
|
||||
cy.get("[data-cy=workflow-block-setup]").within(() => {
|
||||
cy.get("[data-cy=automation-block-setup]").within(() => {
|
||||
cy.get("select")
|
||||
.first()
|
||||
.select("dog")
|
||||
|
@ -29,7 +29,7 @@ context("Create a workflow", () => {
|
|||
|
||||
// Create action
|
||||
cy.get("[data-cy=SAVE_RECORD]").click()
|
||||
cy.get("[data-cy=workflow-block-setup]").within(() => {
|
||||
cy.get("[data-cy=automation-block-setup]").within(() => {
|
||||
cy.get("select")
|
||||
.first()
|
||||
.select("dog")
|
||||
|
@ -42,10 +42,10 @@ context("Create a workflow", () => {
|
|||
})
|
||||
|
||||
// Save
|
||||
cy.contains("Save Workflow").click()
|
||||
cy.contains("Save Automation").click()
|
||||
|
||||
// Activate Workflow
|
||||
cy.get("[data-cy=activate-workflow]").click()
|
||||
// Activate Automation
|
||||
cy.get("[data-cy=activate-automation]").click()
|
||||
cy.contains("Add Record").should("be.visible")
|
||||
cy.get(".stop-button.highlighted").should("be.visible")
|
||||
})
|
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.
|
@ -63,9 +63,10 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "^1.33.0",
|
||||
"@budibase/bbui": "^1.34.2",
|
||||
"@budibase/client": "^0.1.21",
|
||||
"@budibase/colorpicker": "^1.0.1",
|
||||
"@fortawesome/fontawesome-free": "^5.14.0",
|
||||
"@sentry/browser": "5.19.1",
|
||||
"@svelteschool/svelte-forms": "^0.7.0",
|
||||
"britecharts": "^2.16.0",
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
const apiCall = method => async (url, body) => {
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
const apiCall = method => async (
|
||||
url,
|
||||
body,
|
||||
headers = { "Content-Type": "application/json" }
|
||||
) => {
|
||||
const response = await fetch(url, {
|
||||
method: method,
|
||||
body: body && JSON.stringify(body),
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
/**
|
||||
* buildStateOrigins
|
||||
*
|
||||
* Builds an object that details all the bound state in the application, and what updates it.
|
||||
*
|
||||
* @param screenDefinition - the screen definition metadata.
|
||||
* @returns {Object} an object with the client state values and how they are managed.
|
||||
*/
|
||||
export const buildStateOrigins = screenDefinition => {
|
||||
const origins = {}
|
||||
|
||||
function traverse(propValue) {
|
||||
for (let key in propValue) {
|
||||
if (!Array.isArray(propValue[key])) continue
|
||||
|
||||
if (key === "_children") propValue[key].forEach(traverse)
|
||||
|
||||
for (let element of propValue[key]) {
|
||||
if (element["##eventHandlerType"] === "Set State")
|
||||
origins[element.parameters.path] = element
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
traverse(screenDefinition.props)
|
||||
|
||||
return origins
|
||||
}
|
|
@ -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",
|
|
@ -28,6 +28,11 @@ export const getBackendUiStore = () => {
|
|||
},
|
||||
},
|
||||
records: {
|
||||
save: () =>
|
||||
store.update(state => {
|
||||
state.selectedView = state.selectedView
|
||||
return state
|
||||
}),
|
||||
delete: () =>
|
||||
store.update(state => {
|
||||
state.selectedView = state.selectedView
|
||||
|
|
|
@ -1,126 +0,0 @@
|
|||
import { writable } from "svelte/store"
|
||||
import api from "../../api"
|
||||
import Workflow from "./Workflow"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
|
||||
const workflowActions = store => ({
|
||||
fetch: async () => {
|
||||
const responses = await Promise.all([
|
||||
api.get(`/api/workflows`),
|
||||
api.get(`/api/workflows/definitions/list`),
|
||||
])
|
||||
const jsonResponses = await Promise.all(responses.map(x => x.json()))
|
||||
store.update(state => {
|
||||
state.workflows = jsonResponses[0]
|
||||
state.blockDefinitions = {
|
||||
TRIGGER: jsonResponses[1].trigger,
|
||||
ACTION: jsonResponses[1].action,
|
||||
LOGIC: jsonResponses[1].logic,
|
||||
}
|
||||
return state
|
||||
})
|
||||
},
|
||||
create: async ({ name }) => {
|
||||
const workflow = {
|
||||
name,
|
||||
type: "workflow",
|
||||
definition: {
|
||||
steps: [],
|
||||
},
|
||||
}
|
||||
const CREATE_WORKFLOW_URL = `/api/workflows`
|
||||
const response = await api.post(CREATE_WORKFLOW_URL, workflow)
|
||||
const json = await response.json()
|
||||
store.update(state => {
|
||||
state.workflows = [...state.workflows, json.workflow]
|
||||
store.actions.select(json.workflow)
|
||||
return state
|
||||
})
|
||||
},
|
||||
save: async ({ workflow }) => {
|
||||
const UPDATE_WORKFLOW_URL = `/api/workflows`
|
||||
const response = await api.put(UPDATE_WORKFLOW_URL, workflow)
|
||||
const json = await response.json()
|
||||
store.update(state => {
|
||||
const existingIdx = state.workflows.findIndex(
|
||||
existing => existing._id === workflow._id
|
||||
)
|
||||
state.workflows.splice(existingIdx, 1, json.workflow)
|
||||
state.workflows = state.workflows
|
||||
store.actions.select(json.workflow)
|
||||
return state
|
||||
})
|
||||
},
|
||||
delete: async ({ workflow }) => {
|
||||
const { _id, _rev } = workflow
|
||||
const DELETE_WORKFLOW_URL = `/api/workflows/${_id}/${_rev}`
|
||||
await api.delete(DELETE_WORKFLOW_URL)
|
||||
|
||||
store.update(state => {
|
||||
const existingIdx = state.workflows.findIndex(
|
||||
existing => existing._id === _id
|
||||
)
|
||||
state.workflows.splice(existingIdx, 1)
|
||||
state.workflows = state.workflows
|
||||
state.selectedWorkflow = null
|
||||
state.selectedBlock = null
|
||||
return state
|
||||
})
|
||||
},
|
||||
trigger: async ({ workflow }) => {
|
||||
const { _id } = workflow
|
||||
const TRIGGER_WORKFLOW_URL = `/api/workflows/${_id}/trigger`
|
||||
return await api.post(TRIGGER_WORKFLOW_URL)
|
||||
},
|
||||
select: workflow => {
|
||||
store.update(state => {
|
||||
state.selectedWorkflow = new Workflow(cloneDeep(workflow))
|
||||
state.selectedBlock = null
|
||||
return state
|
||||
})
|
||||
},
|
||||
addBlockToWorkflow: block => {
|
||||
store.update(state => {
|
||||
const newBlock = state.selectedWorkflow.addBlock(cloneDeep(block))
|
||||
state.selectedBlock = newBlock
|
||||
return state
|
||||
})
|
||||
},
|
||||
deleteWorkflowBlock: block => {
|
||||
store.update(state => {
|
||||
const idx = state.selectedWorkflow.workflow.definition.steps.findIndex(
|
||||
x => x.id === block.id
|
||||
)
|
||||
state.selectedWorkflow.deleteBlock(block.id)
|
||||
|
||||
// Select next closest step
|
||||
const steps = state.selectedWorkflow.workflow.definition.steps
|
||||
let nextSelectedBlock
|
||||
if (steps[idx] != null) {
|
||||
nextSelectedBlock = steps[idx]
|
||||
} else if (steps[idx - 1] != null) {
|
||||
nextSelectedBlock = steps[idx - 1]
|
||||
} else {
|
||||
nextSelectedBlock =
|
||||
state.selectedWorkflow.workflow.definition.trigger || null
|
||||
}
|
||||
state.selectedBlock = nextSelectedBlock
|
||||
return state
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export const getWorkflowStore = () => {
|
||||
const INITIAL_WORKFLOW_STATE = {
|
||||
workflows: [],
|
||||
blockDefinitions: {
|
||||
TRIGGER: [],
|
||||
ACTION: [],
|
||||
LOGIC: [],
|
||||
},
|
||||
selectedWorkflow: null,
|
||||
}
|
||||
const store = writable(INITIAL_WORKFLOW_STATE)
|
||||
store.actions = workflowActions(store)
|
||||
return store
|
||||
}
|
|
@ -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,48 +1,48 @@
|
|||
<script>
|
||||
import { afterUpdate } from "svelte"
|
||||
import { workflowStore, backendUiStore } from "builderStore"
|
||||
import { automationStore, backendUiStore } from "builderStore"
|
||||
import { notifier } from "builderStore/store/notifications"
|
||||
import Flowchart from "./flowchart/FlowChart.svelte"
|
||||
|
||||
$: workflow = $workflowStore.selectedWorkflow?.workflow
|
||||
$: workflowLive = workflow?.live
|
||||
$: automation = $automationStore.selectedAutomation?.automation
|
||||
$: automationLive = automation?.live
|
||||
$: instanceId = $backendUiStore.selectedDatabase._id
|
||||
|
||||
function onSelect(block) {
|
||||
workflowStore.update(state => {
|
||||
automationStore.update(state => {
|
||||
state.selectedBlock = block
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
function setWorkflowLive(live) {
|
||||
workflow.live = live
|
||||
workflowStore.actions.save({ instanceId, workflow })
|
||||
function setAutomationLive(live) {
|
||||
automation.live = live
|
||||
automationStore.actions.save({ instanceId, automation })
|
||||
if (live) {
|
||||
notifier.info(`Workflow ${workflow.name} enabled.`)
|
||||
notifier.info(`Automation ${automation.name} enabled.`)
|
||||
} else {
|
||||
notifier.danger(`Workflow ${workflow.name} disabled.`)
|
||||
notifier.danger(`Automation ${automation.name} disabled.`)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<section>
|
||||
<Flowchart {workflow} {onSelect} />
|
||||
<Flowchart {automation} {onSelect} />
|
||||
</section>
|
||||
<footer>
|
||||
{#if workflow}
|
||||
{#if automation}
|
||||
<button
|
||||
class:highlighted={workflowLive}
|
||||
class:hoverable={workflowLive}
|
||||
class:highlighted={automationLive}
|
||||
class:hoverable={automationLive}
|
||||
class="stop-button hoverable">
|
||||
<i class="ri-stop-fill" on:click={() => setWorkflowLive(false)} />
|
||||
<i class="ri-stop-fill" on:click={() => setAutomationLive(false)} />
|
||||
</button>
|
||||
<button
|
||||
class:highlighted={!workflowLive}
|
||||
class:hoverable={!workflowLive}
|
||||
class:highlighted={!automationLive}
|
||||
class:hoverable={!automationLive}
|
||||
class="play-button hoverable"
|
||||
data-cy="activate-workflow"
|
||||
on:click={() => setWorkflowLive(true)}>
|
||||
data-cy="activate-automation"
|
||||
on:click={() => setAutomationLive(true)}>
|
||||
<i class="ri-play-fill" />
|
||||
</button>
|
||||
{/if}
|
Before Width: | Height: | Size: 290 B After Width: | Height: | Size: 290 B |
|
@ -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>
|
|
@ -1,13 +1,13 @@
|
|||
<script>
|
||||
import { workflowStore } from "builderStore"
|
||||
import WorkflowBlockTagline from "./WorkflowBlockTagline.svelte"
|
||||
import { automationStore } from "builderStore"
|
||||
import AutomationBlockTagline from "./AutomationBlockTagline.svelte"
|
||||
|
||||
export let onSelect
|
||||
export let block
|
||||
let selected
|
||||
|
||||
$: selected = $workflowStore.selectedBlock?.id === block.id
|
||||
$: steps = $workflowStore.selectedWorkflow?.workflow?.definition?.steps ?? []
|
||||
$: selected = $automationStore.selectedBlock?.id === block.id
|
||||
$: steps = $automationStore.selectedAutomation?.automation?.definition?.steps ?? []
|
||||
$: blockIdx = steps.findIndex(step => step.id === block.id)
|
||||
</script>
|
||||
|
||||
|
@ -32,7 +32,7 @@
|
|||
</header>
|
||||
<hr />
|
||||
<p>
|
||||
<WorkflowBlockTagline {block} />
|
||||
<AutomationBlockTagline {block} />
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -2,17 +2,17 @@
|
|||
import Modal from "svelte-simple-modal"
|
||||
import { notifier } from "builderStore/store/notifications"
|
||||
import { onMount, getContext } from "svelte"
|
||||
import { backendUiStore, workflowStore } from "builderStore"
|
||||
import CreateWorkflowModal from "./CreateWorkflowModal.svelte"
|
||||
import { backendUiStore, automationStore } from "builderStore"
|
||||
import CreateAutomationModal from "./CreateAutomationModal.svelte"
|
||||
import { Button } from "@budibase/bbui"
|
||||
|
||||
const { open, close } = getContext("simple-modal")
|
||||
|
||||
$: selectedWorkflowId = $workflowStore.selectedWorkflow?.workflow?._id
|
||||
$: selectedAutomationId = $automationStore.selectedAutomation?.automation?._id
|
||||
|
||||
function newWorkflow() {
|
||||
function newAutomation() {
|
||||
open(
|
||||
CreateWorkflowModal,
|
||||
CreateAutomationModal,
|
||||
{
|
||||
onClosed: close,
|
||||
},
|
||||
|
@ -21,20 +21,20 @@
|
|||
}
|
||||
|
||||
onMount(() => {
|
||||
workflowStore.actions.fetch()
|
||||
automationStore.actions.fetch()
|
||||
})
|
||||
</script>
|
||||
|
||||
<section>
|
||||
<Button purple wide on:click={newWorkflow}>Create New Workflow</Button>
|
||||
<Button purple wide on:click={newAutomation}>Create New Automation</Button>
|
||||
<ul>
|
||||
{#each $workflowStore.workflows as workflow}
|
||||
{#each $automationStore.automations as automation}
|
||||
<li
|
||||
class="workflow-item"
|
||||
class:selected={workflow._id === selectedWorkflowId}
|
||||
on:click={() => workflowStore.actions.select(workflow)}>
|
||||
<i class="ri-stackshare-line" class:live={workflow.live} />
|
||||
{workflow.name}
|
||||
class="automation-item"
|
||||
class:selected={automation._id === selectedAutomationId}
|
||||
on:click={() => automationStore.actions.select(automation)}>
|
||||
<i class="ri-stackshare-line" class:live={automation.live} />
|
||||
{automation.name}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
@ -68,7 +68,7 @@
|
|||
font-size: 14px;
|
||||
}
|
||||
|
||||
.workflow-item {
|
||||
.automation-item {
|
||||
display: flex;
|
||||
border-radius: 5px;
|
||||
padding-left: 12px;
|
||||
|
@ -78,21 +78,21 @@
|
|||
color: var(--ink);
|
||||
}
|
||||
|
||||
.workflow-item i {
|
||||
.automation-item i {
|
||||
font-size: 24px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.workflow-item:hover {
|
||||
.automation-item:hover {
|
||||
cursor: pointer;
|
||||
background: var(--grey-1);
|
||||
}
|
||||
|
||||
.workflow-item.selected {
|
||||
.automation-item.selected {
|
||||
background: var(--grey-2);
|
||||
}
|
||||
|
||||
.new-workflow-button {
|
||||
.new-automation-button {
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--grey-4);
|
||||
border-radius: 3px;
|
||||
|
@ -108,7 +108,7 @@
|
|||
transition: all 2ms;
|
||||
}
|
||||
|
||||
.new-workflow-button:hover {
|
||||
.new-automation-button:hover {
|
||||
background: var(--grey-1);
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { store, backendUiStore, workflowStore } from "builderStore"
|
||||
import { store, backendUiStore, automationStore } from "builderStore"
|
||||
import { notifier } from "builderStore/store/notifications"
|
||||
import ActionButton from "components/common/ActionButton.svelte"
|
||||
import { Input } from "@budibase/bbui"
|
||||
|
@ -12,19 +12,19 @@
|
|||
$: instanceId = $backendUiStore.selectedDatabase._id
|
||||
$: appId = $store.appId
|
||||
|
||||
async function createWorkflow() {
|
||||
await workflowStore.actions.create({
|
||||
async function createAutomation() {
|
||||
await automationStore.actions.create({
|
||||
name,
|
||||
instanceId,
|
||||
})
|
||||
onClosed()
|
||||
notifier.success(`Workflow ${name} created.`)
|
||||
notifier.success(`Automation ${name} created.`)
|
||||
}
|
||||
</script>
|
||||
|
||||
<header>
|
||||
<i class="ri-stackshare-line" />
|
||||
Create Workflow
|
||||
Create Automation
|
||||
</header>
|
||||
<div>
|
||||
<Input bind:value={name} label="Name" />
|
||||
|
@ -32,10 +32,10 @@
|
|||
<footer>
|
||||
<a href="https://docs.budibase.com">
|
||||
<i class="ri-information-line" />
|
||||
Learn about workflows
|
||||
Learn about automations
|
||||
</a>
|
||||
<ActionButton secondary on:click={onClosed}>Cancel</ActionButton>
|
||||
<ActionButton disabled={!valid} on:click={createWorkflow}>Save</ActionButton>
|
||||
<ActionButton disabled={!valid} on:click={createAutomation}>Save</ActionButton>
|
||||
</footer>
|
||||
|
||||
<style>
|
|
@ -1,22 +1,22 @@
|
|||
<script>
|
||||
import { workflowStore } from "builderStore"
|
||||
import WorkflowList from "./WorkflowList/WorkflowList.svelte"
|
||||
import { automationStore } from "builderStore"
|
||||
import AutomationList from "./AutomationList/AutomationList.svelte"
|
||||
import BlockList from "./BlockList/BlockList.svelte"
|
||||
|
||||
let selectedTab = "WORKFLOWS"
|
||||
let selectedTab = "AUTOMATIONS"
|
||||
</script>
|
||||
|
||||
<header>
|
||||
<span
|
||||
data-cy="workflow-list"
|
||||
class="hoverable workflow-header"
|
||||
class:selected={selectedTab === 'WORKFLOWS'}
|
||||
on:click={() => (selectedTab = 'WORKFLOWS')}>
|
||||
Workflows
|
||||
data-cy="automation-list"
|
||||
class="hoverable automation-header"
|
||||
class:selected={selectedTab === 'AUTOMATIONS'}
|
||||
on:click={() => (selectedTab = 'AUTOMATIONS')}>
|
||||
Automations
|
||||
</span>
|
||||
{#if $workflowStore.selectedWorkflow}
|
||||
{#if $automationStore.selectedAutomation}
|
||||
<span
|
||||
data-cy="add-workflow-component"
|
||||
data-cy="add-automation-component"
|
||||
class="hoverable"
|
||||
class:selected={selectedTab === 'ADD'}
|
||||
on:click={() => (selectedTab = 'ADD')}>
|
||||
|
@ -24,8 +24,8 @@
|
|||
</span>
|
||||
{/if}
|
||||
</header>
|
||||
{#if selectedTab === 'WORKFLOWS'}
|
||||
<WorkflowList />
|
||||
{#if selectedTab === 'AUTOMATIONS'}
|
||||
<AutomationList />
|
||||
{:else if selectedTab === 'ADD'}
|
||||
<BlockList />
|
||||
{/if}
|
||||
|
@ -40,7 +40,7 @@
|
|||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.workflow-header {
|
||||
.automation-header {
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
<script>
|
||||
import { workflowStore } from "builderStore"
|
||||
import { automationStore } from "builderStore"
|
||||
|
||||
export let blockDefinition
|
||||
export let stepId
|
||||
export let blockType
|
||||
|
||||
function addBlockToWorkflow() {
|
||||
workflowStore.actions.addBlockToWorkflow({
|
||||
function addBlockToAutomation() {
|
||||
automationStore.actions.addBlockToAutomation({
|
||||
...blockDefinition,
|
||||
args: blockDefinition.args || {},
|
||||
stepId,
|
||||
|
@ -16,20 +16,20 @@
|
|||
</script>
|
||||
|
||||
<div
|
||||
class="workflow-block hoverable"
|
||||
on:click={addBlockToWorkflow}
|
||||
class="automation-block hoverable"
|
||||
on:click={addBlockToAutomation}
|
||||
data-cy={stepId}>
|
||||
<div>
|
||||
<i class={blockDefinition.icon} />
|
||||
</div>
|
||||
<div class="workflow-text">
|
||||
<div class="automation-text">
|
||||
<h4>{blockDefinition.name}</h4>
|
||||
<p>{blockDefinition.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.workflow-block {
|
||||
.automation-block {
|
||||
display: grid;
|
||||
grid-template-columns: 20px auto;
|
||||
align-items: center;
|
||||
|
@ -38,11 +38,11 @@
|
|||
border-radius: var(--border-radius-m);
|
||||
}
|
||||
|
||||
.workflow-block:hover {
|
||||
.automation-block:hover {
|
||||
background-color: var(--grey-1);
|
||||
}
|
||||
|
||||
.workflow-text {
|
||||
.automation-text {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
|
@ -1,14 +1,14 @@
|
|||
<script>
|
||||
import { workflowStore } from "builderStore"
|
||||
import WorkflowBlock from "./WorkflowBlock.svelte"
|
||||
import { automationStore } from "builderStore"
|
||||
import AutomationBlock from "./AutomationBlock.svelte"
|
||||
import FlatButtonGroup from "components/userInterface/FlatButtonGroup.svelte"
|
||||
|
||||
let selectedTab = "TRIGGER"
|
||||
let buttonProps = []
|
||||
$: blocks = Object.entries($workflowStore.blockDefinitions[selectedTab])
|
||||
$: blocks = Object.entries($automationStore.blockDefinitions[selectedTab])
|
||||
|
||||
$: {
|
||||
if ($workflowStore.selectedWorkflow.hasTrigger()) {
|
||||
if ($automationStore.selectedAutomation.hasTrigger()) {
|
||||
buttonProps = [
|
||||
{ value: "ACTION", text: "Action" },
|
||||
{ value: "LOGIC", text: "Logic" },
|
||||
|
@ -33,7 +33,7 @@
|
|||
<FlatButtonGroup value={selectedTab} {buttonProps} onChange={onChangeTab} />
|
||||
<div id="blocklist">
|
||||
{#each blocks as [stepId, blockDefinition]}
|
||||
<WorkflowBlock {blockDefinition} {stepId} blockType={selectedTab} />
|
||||
<AutomationBlock {blockDefinition} {stepId} blockType={selectedTab} />
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
|
@ -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"
|
|
@ -2,25 +2,25 @@
|
|||
import ModelSelector from "./ParamInputs/ModelSelector.svelte"
|
||||
import RecordSelector from "./ParamInputs/RecordSelector.svelte"
|
||||
import { Input, TextArea, Select, Label } from "@budibase/bbui"
|
||||
import { workflowStore } from "builderStore"
|
||||
import { automationStore } from "builderStore"
|
||||
import BindableInput from "../../userInterface/BindableInput.svelte"
|
||||
|
||||
export let block
|
||||
$: inputs = Object.entries(block.schema?.inputs?.properties || {})
|
||||
$: bindings = getAvailableBindings(
|
||||
block,
|
||||
$workflowStore.selectedWorkflow?.workflow?.definition
|
||||
$automationStore.selectedAutomation?.automation?.definition
|
||||
)
|
||||
|
||||
function getAvailableBindings(block, workflow) {
|
||||
if (!block || !workflow) {
|
||||
function getAvailableBindings(block, automation) {
|
||||
if (!block || !automation) {
|
||||
return []
|
||||
}
|
||||
|
||||
// Find previous steps to the selected one
|
||||
let allSteps = [...workflow.steps]
|
||||
if (workflow.trigger) {
|
||||
allSteps = [workflow.trigger, ...allSteps]
|
||||
let allSteps = [...automation.steps]
|
||||
if (automation.trigger) {
|
||||
allSteps = [automation.trigger, ...allSteps]
|
||||
}
|
||||
const blockIdx = allSteps.findIndex(step => step.id === block.id)
|
||||
|
||||
|
@ -44,7 +44,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<div class="container" data-cy="workflow-block-setup">
|
||||
<div class="container" data-cy="automation-block-setup">
|
||||
<div class="block-label">{block.name}</div>
|
||||
{#each inputs as [key, value]}
|
||||
<div class="bb-margin-xl block-field">
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { store, backendUiStore, workflowStore } from "builderStore"
|
||||
import { store, backendUiStore, automationStore } from "builderStore"
|
||||
import { notifier } from "builderStore/store/notifications"
|
||||
import ActionButton from "components/common/ActionButton.svelte"
|
||||
|
||||
|
@ -10,32 +10,32 @@
|
|||
$: valid = !!name
|
||||
$: instanceId = $backendUiStore.selectedDatabase._id
|
||||
|
||||
async function deleteWorkflow() {
|
||||
await workflowStore.actions.delete({
|
||||
async function deleteAutomation() {
|
||||
await automationStore.actions.delete({
|
||||
instanceId,
|
||||
workflow: $workflowStore.selectedWorkflow.workflow,
|
||||
automation: $automationStore.selectedAutomation.automation,
|
||||
})
|
||||
onClosed()
|
||||
notifier.danger("Workflow deleted.")
|
||||
notifier.danger("Automation deleted.")
|
||||
}
|
||||
</script>
|
||||
|
||||
<header>
|
||||
<i class="ri-stackshare-line" />
|
||||
Delete Workflow
|
||||
Delete Automation
|
||||
</header>
|
||||
<div>
|
||||
<p>
|
||||
Are you sure you want to delete this workflow? This action can't be undone.
|
||||
Are you sure you want to delete this automation? This action can't be undone.
|
||||
</p>
|
||||
</div>
|
||||
<footer>
|
||||
<a href="https://docs.budibase.com">
|
||||
<i class="ri-information-line" />
|
||||
Learn about workflows
|
||||
Learn about automations
|
||||
</a>
|
||||
<ActionButton on:click={onClosed}>Cancel</ActionButton>
|
||||
<ActionButton alert on:click={deleteWorkflow}>Delete</ActionButton>
|
||||
<ActionButton alert on:click={deleteAutomation}>Delete</ActionButton>
|
||||
</footer>
|
||||
|
||||
<style>
|
|
@ -38,7 +38,7 @@
|
|||
<option value={option}>{option}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
{:else if schema.type === "string"}
|
||||
{:else if schema.type === "string" || schema.type === "number"}
|
||||
<BindableInput
|
||||
thin
|
||||
bind:value={value[field]}
|
|
@ -1,49 +1,49 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
import { backendUiStore, workflowStore } from "builderStore"
|
||||
import { backendUiStore, automationStore } from "builderStore"
|
||||
import { notifier } from "builderStore/store/notifications"
|
||||
import WorkflowBlockSetup from "./WorkflowBlockSetup.svelte"
|
||||
import DeleteWorkflowModal from "./DeleteWorkflowModal.svelte"
|
||||
import AutomationBlockSetup from "./AutomationBlockSetup.svelte"
|
||||
import DeleteAutomationModal from "./DeleteAutomationModal.svelte"
|
||||
import { Button, Input, Label } from "@budibase/bbui"
|
||||
|
||||
const { open, close } = getContext("simple-modal")
|
||||
|
||||
let selectedTab = "SETUP"
|
||||
|
||||
$: workflow = $workflowStore.selectedWorkflow?.workflow
|
||||
$: automation = $automationStore.selectedAutomation?.automation
|
||||
$: allowDeleteBlock =
|
||||
$workflowStore.selectedBlock?.type !== "TRIGGER" ||
|
||||
!workflow?.definition?.steps?.length
|
||||
$automationStore.selectedBlock?.type !== "TRIGGER" ||
|
||||
!automation?.definition?.steps?.length
|
||||
|
||||
function deleteWorkflow() {
|
||||
function deleteAutomation() {
|
||||
open(
|
||||
DeleteWorkflowModal,
|
||||
DeleteAutomationModal,
|
||||
{ onClosed: close },
|
||||
{ styleContent: { padding: "0" } }
|
||||
)
|
||||
}
|
||||
|
||||
function deleteWorkflowBlock() {
|
||||
workflowStore.actions.deleteWorkflowBlock($workflowStore.selectedBlock)
|
||||
function deleteAutomationBlock() {
|
||||
automationStore.actions.deleteAutomationBlock($automationStore.selectedBlock)
|
||||
}
|
||||
|
||||
async function testWorkflow() {
|
||||
const result = await workflowStore.actions.trigger({
|
||||
workflow: $workflowStore.selectedWorkflow.workflow,
|
||||
async function testAutomation() {
|
||||
const result = await automationStore.actions.trigger({
|
||||
automation: $automationStore.selectedAutomation.automation,
|
||||
})
|
||||
if (result.status === 200) {
|
||||
notifier.success(`Workflow ${workflow.name} triggered successfully.`)
|
||||
notifier.success(`Automation ${automation.name} triggered successfully.`)
|
||||
} else {
|
||||
notifier.danger(`Failed to trigger workflow ${workflow.name}.`)
|
||||
notifier.danger(`Failed to trigger automation ${automation.name}.`)
|
||||
}
|
||||
}
|
||||
|
||||
async function saveWorkflow() {
|
||||
await workflowStore.actions.save({
|
||||
async function saveAutomation() {
|
||||
await automationStore.actions.save({
|
||||
instanceId: $backendUiStore.selectedDatabase._id,
|
||||
workflow,
|
||||
automation,
|
||||
})
|
||||
notifier.success(`Workflow ${workflow.name} saved.`)
|
||||
notifier.success(`Automation ${automation.name} saved.`)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -56,38 +56,38 @@
|
|||
Setup
|
||||
</span>
|
||||
</header>
|
||||
{#if $workflowStore.selectedBlock}
|
||||
<WorkflowBlockSetup bind:block={$workflowStore.selectedBlock} />
|
||||
{#if $automationStore.selectedBlock}
|
||||
<AutomationBlockSetup bind:block={$automationStore.selectedBlock} />
|
||||
<div class="buttons">
|
||||
<Button green wide data-cy="save-workflow-setup" on:click={saveWorkflow}>
|
||||
Save Workflow
|
||||
<Button green wide data-cy="save-automation-setup" on:click={saveAutomation}>
|
||||
Save Automation
|
||||
</Button>
|
||||
<Button
|
||||
disabled={!allowDeleteBlock}
|
||||
red
|
||||
wide
|
||||
on:click={deleteWorkflowBlock}>
|
||||
on:click={deleteAutomationBlock}>
|
||||
Delete Block
|
||||
</Button>
|
||||
</div>
|
||||
{:else if $workflowStore.selectedWorkflow}
|
||||
{:else if $automationStore.selectedAutomation}
|
||||
<div class="panel">
|
||||
<div class="panel-body">
|
||||
<div class="block-label">
|
||||
Workflow
|
||||
<b>{workflow.name}</b>
|
||||
Automation
|
||||
<b>{automation.name}</b>
|
||||
</div>
|
||||
</div>
|
||||
<Button secondary wide on:click={testWorkflow}>Test Workflow</Button>
|
||||
<Button secondary wide on:click={testAutomation}>Test Automation</Button>
|
||||
<div class="buttons">
|
||||
<Button
|
||||
green
|
||||
wide
|
||||
data-cy="save-workflow-setup"
|
||||
on:click={saveWorkflow}>
|
||||
Save Workflow
|
||||
data-cy="save-automation-setup"
|
||||
on:click={saveAutomation}>
|
||||
Save Automation
|
||||
</Button>
|
||||
<Button red wide on:click={deleteWorkflow}>Delete Workflow</Button>
|
||||
<Button red wide on:click={deleteAutomation}>Delete Automation</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
|
@ -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"
|
|
@ -0,0 +1,299 @@
|
|||
<script>
|
||||
import { notifier } from "builderStore/store/notifications"
|
||||
import { Heading, Body, Button } from "@budibase/bbui"
|
||||
import { FILE_TYPES } from "constants/backend"
|
||||
import api from "builderStore/api"
|
||||
|
||||
const BYTES_IN_KB = 1000
|
||||
const BYTES_IN_MB = 1000000
|
||||
|
||||
export let files = []
|
||||
export let fileSizeLimit = BYTES_IN_MB * 20
|
||||
|
||||
let selectedImageIdx = 0
|
||||
let fileDragged = false
|
||||
|
||||
$: selectedImage = files[selectedImageIdx]
|
||||
|
||||
function determineFileIcon(extension) {
|
||||
const ext = extension.toLowerCase()
|
||||
|
||||
if (FILE_TYPES.IMAGE.includes(ext)) return "ri-image-2-line"
|
||||
if (FILE_TYPES.CODE.includes(ext)) return "ri-terminal-box-line"
|
||||
|
||||
return "ri-file-line"
|
||||
}
|
||||
|
||||
async function processFiles(fileList) {
|
||||
const fileArray = Array.from(fileList)
|
||||
|
||||
if (fileArray.some(file => file.size >= fileSizeLimit)) {
|
||||
notifier.danger(
|
||||
`Files cannot exceed ${fileSizeLimit /
|
||||
BYTES_IN_MB}MB. Please try again with smaller files.`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const filesToProcess = fileArray.map(({ name, path, size }) => ({
|
||||
name,
|
||||
path,
|
||||
size,
|
||||
}))
|
||||
|
||||
const response = await api.post(`/api/attachments/process`, {
|
||||
files: filesToProcess,
|
||||
})
|
||||
const processedFiles = await response.json()
|
||||
files = [...processedFiles, ...files]
|
||||
selectedImageIdx = 0
|
||||
}
|
||||
|
||||
async function removeFile() {
|
||||
files.splice(selectedImageIdx, 1)
|
||||
files = files
|
||||
selectedImageIdx = 0
|
||||
}
|
||||
|
||||
function navigateLeft() {
|
||||
selectedImageIdx -= 1
|
||||
}
|
||||
|
||||
function navigateRight() {
|
||||
selectedImageIdx += 1
|
||||
}
|
||||
|
||||
function handleFile(evt) {
|
||||
processFiles(evt.target.files)
|
||||
}
|
||||
|
||||
function handleDragOver(evt) {
|
||||
evt.preventDefault()
|
||||
fileDragged = true
|
||||
}
|
||||
|
||||
function handleDragLeave(evt) {
|
||||
evt.preventDefault()
|
||||
fileDragged = false
|
||||
}
|
||||
|
||||
function handleDrop(evt) {
|
||||
evt.preventDefault()
|
||||
processFiles(evt.dataTransfer.files)
|
||||
fileDragged = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="dropzone"
|
||||
on:dragover={handleDragOver}
|
||||
on:dragleave={handleDragLeave}
|
||||
on:dragenter={handleDragOver}
|
||||
on:drop={handleDrop}
|
||||
class:fileDragged>
|
||||
<ul>
|
||||
{#if selectedImage}
|
||||
<li>
|
||||
<header>
|
||||
<div>
|
||||
<i
|
||||
class={`file-icon ${determineFileIcon(selectedImage.extension)}`} />
|
||||
<span class="filename">{selectedImage.name}</span>
|
||||
</div>
|
||||
<p>
|
||||
{#if selectedImage.size <= BYTES_IN_MB}
|
||||
{selectedImage.size / BYTES_IN_KB}KB
|
||||
{:else}{selectedImage.size / BYTES_IN_MB}MB{/if}
|
||||
</p>
|
||||
</header>
|
||||
<div class="delete-button" on:click={removeFile}>
|
||||
<i class="ri-close-line" />
|
||||
</div>
|
||||
{#if selectedImageIdx !== 0}
|
||||
<div class="nav left" on:click={navigateLeft}>
|
||||
<i class="ri-arrow-left-line" />
|
||||
</div>
|
||||
{/if}
|
||||
<img src={selectedImage.url} />
|
||||
{#if selectedImageIdx !== files.length - 1}
|
||||
<div class="nav right" on:click={navigateRight}>
|
||||
<i class="ri-arrow-right-line" />
|
||||
</div>
|
||||
{/if}
|
||||
</li>
|
||||
{/if}
|
||||
</ul>
|
||||
<i class="ri-folder-upload-line" />
|
||||
<input id="file-upload" type="file" multiple on:change={handleFile} />
|
||||
<label for="file-upload">Upload</label>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.dropzone {
|
||||
padding: var(--spacing-l);
|
||||
border: 2px dashed var(--grey-7);
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
border-radius: 10px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.fileDragged {
|
||||
border: 2px dashed var(--grey-7);
|
||||
transform: scale(1.03);
|
||||
background: var(--blue-light);
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
label {
|
||||
font-family: var(--font-sans);
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
border-radius: var(--border-radius-s);
|
||||
color: var(--white);
|
||||
padding: var(--spacing-s) var(--spacing-l);
|
||||
transition: all 0.2s ease 0s;
|
||||
display: inline-flex;
|
||||
text-rendering: optimizeLegibility;
|
||||
min-width: auto;
|
||||
outline: none;
|
||||
font-feature-settings: "case" 1, "rlig" 1, "calt" 0;
|
||||
-webkit-box-align: center;
|
||||
user-select: none;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 10px;
|
||||
width: 100%;
|
||||
border: solid 1.5px var(--ink);
|
||||
background-color: var(--ink);
|
||||
}
|
||||
|
||||
div.nav {
|
||||
position: absolute;
|
||||
background: black;
|
||||
color: var(--white);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
bottom: var(--spacing-s);
|
||||
border-radius: 10px;
|
||||
transition: 0.2s transform;
|
||||
}
|
||||
|
||||
.nav:hover {
|
||||
cursor: pointer;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.left {
|
||||
left: var(--spacing-s);
|
||||
}
|
||||
|
||||
.right {
|
||||
right: var(--spacing-s);
|
||||
}
|
||||
|
||||
li {
|
||||
position: relative;
|
||||
height: 300px;
|
||||
background: var(--grey-7);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
img {
|
||||
border-radius: 10px;
|
||||
width: 100%;
|
||||
box-shadow: 0 var(--spacing-s) 12px rgba(0, 0, 0, 0.15);
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 3em;
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
color: var(--white);
|
||||
font-size: 2em;
|
||||
margin-right: var(--spacing-s);
|
||||
}
|
||||
|
||||
ul {
|
||||
padding: 0;
|
||||
display: grid;
|
||||
grid-gap: var(--spacing-s);
|
||||
list-style-type: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
position: absolute;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(12, 12, 12, 1),
|
||||
rgba(60, 60, 60, 0)
|
||||
);
|
||||
width: 100%;
|
||||
border-top-left-radius: 10px;
|
||||
border-top-right-radius: 10px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
header > div {
|
||||
color: var(--white);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 15px;
|
||||
margin-left: var(--spacing-m);
|
||||
width: 60%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.filename {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
header > p {
|
||||
color: var(--grey-5);
|
||||
margin-right: var(--spacing-m);
|
||||
}
|
||||
|
||||
.delete-button {
|
||||
position: absolute;
|
||||
top: var(--spacing-s);
|
||||
right: var(--spacing-s);
|
||||
padding: var(--spacing-s);
|
||||
border-radius: 10px;
|
||||
opacity: 0;
|
||||
transition: all 0.3s;
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.delete-button i {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.delete-button:hover {
|
||||
opacity: 1;
|
||||
cursor: pointer;
|
||||
background: linear-gradient(
|
||||
to top right,
|
||||
rgba(60, 60, 60, 0),
|
||||
rgba(255, 0, 0, 0.2)
|
||||
);
|
||||
}
|
||||
</style>
|
|
@ -1,5 +1,7 @@
|
|||
<script>
|
||||
import { Circle } from "svelte-loading-spinners"
|
||||
|
||||
export let size = "60"
|
||||
</script>
|
||||
|
||||
<Circle size="60" color="#000000" unit="px" />
|
||||
<Circle {size} color="#000000" unit="px" />
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
<script>
|
||||
import { FILE_TYPES } from "constants/backend"
|
||||
|
||||
export let files
|
||||
export let height = "70"
|
||||
export let width = "70"
|
||||
</script>
|
||||
|
||||
<div class="file-list">
|
||||
{#each files as file}
|
||||
<div class="file">
|
||||
{#if FILE_TYPES.IMAGE.includes(file.extension.toLowerCase())}
|
||||
<img {width} {height} src={file.url} />
|
||||
{:else}
|
||||
<i class="ri-file-line" />
|
||||
<span class="extension">.{file.extension}</span>
|
||||
<span>{file.name}</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.file-list {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-gap: var(--spacing-m);
|
||||
grid-template-columns: repeat(10, 1fr);
|
||||
}
|
||||
|
||||
img {
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 36px;
|
||||
margin-bottom: var(--spacing-m);
|
||||
}
|
||||
|
||||
.file {
|
||||
position: relative;
|
||||
height: 75px;
|
||||
width: 75px;
|
||||
border: 2px dashed var(--grey-7);
|
||||
padding: var(--spacing-m);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.extension {
|
||||
position: absolute;
|
||||
top: var(--spacing-s);
|
||||
left: var(--spacing-s);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
span {
|
||||
width: 75px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
</style>
|
|
@ -6,6 +6,7 @@
|
|||
import { Button, Icon } from "@budibase/bbui"
|
||||
import ActionButton from "components/common/ActionButton.svelte"
|
||||
import LinkedRecord from "./LinkedRecord.svelte"
|
||||
import AttachmentList from "./AttachmentList.svelte"
|
||||
import TablePagination from "./TablePagination.svelte"
|
||||
import { DeleteRecordModal, CreateEditRecordModal } from "./modals"
|
||||
import RowPopover from "./popovers/Row.svelte"
|
||||
|
@ -90,6 +91,8 @@
|
|||
<td>
|
||||
{#if schema[header].type === 'link'}
|
||||
<LinkedRecord field={schema[header]} ids={row[header]} />
|
||||
{:else if schema[header].type === 'attachment'}
|
||||
<AttachmentList files={row[header] || []} />
|
||||
{:else}{getOr('', header, row)}{/if}
|
||||
</td>
|
||||
{/each}
|
||||
|
@ -108,6 +111,7 @@
|
|||
section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
import api from "builderStore/api"
|
||||
import { Button, Icon } from "@budibase/bbui"
|
||||
import ActionButton from "components/common/ActionButton.svelte"
|
||||
import LinkedRecord from "./LinkedRecord.svelte"
|
||||
import AttachmentList from "./AttachmentList.svelte"
|
||||
import TablePagination from "./TablePagination.svelte"
|
||||
import { DeleteRecordModal, CreateEditRecordModal } from "./modals"
|
||||
import RowPopover from "./popovers/Row.svelte"
|
||||
|
@ -59,7 +59,11 @@
|
|||
{#each paginatedData as row}
|
||||
<tr>
|
||||
{#each columns as header}
|
||||
<td>{getOr('', header, row)}</td>
|
||||
<td>
|
||||
{#if schema[header].type === 'attachment'}
|
||||
<AttachmentList files={row[header] || []} />
|
||||
{:else}{getOr('', header, row)}{/if}
|
||||
</td>
|
||||
{/each}
|
||||
</tr>
|
||||
{/each}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { onMount } from "svelte"
|
||||
import { onMount, tick } from "svelte"
|
||||
import { store, backendUiStore } from "builderStore"
|
||||
import { notifier } from "builderStore/store/notifications"
|
||||
import { compose, map, get, flatten } from "lodash/fp"
|
||||
|
@ -34,12 +34,9 @@
|
|||
return
|
||||
}
|
||||
|
||||
backendUiStore.update(state => {
|
||||
state.selectedView = state.selectedView
|
||||
onClosed()
|
||||
notifier.success("Record created successfully.")
|
||||
return state
|
||||
})
|
||||
onClosed()
|
||||
notifier.success("Record saved successfully.")
|
||||
backendUiStore.actions.records.save(recordResponse)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
<script>
|
||||
import { Input, Select } from "@budibase/bbui"
|
||||
import DatePicker from "components/common/DatePicker.svelte"
|
||||
import { Input, Select, Label, DatePicker } from "@budibase/bbui"
|
||||
import Dropzone from "components/common/Dropzone.svelte"
|
||||
|
||||
export let meta
|
||||
export let value = meta.type === "boolean" ? false : ""
|
||||
export let originalValue
|
||||
|
||||
let isSelect =
|
||||
meta.type === "string" &&
|
||||
|
@ -17,6 +18,7 @@
|
|||
if (meta.type === "datetime") return "date"
|
||||
if (meta.type === "number") return "number"
|
||||
if (meta.type === "boolean") return "checkbox"
|
||||
if (meta.type === "attachment") return "file"
|
||||
if (isSelect) return "select"
|
||||
|
||||
return "text"
|
||||
|
@ -45,7 +47,11 @@
|
|||
{/each}
|
||||
</Select>
|
||||
{:else if type === 'date'}
|
||||
<DatePicker label={meta.name} bind:value />
|
||||
<Label small forAttr={'datepicker-label'}>{meta.name}</Label>
|
||||
<DatePicker bind:value />
|
||||
{:else if type === 'file'}
|
||||
<Label small forAttr={'dropzone-label'}>{meta.name}</Label>
|
||||
<Dropzone bind:files={value} />
|
||||
{:else}
|
||||
{#if type === 'checkbox'}
|
||||
<label>{meta.name}</label>
|
||||
|
@ -64,7 +70,6 @@
|
|||
label {
|
||||
font-weight: 500;
|
||||
font-size: var(--font-size-s);
|
||||
float: left;
|
||||
margin-right: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,7 +1,14 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
import { backendUiStore } from "builderStore"
|
||||
import { DropdownMenu, Button, Icon, Input, Select } from "@budibase/bbui"
|
||||
import {
|
||||
DropdownMenu,
|
||||
Button,
|
||||
Icon,
|
||||
Input,
|
||||
Select,
|
||||
Heading,
|
||||
} from "@budibase/bbui"
|
||||
import { FIELDS } from "constants/backend"
|
||||
import CreateEditRecord from "../modals/CreateEditRecord.svelte"
|
||||
import DeleteRecordModal from "../modals/DeleteRecord.svelte"
|
||||
|
@ -48,11 +55,11 @@
|
|||
<ul>
|
||||
<li data-cy="edit-row" on:click={showEditor}>
|
||||
<Icon name="edit" />
|
||||
Edit
|
||||
<span>Edit</span>
|
||||
</li>
|
||||
<li data-cy="delete-row" on:click={deleteRow}>
|
||||
<Icon name="delete" />
|
||||
Delete
|
||||
<span>Delete</span>
|
||||
</li>
|
||||
</ul>
|
||||
{/if}
|
||||
|
@ -79,7 +86,6 @@
|
|||
li {
|
||||
display: flex;
|
||||
font-family: var(--font-sans);
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--ink);
|
||||
padding: var(--spacing-s) var(--spacing-m);
|
||||
margin: auto 0px;
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -35,8 +35,6 @@
|
|||
c => c._component === componentInstance._component
|
||||
) || {}
|
||||
|
||||
let panelDefinition = {}
|
||||
|
||||
$: panelDefinition =
|
||||
componentPropDefinition.properties &&
|
||||
componentPropDefinition.properties[selectedCategory.value]
|
||||
|
|
|
@ -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,293 @@
|
|||
<script>
|
||||
import { DropdownMenu, Button, Input } from "@budibase/bbui"
|
||||
import { createEventDispatcher, tick } from "svelte"
|
||||
|
||||
import icons from "./icons.js"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let value = ""
|
||||
export let maxIconsPerPage = 30
|
||||
|
||||
let searchTerm = ""
|
||||
let selectedLetter = "A"
|
||||
|
||||
let currentPage = 1
|
||||
let filteredIcons = findIconByTerm(selectedLetter)
|
||||
|
||||
$: dispatch("change", value)
|
||||
|
||||
const alphabet = [
|
||||
"A",
|
||||
"B",
|
||||
"C",
|
||||
"D",
|
||||
"E",
|
||||
"F",
|
||||
"G",
|
||||
"H",
|
||||
"I",
|
||||
"J",
|
||||
"K",
|
||||
"L",
|
||||
"M",
|
||||
"N",
|
||||
"O",
|
||||
"P",
|
||||
"Q",
|
||||
"R",
|
||||
"S",
|
||||
"T",
|
||||
"U",
|
||||
"V",
|
||||
"W",
|
||||
"X",
|
||||
"Y",
|
||||
"Z",
|
||||
]
|
||||
let buttonAnchor, dropdown
|
||||
let loading = false
|
||||
|
||||
function findIconByTerm(term) {
|
||||
const r = new RegExp(`\^${term}`, "i")
|
||||
return icons.filter(i => r.test(i.label))
|
||||
}
|
||||
|
||||
async function switchLetter(letter) {
|
||||
currentPage = 1
|
||||
searchTerm = ""
|
||||
loading = true
|
||||
selectedLetter = letter
|
||||
filteredIcons = findIconByTerm(letter)
|
||||
await tick() //svg icons do not update without tick
|
||||
loading = false
|
||||
}
|
||||
|
||||
async function findIconOnPage() {
|
||||
loading = true
|
||||
const iconIdx = filteredIcons.findIndex(i => i.value === value)
|
||||
if (iconIdx !== -1) {
|
||||
currentPage = Math.ceil(iconIdx / maxIconsPerPage)
|
||||
}
|
||||
await tick() //svg icons do not update without tick
|
||||
loading = false
|
||||
}
|
||||
|
||||
async function setSelectedUI() {
|
||||
if (value) {
|
||||
const letter = displayValue.substring(0, 1)
|
||||
await switchLetter(letter)
|
||||
await findIconOnPage()
|
||||
}
|
||||
}
|
||||
|
||||
async function pageClick(next) {
|
||||
loading = true
|
||||
if (next && currentPage < totalPages) {
|
||||
currentPage++
|
||||
} else if (!next && currentPage > 1) {
|
||||
currentPage--
|
||||
}
|
||||
await tick() //svg icons do not update without tick
|
||||
loading = false
|
||||
}
|
||||
|
||||
async function searchForIcon(e) {
|
||||
currentPage = 1
|
||||
loading = true
|
||||
filteredIcons = findIconByTerm(searchTerm)
|
||||
await tick() //svg icons do not update without tick
|
||||
loading = false
|
||||
}
|
||||
|
||||
$: displayValue = value ? value.substring(7) : "Pick Icon"
|
||||
|
||||
$: totalPages = Math.ceil(filteredIcons.length / maxIconsPerPage)
|
||||
$: pageEndIdx = maxIconsPerPage * currentPage
|
||||
$: pagedIcons = filteredIcons.slice(pageEndIdx - maxIconsPerPage, pageEndIdx)
|
||||
|
||||
$: pagerText = `Page ${currentPage} of ${totalPages}`
|
||||
</script>
|
||||
|
||||
<div bind:this={buttonAnchor}>
|
||||
<Button secondary on:click={dropdown.show}>{displayValue}</Button>
|
||||
</div>
|
||||
<DropdownMenu
|
||||
bind:this={dropdown}
|
||||
on:open={setSelectedUI}
|
||||
anchor={buttonAnchor}>
|
||||
<div class="container">
|
||||
<div class="search-area">
|
||||
<div class="alphabet-area">
|
||||
{#each alphabet as letter, idx}
|
||||
<span
|
||||
class="letter"
|
||||
class:letter-selected={letter === selectedLetter}
|
||||
on:click={() => switchLetter(letter)}>
|
||||
{letter}
|
||||
</span>
|
||||
{#if idx !== alphabet.length - 1}
|
||||
<span>-</span>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
<div class="search-input">
|
||||
<div class="input-wrapper">
|
||||
<Input bind:value={searchTerm} thin placeholder="Search Icon" />
|
||||
</div>
|
||||
<Button secondary on:click={searchForIcon}>Search</Button>
|
||||
</div>
|
||||
<div class="page-area">
|
||||
<div class="pager">
|
||||
<span on:click={() => pageClick(false)}>
|
||||
<i class="page-btn fas fa-chevron-left" />
|
||||
</span>
|
||||
<span>{pagerText}</span>
|
||||
<span on:click={() => pageClick(true)}>
|
||||
<i class="page-btn fas fa-chevron-right" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{#if pagedIcons.length > 0}
|
||||
<div class="icon-area">
|
||||
{#if !loading}
|
||||
{#each pagedIcons as icon}
|
||||
<div
|
||||
class="icon-container"
|
||||
class:selected={value === icon.value}
|
||||
on:click={() => (value = icon.value)}>
|
||||
<div class="icon-preview">
|
||||
<i class={`${icon.value} fa-3x`} />
|
||||
</div>
|
||||
<div class="icon-label">{icon.label}</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="no-icons">
|
||||
<h5>
|
||||
{`There is no icons for this ${searchTerm ? 'search' : 'page'}`}
|
||||
</h5>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</DropdownMenu>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
width: 610px;
|
||||
height: 350px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 10px 0px 10px 15px;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.search-area {
|
||||
flex: 0 0 80px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.icon-area {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
grid-gap: 5px;
|
||||
justify-content: flex-start;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.no-icons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.alphabet-area {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
padding-bottom: 10px;
|
||||
padding-right: 15px;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
width: 100%;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
width: 510px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.page-area {
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.letter {
|
||||
color: var(--blue);
|
||||
}
|
||||
|
||||
.letter:hover {
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.letter-selected {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.icon-container {
|
||||
height: 100px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
border: var(--border-dark);
|
||||
}
|
||||
|
||||
.icon-container:hover {
|
||||
cursor: pointer;
|
||||
background: var(--grey-2);
|
||||
}
|
||||
|
||||
.selected {
|
||||
background: var(--grey-3);
|
||||
}
|
||||
|
||||
.icon-preview {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.icon-label {
|
||||
flex: 0 0 20px;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.page-btn {
|
||||
color: var(--blue);
|
||||
}
|
||||
|
||||
.page-btn:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,3 @@
|
|||
import "@fortawesome/fontawesome-free/js/all.js"
|
||||
|
||||
export { default as IconSelect } from "./IconSelect.svelte"
|
|
@ -71,7 +71,7 @@
|
|||
|
||||
let temp = runtimeToReadableBinding(bindableProperties, value)
|
||||
|
||||
return value === undefined && props.defaultValue !== undefined
|
||||
return !value && props.defaultValue !== undefined
|
||||
? props.defaultValue
|
||||
: temp
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import ModelViewSelect from "components/userInterface/ModelViewSelect.svelte"
|
|||
import ModelViewFieldSelect from "components/userInterface/ModelViewFieldSelect.svelte"
|
||||
import Event from "components/userInterface/EventsEditor/EventPropertyControl.svelte"
|
||||
import ScreenSelect from "components/userInterface/ScreenSelect.svelte"
|
||||
import { IconSelect } from "components/userInterface/IconSelect"
|
||||
import Colorpicker from "@budibase/colorpicker"
|
||||
|
||||
import { all } from "./propertyCategories.js"
|
||||
|
@ -221,16 +222,41 @@ export default {
|
|||
settings: [{ label: "URL", key: "url", control: Input }],
|
||||
},
|
||||
},
|
||||
// {
|
||||
// _component: "@budibase/standard-components/icon",
|
||||
// name: "Icon",
|
||||
// description: "A basic component for displaying icons",
|
||||
// icon: "ri-sun-fill",
|
||||
// children: [],
|
||||
// properties: {
|
||||
// design: { ...all },
|
||||
// },
|
||||
// },
|
||||
{
|
||||
_component: "@budibase/standard-components/icon",
|
||||
name: "Icon",
|
||||
description: "A basic component for displaying icons",
|
||||
icon: "ri-sun-fill",
|
||||
children: [],
|
||||
properties: {
|
||||
design: {},
|
||||
settings: [
|
||||
{ label: "Icon", key: "icon", control: IconSelect },
|
||||
{
|
||||
label: "Size",
|
||||
key: "size",
|
||||
control: OptionSelect,
|
||||
defaultValue: "fa-lg",
|
||||
options: [
|
||||
{ value: "fa-xs", label: "xs" },
|
||||
{ value: "fa-sm", label: "sm" },
|
||||
{ value: "fa-lg", label: "lg" },
|
||||
{ value: "fa-2x", label: "2x" },
|
||||
{ value: "fa-3x", label: "3x" },
|
||||
{ value: "fa-5x", label: "5x" },
|
||||
{ value: "fa-7x", label: "7x" },
|
||||
{ value: "fa-10x", label: "10x" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Color",
|
||||
key: "color",
|
||||
control: Colorpicker,
|
||||
defaultValue: "#000",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
_component: "@budibase/standard-components/link",
|
||||
name: "Link",
|
||||
|
|
|
@ -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"
|
|
@ -51,24 +51,15 @@ export const FIELDS = {
|
|||
},
|
||||
},
|
||||
},
|
||||
// IMAGE: {
|
||||
// name: "File",
|
||||
// icon: "ri-image-line",
|
||||
// type: "file",
|
||||
// constraints: {
|
||||
// type: "string",
|
||||
// presence: { allowEmpty: true },
|
||||
// },
|
||||
// },
|
||||
// FILE: {
|
||||
// name: "Image",
|
||||
// icon: "ri-file-line",
|
||||
// type: "file",
|
||||
// constraints: {
|
||||
// type: "string",
|
||||
// presence: { allowEmpty: true },
|
||||
// },
|
||||
// },
|
||||
ATTACHMENT: {
|
||||
name: "Attachment",
|
||||
icon: "ri-file-line",
|
||||
type: "attachment",
|
||||
constraints: {
|
||||
type: "array",
|
||||
presence: { allowEmpty: true },
|
||||
},
|
||||
},
|
||||
// LINKED_FIELDS: {
|
||||
// name: "Linked Fields",
|
||||
// icon: "ri-link",
|
||||
|
@ -79,3 +70,9 @@ export const FIELDS = {
|
|||
// },
|
||||
// },
|
||||
}
|
||||
|
||||
export const FILE_TYPES = {
|
||||
IMAGE: ["png", "tiff", "gif", "raw", "jpg", "jpeg"],
|
||||
CODE: ["js", "rs", "py", "java", "rb", "hs", "yml"],
|
||||
DOCUMENT: ["odf", "docx", "doc", "pdf", "csv"],
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
<!-- routify:options index=3 -->
|
||||
<script>
|
||||
import { workflowStore } from "builderStore"
|
||||
import { WorkflowPanel, SetupPanel } from "components/workflow"
|
||||
import { automationStore } from "builderStore"
|
||||
import { AutomationPanel, SetupPanel } from "components/automation"
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
<div class="nav">
|
||||
<div class="inner">
|
||||
<WorkflowPanel />
|
||||
<AutomationPanel />
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<slot />
|
||||
</div>
|
||||
{#if $workflowStore.selectedWorkflow}
|
||||
{#if $automationStore.selectedAutomation}
|
||||
<div class="nav">
|
||||
<div class="inner">
|
||||
<SetupPanel />
|
|
@ -0,0 +1,5 @@
|
|||
<script>
|
||||
import { AutomationBuilder } from "components/automation"
|
||||
</script>
|
||||
|
||||
<AutomationBuilder />
|
|
@ -1,3 +1,4 @@
|
|||
<!-- routify:options index=1 -->
|
||||
<script>
|
||||
import { getContext } from "svelte"
|
||||
import { store, backendUiStore } from "builderStore"
|
||||
|
|
|
@ -48,12 +48,14 @@
|
|||
<Button secondary medium on:click={deployApp}>
|
||||
Deploy App
|
||||
{#if loading}
|
||||
<Spinner ratio={'0.5'} />
|
||||
<Spinner size="10" />
|
||||
{/if}
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
<img src="/_builder/assets/deploy-rocket.jpg" />
|
||||
<img
|
||||
src="/_builder/assets/deploy-rocket.jpg"
|
||||
alt="Rocket flying through sky" />
|
||||
</section>
|
||||
|
||||
<style>
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
<!-- routify:options index=1 -->
|
||||
<script>
|
||||
import { store, backendUiStore } from "builderStore"
|
||||
import { goto } from "@sveltech/routify"
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
<script>
|
||||
import { WorkflowBuilder } from "components/workflow"
|
||||
</script>
|
||||
|
||||
<WorkflowBuilder />
|
|
@ -1,32 +0,0 @@
|
|||
import { buildStateOrigins } from "../src/builderStore/buildStateOrigins"
|
||||
|
||||
it("builds the correct stateOrigins object from a screen definition with handlers", () => {
|
||||
expect(
|
||||
buildStateOrigins({
|
||||
name: "screen1",
|
||||
description: "",
|
||||
props: {
|
||||
_component: "@budibase/standard-components/container",
|
||||
className: "",
|
||||
type: "div",
|
||||
onClick: [
|
||||
{
|
||||
"##eventHandlerType": "Set State",
|
||||
parameters: {
|
||||
path: "testKey",
|
||||
value: "value",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
).toEqual({
|
||||
testKey: {
|
||||
"##eventHandlerType": "Set State",
|
||||
parameters: {
|
||||
path: "testKey",
|
||||
value: "value",
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
|
@ -709,14 +709,23 @@
|
|||
lodash "^4.17.13"
|
||||
to-fast-properties "^2.0.0"
|
||||
|
||||
"@budibase/bbui@^1.33.0":
|
||||
version "1.33.0"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.33.0.tgz#216b24dd815f45880e9795e66b04848329b0390f"
|
||||
integrity sha512-Rrt5eLbea014TIfAbT40kP0D0AWNUi8Q0kDr3UZO6Aq4UXgjc0f53ZuJ7Kb66YRDWrqiucjf1FtvOUs3/YaD6g==
|
||||
"@budibase/bbui@^1.34.2":
|
||||
version "1.34.2"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.34.2.tgz#e4fcc728dc8d51a918f8ebd5c3f0b0afacfa4047"
|
||||
integrity sha512-6RusGPZnEpHx1gtGcjk/lFLgMgFdDpSIxB8v2MiA+kp+uP1pFlzegbaDh+/JXyqFwK7HO91I0yXXBoPjibi7Aw==
|
||||
dependencies:
|
||||
sirv-cli "^0.4.6"
|
||||
svelte-flatpickr "^2.4.0"
|
||||
|
||||
"@budibase/client@^0.1.21":
|
||||
version "0.1.21"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/client/-/client-0.1.21.tgz#db414445c132b373f6c25e39d62628eb60cd8ac3"
|
||||
integrity sha512-/ju0vYbWh9MUjmxkGNlOL4S/VQd4p5mbz5rHu0yt55ak9t/yyzI6PzBBxlucBeRbXYd9OFynFjy1pvYt1v+z9Q==
|
||||
dependencies:
|
||||
deep-equal "^2.0.1"
|
||||
mustache "^4.0.1"
|
||||
regexparam "^1.3.0"
|
||||
|
||||
"@budibase/colorpicker@^1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/colorpicker/-/colorpicker-1.0.1.tgz#940c180e7ebba0cb0756c4c8ef13f5dfab58e810"
|
||||
|
@ -769,6 +778,11 @@
|
|||
debug "^3.1.0"
|
||||
lodash.once "^4.1.1"
|
||||
|
||||
"@fortawesome/fontawesome-free@^5.14.0":
|
||||
version "5.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.14.0.tgz#a371e91029ebf265015e64f81bfbf7d228c9681f"
|
||||
integrity sha512-OfdMsF+ZQgdKHP9jUbmDcRrP0eX90XXrsXIdyjLbkmSBzmMXPABB8eobUJtivaupucYaByz6WNe1PI1JuYm3qA==
|
||||
|
||||
"@hapi/address@^2.1.2":
|
||||
version "2.1.4"
|
||||
resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5"
|
||||
|
@ -1388,6 +1402,11 @@ array-equal@^1.0.0:
|
|||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93"
|
||||
|
||||
array-filter@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-1.0.0.tgz#baf79e62e6ef4c2a4c0b831232daffec251f9d83"
|
||||
integrity sha1-uveeYubvTCpMC4MSMtr/7CUfnYM=
|
||||
|
||||
array-union@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
|
||||
|
@ -1442,6 +1461,13 @@ atob@^2.1.2:
|
|||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
|
||||
|
||||
available-typed-arrays@^1.0.0, available-typed-arrays@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz#6b098ca9d8039079ee3f77f7b783c4480ba513f5"
|
||||
integrity sha512-XWX3OX8Onv97LMk/ftVyBibpGwY5a8SmuxZPzeOxqmuEqUCOM9ZE+uIaD1VNJ5QnvU2UQusvmKbuM1FR8QWGfQ==
|
||||
dependencies:
|
||||
array-filter "^1.0.0"
|
||||
|
||||
aws-sign2@~0.7.0:
|
||||
version "0.7.0"
|
||||
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
|
||||
|
@ -2391,6 +2417,26 @@ decode-uri-component@^0.2.0:
|
|||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
|
||||
|
||||
deep-equal@^2.0.1:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.0.3.tgz#cad1c15277ad78a5c01c49c2dee0f54de8a6a7b0"
|
||||
integrity sha512-Spqdl4H+ky45I9ByyJtXteOm9CaIrPmnIPmOhrkKGNYWeDgCvJ8jNYVCTjChxW4FqGuZnLHADc8EKRMX6+CgvA==
|
||||
dependencies:
|
||||
es-abstract "^1.17.5"
|
||||
es-get-iterator "^1.1.0"
|
||||
is-arguments "^1.0.4"
|
||||
is-date-object "^1.0.2"
|
||||
is-regex "^1.0.5"
|
||||
isarray "^2.0.5"
|
||||
object-is "^1.1.2"
|
||||
object-keys "^1.1.1"
|
||||
object.assign "^4.1.0"
|
||||
regexp.prototype.flags "^1.3.0"
|
||||
side-channel "^1.0.2"
|
||||
which-boxed-primitive "^1.0.1"
|
||||
which-collection "^1.0.1"
|
||||
which-typed-array "^1.1.2"
|
||||
|
||||
deep-is@~0.1.3:
|
||||
version "0.1.3"
|
||||
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
|
||||
|
@ -2560,6 +2606,54 @@ es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.5:
|
|||
string.prototype.trimleft "^2.1.1"
|
||||
string.prototype.trimright "^2.1.1"
|
||||
|
||||
es-abstract@^1.17.4:
|
||||
version "1.17.6"
|
||||
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.6.tgz#9142071707857b2cacc7b89ecb670316c3e2d52a"
|
||||
integrity sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==
|
||||
dependencies:
|
||||
es-to-primitive "^1.2.1"
|
||||
function-bind "^1.1.1"
|
||||
has "^1.0.3"
|
||||
has-symbols "^1.0.1"
|
||||
is-callable "^1.2.0"
|
||||
is-regex "^1.1.0"
|
||||
object-inspect "^1.7.0"
|
||||
object-keys "^1.1.1"
|
||||
object.assign "^4.1.0"
|
||||
string.prototype.trimend "^1.0.1"
|
||||
string.prototype.trimstart "^1.0.1"
|
||||
|
||||
es-abstract@^1.18.0-next.0:
|
||||
version "1.18.0-next.0"
|
||||
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.0.tgz#b302834927e624d8e5837ed48224291f2c66e6fc"
|
||||
integrity sha512-elZXTZXKn51hUBdJjSZGYRujuzilgXo8vSPQzjGYXLvSlGiCo8VO8ZGV3kjo9a0WNJJ57hENagwbtlRuHuzkcQ==
|
||||
dependencies:
|
||||
es-to-primitive "^1.2.1"
|
||||
function-bind "^1.1.1"
|
||||
has "^1.0.3"
|
||||
has-symbols "^1.0.1"
|
||||
is-callable "^1.2.0"
|
||||
is-negative-zero "^2.0.0"
|
||||
is-regex "^1.1.1"
|
||||
object-inspect "^1.8.0"
|
||||
object-keys "^1.1.1"
|
||||
object.assign "^4.1.0"
|
||||
string.prototype.trimend "^1.0.1"
|
||||
string.prototype.trimstart "^1.0.1"
|
||||
|
||||
es-get-iterator@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.0.tgz#bb98ad9d6d63b31aacdc8f89d5d0ee57bcb5b4c8"
|
||||
integrity sha512-UfrmHuWQlNMTs35e1ypnvikg6jCz3SK8v8ImvmDsh36fCVUR1MqoFDiyn0/k52C8NqO3YsO8Oe0azeesNuqSsQ==
|
||||
dependencies:
|
||||
es-abstract "^1.17.4"
|
||||
has-symbols "^1.0.1"
|
||||
is-arguments "^1.0.4"
|
||||
is-map "^2.0.1"
|
||||
is-set "^2.0.1"
|
||||
is-string "^1.0.5"
|
||||
isarray "^2.0.5"
|
||||
|
||||
es-to-primitive@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a"
|
||||
|
@ -2866,7 +2960,7 @@ for-in@^1.0.2:
|
|||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
|
||||
|
||||
foreach@~2.0.1:
|
||||
foreach@^2.0.5, foreach@~2.0.1:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99"
|
||||
integrity sha1-C+4AUBiusmDQo6865ljdATbsG5k=
|
||||
|
@ -3240,16 +3334,31 @@ is-accessor-descriptor@^1.0.0:
|
|||
dependencies:
|
||||
kind-of "^6.0.0"
|
||||
|
||||
is-arguments@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.0.4.tgz#3faf966c7cba0ff437fb31f6250082fcf0448cf3"
|
||||
integrity sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==
|
||||
|
||||
is-arrayish@^0.2.1:
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
|
||||
|
||||
is-bigint@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.0.tgz#73da8c33208d00f130e9b5e15d23eac9215601c4"
|
||||
integrity sha512-t5mGUXC/xRheCK431ylNiSkGGpBp8bHENBcENTkDT6ppwPzEVxNGZRvgvmOEfbWkFhA7D2GEuE2mmQTr78sl2g==
|
||||
|
||||
is-binary-path@~2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
|
||||
dependencies:
|
||||
binary-extensions "^2.0.0"
|
||||
|
||||
is-boolean-object@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.0.1.tgz#10edc0900dd127697a92f6f9807c7617d68ac48e"
|
||||
integrity sha512-TqZuVwa/sppcrhUCAYkGBk7w0yxfQQnxq28fjkO53tnK9FQXmdwz2JS5+GjsWQ6RByES1K40nI+yDic5c9/aAQ==
|
||||
|
||||
is-buffer@^1.1.5:
|
||||
version "1.1.6"
|
||||
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
|
||||
|
@ -3258,6 +3367,11 @@ is-callable@^1.1.4, is-callable@^1.1.5:
|
|||
version "1.1.5"
|
||||
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.5.tgz#f7e46b596890456db74e7f6e976cb3273d06faab"
|
||||
|
||||
is-callable@^1.2.0:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.1.tgz#4d1e21a4f437509d25ce55f8184350771421c96d"
|
||||
integrity sha512-wliAfSzx6V+6WfMOmus1xy0XvSgf/dlStkvTfq7F0g4bOIW0PSUbnyse3NhDwdyYS1ozfUtAAySqTws3z9Eqgg==
|
||||
|
||||
is-ci@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c"
|
||||
|
@ -3276,7 +3390,7 @@ is-data-descriptor@^1.0.0:
|
|||
dependencies:
|
||||
kind-of "^6.0.0"
|
||||
|
||||
is-date-object@^1.0.1:
|
||||
is-date-object@^1.0.1, is-date-object@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e"
|
||||
|
||||
|
@ -3341,10 +3455,25 @@ is-installed-globally@^0.3.2:
|
|||
global-dirs "^2.0.1"
|
||||
is-path-inside "^3.0.1"
|
||||
|
||||
is-map@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.1.tgz#520dafc4307bb8ebc33b813de5ce7c9400d644a1"
|
||||
integrity sha512-T/S49scO8plUiAOA2DBTBG3JHpn1yiw0kRp6dgiZ0v2/6twi5eiB0rHtHFH9ZIrvlWc6+4O+m4zg5+Z833aXgw==
|
||||
|
||||
is-module@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591"
|
||||
|
||||
is-negative-zero@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.0.tgz#9553b121b0fac28869da9ed459e20c7543788461"
|
||||
integrity sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE=
|
||||
|
||||
is-number-object@^1.0.3:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.4.tgz#36ac95e741cf18b283fc1ddf5e83da798e3ec197"
|
||||
integrity sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw==
|
||||
|
||||
is-number@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195"
|
||||
|
@ -3401,6 +3530,18 @@ is-regex@^1.0.5:
|
|||
dependencies:
|
||||
has "^1.0.3"
|
||||
|
||||
is-regex@^1.1.0, is-regex@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9"
|
||||
integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==
|
||||
dependencies:
|
||||
has-symbols "^1.0.1"
|
||||
|
||||
is-set@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.1.tgz#d1604afdab1724986d30091575f54945da7e5f43"
|
||||
integrity sha512-eJEzOtVyenDs1TMzSQ3kU3K+E0GUS9sno+F0OBT97xsgcJsF9nXMBtkT9/kut5JEpM7oL7X/0qxR17K3mcwIAA==
|
||||
|
||||
is-stream@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
|
||||
|
@ -3409,16 +3550,41 @@ is-stream@^2.0.0:
|
|||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3"
|
||||
|
||||
is-string@^1.0.4, is-string@^1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6"
|
||||
integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==
|
||||
|
||||
is-symbol@^1.0.2:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937"
|
||||
dependencies:
|
||||
has-symbols "^1.0.1"
|
||||
|
||||
is-typed-array@^1.1.3:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.3.tgz#a4ff5a5e672e1a55f99c7f54e59597af5c1df04d"
|
||||
integrity sha512-BSYUBOK/HJibQ30wWkWold5txYwMUXQct9YHAQJr8fSwvZoiglcqB0pd7vEN23+Tsi9IUEjztdOSzl4qLVYGTQ==
|
||||
dependencies:
|
||||
available-typed-arrays "^1.0.0"
|
||||
es-abstract "^1.17.4"
|
||||
foreach "^2.0.5"
|
||||
has-symbols "^1.0.1"
|
||||
|
||||
is-typedarray@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
|
||||
|
||||
is-weakmap@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2"
|
||||
integrity sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==
|
||||
|
||||
is-weakset@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.1.tgz#e9a0af88dbd751589f5e50d80f4c98b780884f83"
|
||||
integrity sha512-pi4vhbhVHGLxohUw7PhGsueT4vRGFoXhP7+RGN0jKIv9+8PWYCQTqtADngrxOm2g46hoH0+g8uZZBzMrvVGDmw==
|
||||
|
||||
is-windows@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
|
||||
|
@ -3439,6 +3605,11 @@ isarray@1.0.0, isarray@~1.0.0:
|
|||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
|
||||
|
||||
isarray@^2.0.5:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
|
||||
integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==
|
||||
|
||||
isbuffer@~0.0.0:
|
||||
version "0.0.0"
|
||||
resolved "https://registry.yarnpkg.com/isbuffer/-/isbuffer-0.0.0.tgz#38c146d9df528b8bf9b0701c3d43cf12df3fc39b"
|
||||
|
@ -4558,6 +4729,19 @@ object-inspect@^1.7.0:
|
|||
version "1.7.0"
|
||||
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67"
|
||||
|
||||
object-inspect@^1.8.0:
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0"
|
||||
integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==
|
||||
|
||||
object-is@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.2.tgz#c5d2e87ff9e119f78b7a088441519e2eec1573b6"
|
||||
integrity sha512-5lHCz+0uufF6wZ7CRFWJN3hp8Jqblpgve06U5CMQ3f//6iDjPr2PEo9MWCjEssDsa+UZEL4PkFpr+BMop6aKzQ==
|
||||
dependencies:
|
||||
define-properties "^1.1.3"
|
||||
es-abstract "^1.17.5"
|
||||
|
||||
object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
|
||||
|
@ -5068,6 +5252,19 @@ regex-not@^1.0.0, regex-not@^1.0.2:
|
|||
extend-shallow "^3.0.2"
|
||||
safe-regex "^1.1.0"
|
||||
|
||||
regexp.prototype.flags@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz#7aba89b3c13a64509dabcf3ca8d9fbb9bdf5cb75"
|
||||
integrity sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==
|
||||
dependencies:
|
||||
define-properties "^1.1.3"
|
||||
es-abstract "^1.17.0-next.1"
|
||||
|
||||
regexparam@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/regexparam/-/regexparam-1.3.0.tgz#2fe42c93e32a40eff6235d635e0ffa344b92965f"
|
||||
integrity sha512-6IQpFBv6e5vz1QAqI+V4k8P2e/3gRrqfCJ9FI+O1FLQTO+Uz6RXZEZOPmTJ6hlGj7gkERzY5BRCv09whKP96/g==
|
||||
|
||||
regexpu-core@^4.7.0:
|
||||
version "4.7.0"
|
||||
resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.7.0.tgz#fcbf458c50431b0bb7b45d6967b8192d91f3d938"
|
||||
|
@ -5475,6 +5672,14 @@ shortid@^2.2.15:
|
|||
dependencies:
|
||||
nanoid "^2.1.0"
|
||||
|
||||
side-channel@^1.0.2:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.3.tgz#cdc46b057550bbab63706210838df5d4c19519c3"
|
||||
integrity sha512-A6+ByhlLkksFoUepsGxfj5x1gTSrs+OydsRptUxeNCabQpCFUvcwIczgOigI8vhY/OJCnPnyE9rGiwgvr9cS1g==
|
||||
dependencies:
|
||||
es-abstract "^1.18.0-next.0"
|
||||
object-inspect "^1.8.0"
|
||||
|
||||
signal-exit@^3.0.0, signal-exit@^3.0.2:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
|
||||
|
@ -5701,7 +5906,7 @@ string-width@^4.2.0:
|
|||
is-fullwidth-code-point "^3.0.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
string.prototype.trimend@^1.0.0:
|
||||
string.prototype.trimend@^1.0.0, string.prototype.trimend@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913"
|
||||
dependencies:
|
||||
|
@ -5724,7 +5929,7 @@ string.prototype.trimright@^2.1.1:
|
|||
es-abstract "^1.17.5"
|
||||
string.prototype.trimend "^1.0.0"
|
||||
|
||||
string.prototype.trimstart@^1.0.0:
|
||||
string.prototype.trimstart@^1.0.0, string.prototype.trimstart@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz#14af6d9f34b053f7cfc89b72f8f2ee14b9039a54"
|
||||
dependencies:
|
||||
|
@ -6179,10 +6384,43 @@ whatwg-url@^8.0.0:
|
|||
tr46 "^2.0.2"
|
||||
webidl-conversions "^5.0.0"
|
||||
|
||||
which-boxed-primitive@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.1.tgz#cbe8f838ebe91ba2471bb69e9edbda67ab5a5ec1"
|
||||
integrity sha512-7BT4TwISdDGBgaemWU0N0OU7FeAEJ9Oo2P1PHRm/FCWoEi2VLWC9b6xvxAA3C/NMpxg3HXVgi0sMmGbNUbNepQ==
|
||||
dependencies:
|
||||
is-bigint "^1.0.0"
|
||||
is-boolean-object "^1.0.0"
|
||||
is-number-object "^1.0.3"
|
||||
is-string "^1.0.4"
|
||||
is-symbol "^1.0.2"
|
||||
|
||||
which-collection@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.1.tgz#70eab71ebbbd2aefaf32f917082fc62cdcb70906"
|
||||
integrity sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==
|
||||
dependencies:
|
||||
is-map "^2.0.1"
|
||||
is-set "^2.0.1"
|
||||
is-weakmap "^2.0.1"
|
||||
is-weakset "^2.0.1"
|
||||
|
||||
which-module@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
|
||||
|
||||
which-typed-array@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.2.tgz#e5f98e56bda93e3dac196b01d47c1156679c00b2"
|
||||
integrity sha512-KT6okrd1tE6JdZAy3o2VhMoYPh3+J6EMZLyrxBQsZflI1QCZIxMrIYLkosd8Twf+YfknVIHmYQPgJt238p8dnQ==
|
||||
dependencies:
|
||||
available-typed-arrays "^1.0.2"
|
||||
es-abstract "^1.17.5"
|
||||
foreach "^2.0.5"
|
||||
function-bind "^1.1.1"
|
||||
has-symbols "^1.0.1"
|
||||
is-typed-array "^1.1.3"
|
||||
|
||||
which@^1.2.9, which@^1.3.0:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
|
||||
|
|
|
@ -71,6 +71,7 @@
|
|||
"pino-pretty": "^4.0.0",
|
||||
"pouchdb": "^7.2.1",
|
||||
"pouchdb-all-dbs": "^1.0.2",
|
||||
"sharp": "^0.26.0",
|
||||
"squirrelly": "^7.5.0",
|
||||
"tar-fs": "^2.1.0",
|
||||
"uuid": "^3.3.2",
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
const CouchDB = require("../../db")
|
||||
const newid = require("../../db/newid")
|
||||
const actions = require("../../workflows/actions")
|
||||
const logic = require("../../workflows/logic")
|
||||
const triggers = require("../../workflows/triggers")
|
||||
const actions = require("../../automations/actions")
|
||||
const logic = require("../../automations/logic")
|
||||
const triggers = require("../../automations/triggers")
|
||||
|
||||
/*************************
|
||||
* *
|
||||
|
@ -10,12 +10,12 @@ const triggers = require("../../workflows/triggers")
|
|||
* *
|
||||
*************************/
|
||||
|
||||
function cleanWorkflowInputs(workflow) {
|
||||
if (workflow == null) {
|
||||
return workflow
|
||||
function cleanAutomationInputs(automation) {
|
||||
if (automation == null) {
|
||||
return automation
|
||||
}
|
||||
let steps = workflow.definition.steps
|
||||
let trigger = workflow.definition.trigger
|
||||
let steps = automation.definition.steps
|
||||
let trigger = automation.definition.trigger
|
||||
let allSteps = [...steps, trigger]
|
||||
for (let step of allSteps) {
|
||||
if (step == null) {
|
||||
|
@ -27,25 +27,25 @@ function cleanWorkflowInputs(workflow) {
|
|||
}
|
||||
}
|
||||
}
|
||||
return workflow
|
||||
return automation
|
||||
}
|
||||
|
||||
exports.create = async function(ctx) {
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
let workflow = ctx.request.body
|
||||
let automation = ctx.request.body
|
||||
|
||||
workflow._id = newid()
|
||||
automation._id = newid()
|
||||
|
||||
workflow.type = "workflow"
|
||||
workflow = cleanWorkflowInputs(workflow)
|
||||
const response = await db.post(workflow)
|
||||
workflow._rev = response.rev
|
||||
automation.type = "automation"
|
||||
automation = cleanAutomationInputs(automation)
|
||||
const response = await db.post(automation)
|
||||
automation._rev = response.rev
|
||||
|
||||
ctx.status = 200
|
||||
ctx.body = {
|
||||
message: "Workflow created successfully",
|
||||
workflow: {
|
||||
...workflow,
|
||||
message: "Automation created successfully",
|
||||
automation: {
|
||||
...automation,
|
||||
...response,
|
||||
},
|
||||
}
|
||||
|
@ -53,17 +53,17 @@ exports.create = async function(ctx) {
|
|||
|
||||
exports.update = async function(ctx) {
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
let workflow = ctx.request.body
|
||||
let automation = ctx.request.body
|
||||
|
||||
workflow = cleanWorkflowInputs(workflow)
|
||||
const response = await db.put(workflow)
|
||||
workflow._rev = response.rev
|
||||
automation = cleanAutomationInputs(automation)
|
||||
const response = await db.put(automation)
|
||||
automation._rev = response.rev
|
||||
|
||||
ctx.status = 200
|
||||
ctx.body = {
|
||||
message: `Workflow ${workflow._id} updated successfully.`,
|
||||
workflow: {
|
||||
...workflow,
|
||||
message: `Automation ${automation._id} updated successfully.`,
|
||||
automation: {
|
||||
...automation,
|
||||
_rev: response.rev,
|
||||
_id: response.id,
|
||||
},
|
||||
|
@ -73,7 +73,7 @@ exports.update = async function(ctx) {
|
|||
exports.fetch = async function(ctx) {
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
const response = await db.query(`database/by_type`, {
|
||||
key: ["workflow"],
|
||||
key: ["automation"],
|
||||
include_docs: true,
|
||||
})
|
||||
ctx.body = response.rows.map(row => row.doc)
|
||||
|
@ -117,14 +117,14 @@ module.exports.getDefinitionList = async function(ctx) {
|
|||
|
||||
exports.trigger = async function(ctx) {
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
let workflow = await db.get(ctx.params.id)
|
||||
await triggers.externalTrigger(workflow, {
|
||||
let automation = await db.get(ctx.params.id)
|
||||
await triggers.externalTrigger(automation, {
|
||||
...ctx.request.body,
|
||||
instanceId: ctx.user.instanceId,
|
||||
})
|
||||
ctx.status = 200
|
||||
ctx.body = {
|
||||
message: `Workflow ${workflow._id} has been triggered.`,
|
||||
workflow,
|
||||
message: `Automation ${automation._id} has been triggered.`,
|
||||
automation,
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ const fs = require("fs")
|
|||
const AWS = require("aws-sdk")
|
||||
const fetch = require("node-fetch")
|
||||
const { budibaseAppsDir } = require("../../../utilities/budibaseDir")
|
||||
const PouchDB = require("../../../db")
|
||||
|
||||
async function invalidateCDN(cfDistribution, appId) {
|
||||
const cf = new AWS.CloudFront({})
|
||||
|
@ -63,8 +64,22 @@ function walkDir(dirPath, callback) {
|
|||
}
|
||||
}
|
||||
|
||||
function prepareUploadForS3({ filePath, s3Key, metadata, s3 }) {
|
||||
const fileExtension = [...filePath.split(".")].pop()
|
||||
const fileBytes = fs.readFileSync(filePath)
|
||||
return s3
|
||||
.upload({
|
||||
Key: s3Key,
|
||||
Body: fileBytes,
|
||||
ContentType: CONTENT_TYPE_MAP[fileExtension.toLowerCase()],
|
||||
Metadata: metadata,
|
||||
})
|
||||
.promise()
|
||||
}
|
||||
|
||||
exports.uploadAppAssets = async function({
|
||||
appId,
|
||||
instanceId,
|
||||
credentials,
|
||||
bucket,
|
||||
cfDistribution,
|
||||
|
@ -86,30 +101,47 @@ exports.uploadAppAssets = async function({
|
|||
|
||||
const appPages = fs.readdirSync(appAssetsPath)
|
||||
|
||||
const uploads = []
|
||||
let uploads = []
|
||||
|
||||
for (let page of appPages) {
|
||||
walkDir(`${appAssetsPath}/${page}`, function prepareUploadsForS3(filePath) {
|
||||
const fileExtension = [...filePath.split(".")].pop()
|
||||
const fileBytes = fs.readFileSync(filePath)
|
||||
|
||||
const upload = s3
|
||||
.upload({
|
||||
Key: filePath.replace(appAssetsPath, `assets/${appId}`),
|
||||
Body: fileBytes,
|
||||
ContentType: CONTENT_TYPE_MAP[fileExtension],
|
||||
Metadata: {
|
||||
accountId,
|
||||
},
|
||||
})
|
||||
.promise()
|
||||
|
||||
uploads.push(upload)
|
||||
// Upload HTML, CSS and JS for each page of the web app
|
||||
walkDir(`${appAssetsPath}/${page}`, function(filePath) {
|
||||
const appAssetUpload = prepareUploadForS3({
|
||||
filePath,
|
||||
s3Key: filePath.replace(appAssetsPath, `assets/${appId}`),
|
||||
s3,
|
||||
metadata: { accountId },
|
||||
})
|
||||
uploads.push(appAssetUpload)
|
||||
})
|
||||
}
|
||||
|
||||
// Upload file attachments
|
||||
const db = new PouchDB(instanceId)
|
||||
const fileUploads = await db.get("_local/fileuploads")
|
||||
if (fileUploads) {
|
||||
for (let file of fileUploads.uploads) {
|
||||
if (file.uploaded) continue
|
||||
|
||||
const attachmentUpload = prepareUploadForS3({
|
||||
filePath: file.path,
|
||||
s3Key: `assets/${appId}/attachments/${file.name}`,
|
||||
s3,
|
||||
metadata: { accountId },
|
||||
})
|
||||
|
||||
uploads.push(attachmentUpload)
|
||||
|
||||
// mark file as uploaded
|
||||
file.uploaded = true
|
||||
}
|
||||
|
||||
db.put(fileUploads)
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.all(uploads)
|
||||
// TODO: update dynamoDB with a synopsis of the app deployment for historical purposes
|
||||
await invalidateCDN(cfDistribution, appId)
|
||||
} catch (err) {
|
||||
console.error("Error uploading budibase app assets to s3", err)
|
||||
|
|
|
@ -42,6 +42,7 @@ exports.deployApp = async function(ctx) {
|
|||
await uploadAppAssets({
|
||||
clientId,
|
||||
appId: ctx.user.appId,
|
||||
instanceId: ctx.user.instanceId,
|
||||
...credentials,
|
||||
})
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -188,7 +188,7 @@ exports.destroy = async function(ctx) {
|
|||
}
|
||||
ctx.body = await db.remove(ctx.params.recordId, ctx.params.revId)
|
||||
ctx.status = 200
|
||||
// for workflows
|
||||
// for automations
|
||||
ctx.record = record
|
||||
emitEvent(`record:delete`, ctx, record)
|
||||
}
|
||||
|
|
|
@ -1,13 +1,18 @@
|
|||
const send = require("koa-send")
|
||||
const { resolve, join } = require("path")
|
||||
const jwt = require("jsonwebtoken")
|
||||
const fetch = require("node-fetch")
|
||||
const fs = require("fs")
|
||||
const uuid = require("uuid")
|
||||
|
||||
const {
|
||||
budibaseAppsDir,
|
||||
budibaseTempDir,
|
||||
} = require("../../utilities/budibaseDir")
|
||||
const CouchDB = require("../../db")
|
||||
const setBuilderToken = require("../../utilities/builder/setBuilderToken")
|
||||
const { ANON_LEVEL_ID } = require("../../utilities/accessLevels")
|
||||
const jwt = require("jsonwebtoken")
|
||||
const fetch = require("node-fetch")
|
||||
const fileProcessor = require("../../utilities/fileProcessor")
|
||||
|
||||
exports.serveBuilder = async function(ctx) {
|
||||
let builderPath = resolve(__dirname, "../../../builder")
|
||||
|
@ -17,6 +22,66 @@ exports.serveBuilder = async function(ctx) {
|
|||
await send(ctx, ctx.file, { root: ctx.devPath || builderPath })
|
||||
}
|
||||
|
||||
exports.processLocalFileUpload = async function(ctx) {
|
||||
const { files } = ctx.request.body
|
||||
|
||||
const attachmentsPath = resolve(
|
||||
budibaseAppsDir(),
|
||||
ctx.user.appId,
|
||||
"attachments"
|
||||
)
|
||||
|
||||
// create attachments dir if it doesnt exist
|
||||
!fs.existsSync(attachmentsPath) &&
|
||||
fs.mkdirSync(attachmentsPath, { recursive: true })
|
||||
|
||||
const filesToProcess = files.map(file => {
|
||||
const fileExtension = [...file.path.split(".")].pop()
|
||||
// filenames converted to UUIDs so they are unique
|
||||
const fileName = `${uuid.v4()}.${fileExtension}`
|
||||
|
||||
return {
|
||||
...file,
|
||||
fileName,
|
||||
extension: fileExtension,
|
||||
outputPath: join(attachmentsPath, fileName),
|
||||
url: join("/attachments", fileName),
|
||||
}
|
||||
})
|
||||
|
||||
const fileProcessOperations = filesToProcess.map(file =>
|
||||
fileProcessor.process(file)
|
||||
)
|
||||
|
||||
try {
|
||||
const processedFiles = await Promise.all(fileProcessOperations)
|
||||
|
||||
let pendingFileUploads
|
||||
// local document used to track which files need to be uploaded
|
||||
// db.get throws an error if the document doesn't exist
|
||||
// need to use a promise to default
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
await db
|
||||
.get("_local/fileuploads")
|
||||
.then(data => {
|
||||
pendingFileUploads = data
|
||||
})
|
||||
.catch(() => {
|
||||
pendingFileUploads = { _id: "_local/fileuploads", uploads: [] }
|
||||
})
|
||||
|
||||
pendingFileUploads.uploads = [
|
||||
...processedFiles,
|
||||
...pendingFileUploads.uploads,
|
||||
]
|
||||
await db.put(pendingFileUploads)
|
||||
|
||||
ctx.body = processedFiles
|
||||
} catch (err) {
|
||||
ctx.throw(500, err)
|
||||
}
|
||||
}
|
||||
|
||||
exports.serveApp = async function(ctx) {
|
||||
const mainOrAuth = ctx.isAuthenticated ? "main" : "unauthenticated"
|
||||
|
||||
|
@ -62,6 +127,24 @@ exports.serveApp = async function(ctx) {
|
|||
await send(ctx, ctx.file || "index.html", { root: ctx.devPath || appPath })
|
||||
}
|
||||
|
||||
exports.serveAttachment = async function(ctx) {
|
||||
const appId = ctx.user.appId
|
||||
|
||||
const attachmentsPath = resolve(budibaseAppsDir(), appId, "attachments")
|
||||
|
||||
// Serve from CloudFront
|
||||
if (process.env.CLOUD) {
|
||||
const S3_URL = `https://cdn.app.budi.live/assets/${appId}/attachments/${ctx.file}`
|
||||
|
||||
const response = await fetch(S3_URL)
|
||||
const body = await response.text()
|
||||
ctx.body = body
|
||||
return
|
||||
}
|
||||
|
||||
await send(ctx, ctx.file, { root: attachmentsPath })
|
||||
}
|
||||
|
||||
exports.serveAppAsset = async function(ctx) {
|
||||
// default to homedir
|
||||
const mainOrAuth = ctx.isAuthenticated ? "main" : "unauthenticated"
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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")
|
||||
|
@ -30,7 +30,7 @@ function generateValidator(existing = false) {
|
|||
_id: existing ? Joi.string().required() : Joi.string(),
|
||||
_rev: existing ? Joi.string().required() : Joi.string(),
|
||||
name: Joi.string().required(),
|
||||
type: Joi.string().valid("workflow").required(),
|
||||
type: Joi.string().valid("automation").required(),
|
||||
definition: Joi.object({
|
||||
steps: Joi.array().required().items(generateStepSchema(["ACTION", "LOGIC"])),
|
||||
trigger: generateStepSchema(["TRIGGER"]),
|
||||
|
@ -40,40 +40,40 @@ function generateValidator(existing = false) {
|
|||
|
||||
router
|
||||
.get(
|
||||
"/api/workflows/trigger/list",
|
||||
"/api/automations/trigger/list",
|
||||
authorized(BUILDER),
|
||||
controller.getTriggerList
|
||||
)
|
||||
.get(
|
||||
"/api/workflows/action/list",
|
||||
"/api/automations/action/list",
|
||||
authorized(BUILDER),
|
||||
controller.getActionList
|
||||
)
|
||||
.get(
|
||||
"/api/workflows/logic/list",
|
||||
"/api/automations/logic/list",
|
||||
authorized(BUILDER),
|
||||
controller.getLogicList
|
||||
)
|
||||
.get(
|
||||
"/api/workflows/definitions/list",
|
||||
"/api/automations/definitions/list",
|
||||
authorized(BUILDER),
|
||||
controller.getDefinitionList
|
||||
)
|
||||
.get("/api/workflows", authorized(BUILDER), controller.fetch)
|
||||
.get("/api/workflows/:id", authorized(BUILDER), controller.find)
|
||||
.get("/api/automations", authorized(BUILDER), controller.fetch)
|
||||
.get("/api/automations/:id", authorized(BUILDER), controller.find)
|
||||
.put(
|
||||
"/api/workflows",
|
||||
"/api/automations",
|
||||
authorized(BUILDER),
|
||||
generateValidator(true),
|
||||
controller.update
|
||||
)
|
||||
.post(
|
||||
"/api/workflows",
|
||||
"/api/automations",
|
||||
authorized(BUILDER),
|
||||
generateValidator(false),
|
||||
controller.create
|
||||
)
|
||||
.post("/api/workflows/:id/trigger", controller.trigger)
|
||||
.delete("/api/workflows/:id/:rev", authorized(BUILDER), controller.destroy)
|
||||
.post("/api/automations/:id/trigger", controller.trigger)
|
||||
.delete("/api/automations/:id/:rev", authorized(BUILDER), controller.destroy)
|
||||
|
||||
module.exports = router
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@ const Router = require("@koa/router")
|
|||
const controller = require("../controllers/static")
|
||||
const { budibaseTempDir } = require("../../utilities/budibaseDir")
|
||||
const env = require("../../environment")
|
||||
const authorized = require("../../middleware/authorized")
|
||||
const { BUILDER } = require("../../utilities/accessLevels")
|
||||
|
||||
const router = Router()
|
||||
|
||||
|
@ -21,8 +23,14 @@ if (env.NODE_ENV !== "production") {
|
|||
}
|
||||
|
||||
router
|
||||
.post(
|
||||
"/api/attachments/process",
|
||||
authorized(BUILDER),
|
||||
controller.processLocalFileUpload
|
||||
)
|
||||
.get("/componentlibrary", controller.serveComponentLibrary)
|
||||
.get("/assets/:file*", controller.serveAppAsset)
|
||||
.get("/attachments/:file*", controller.serveAttachment)
|
||||
.get("/:appId/:path*", controller.serveApp)
|
||||
|
||||
module.exports = router
|
||||
|
|
|
@ -13,9 +13,9 @@ const {
|
|||
|
||||
const { delay } = require("./testUtils")
|
||||
|
||||
const TEST_WORKFLOW = {
|
||||
_id: "Test Workflow",
|
||||
name: "My Workflow",
|
||||
const TEST_AUTOMATION = {
|
||||
_id: "Test Automation",
|
||||
name: "My Automation",
|
||||
pageId: "123123123",
|
||||
screenId: "kasdkfldsafkl",
|
||||
live: true,
|
||||
|
@ -27,20 +27,20 @@ const TEST_WORKFLOW = {
|
|||
steps: [
|
||||
],
|
||||
},
|
||||
type: "workflow",
|
||||
type: "automation",
|
||||
}
|
||||
|
||||
let ACTION_DEFINITIONS = {}
|
||||
let TRIGGER_DEFINITIONS = {}
|
||||
let LOGIC_DEFINITIONS = {}
|
||||
|
||||
describe("/workflows", () => {
|
||||
describe("/automations", () => {
|
||||
let request
|
||||
let server
|
||||
let app
|
||||
let instance
|
||||
let workflow
|
||||
let workflowId
|
||||
let automation
|
||||
let automationId
|
||||
|
||||
beforeAll(async () => {
|
||||
({ request, server } = await supertest())
|
||||
|
@ -49,7 +49,7 @@ describe("/workflows", () => {
|
|||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
if (workflow) await destroyDocument(workflow.id)
|
||||
if (automation) await destroyDocument(automation.id)
|
||||
instance = await createInstance(request, app._id)
|
||||
})
|
||||
|
||||
|
@ -57,18 +57,18 @@ describe("/workflows", () => {
|
|||
server.close()
|
||||
})
|
||||
|
||||
const createWorkflow = async () => {
|
||||
workflow = await insertDocument(instance._id, {
|
||||
type: "workflow",
|
||||
...TEST_WORKFLOW
|
||||
const createAutomation = async () => {
|
||||
automation = await insertDocument(instance._id, {
|
||||
type: "automation",
|
||||
...TEST_AUTOMATION
|
||||
})
|
||||
workflow = { ...workflow, ...TEST_WORKFLOW }
|
||||
automation = { ...automation, ...TEST_AUTOMATION }
|
||||
}
|
||||
|
||||
describe("get definitions", () => {
|
||||
it("returns a list of definitions for actions", async () => {
|
||||
const res = await request
|
||||
.get(`/api/workflows/action/list`)
|
||||
.get(`/api/automations/action/list`)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
@ -79,7 +79,7 @@ describe("/workflows", () => {
|
|||
|
||||
it("returns a list of definitions for triggers", async () => {
|
||||
const res = await request
|
||||
.get(`/api/workflows/trigger/list`)
|
||||
.get(`/api/automations/trigger/list`)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
@ -90,7 +90,7 @@ describe("/workflows", () => {
|
|||
|
||||
it("returns a list of definitions for actions", async () => {
|
||||
const res = await request
|
||||
.get(`/api/workflows/logic/list`)
|
||||
.get(`/api/automations/logic/list`)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
@ -101,7 +101,7 @@ describe("/workflows", () => {
|
|||
|
||||
it("returns all of the definitions in one", async () => {
|
||||
const res = await request
|
||||
.get(`/api/workflows/definitions/list`)
|
||||
.get(`/api/automations/definitions/list`)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
@ -113,7 +113,7 @@ describe("/workflows", () => {
|
|||
})
|
||||
|
||||
describe("create", () => {
|
||||
it("should setup the workflow fully", () => {
|
||||
it("should setup the automation fully", () => {
|
||||
let trigger = TRIGGER_DEFINITIONS["RECORD_SAVED"]
|
||||
trigger.id = "wadiawdo34"
|
||||
let saveAction = ACTION_DEFINITIONS["SAVE_RECORD"]
|
||||
|
@ -123,51 +123,51 @@ describe("/workflows", () => {
|
|||
}
|
||||
saveAction.id = "awde444wk"
|
||||
|
||||
TEST_WORKFLOW.definition.steps.push(saveAction)
|
||||
TEST_WORKFLOW.definition.trigger = trigger
|
||||
TEST_AUTOMATION.definition.steps.push(saveAction)
|
||||
TEST_AUTOMATION.definition.trigger = trigger
|
||||
})
|
||||
|
||||
it("returns a success message when the workflow is successfully created", async () => {
|
||||
it("returns a success message when the automation is successfully created", async () => {
|
||||
const res = await request
|
||||
.post(`/api/workflows`)
|
||||
.post(`/api/automations`)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.send(TEST_WORKFLOW)
|
||||
.send(TEST_AUTOMATION)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
expect(res.body.message).toEqual("Workflow created successfully")
|
||||
expect(res.body.workflow.name).toEqual("My Workflow")
|
||||
expect(res.body.workflow._id).not.toEqual(null)
|
||||
workflowId = res.body.workflow._id
|
||||
expect(res.body.message).toEqual("Automation created successfully")
|
||||
expect(res.body.automation.name).toEqual("My Automation")
|
||||
expect(res.body.automation._id).not.toEqual(null)
|
||||
automationId = res.body.automation._id
|
||||
})
|
||||
|
||||
it("should apply authorization to endpoint", async () => {
|
||||
await builderEndpointShouldBlockNormalUsers({
|
||||
request,
|
||||
method: "POST",
|
||||
url: `/api/workflows`,
|
||||
url: `/api/automations`,
|
||||
instanceId: instance._id,
|
||||
appId: app._id,
|
||||
body: TEST_WORKFLOW
|
||||
body: TEST_AUTOMATION
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("trigger", () => {
|
||||
it("trigger the workflow successfully", async () => {
|
||||
it("trigger the automation successfully", async () => {
|
||||
let model = await createModel(request, app._id, instance._id)
|
||||
TEST_WORKFLOW.definition.trigger.inputs.modelId = model._id
|
||||
TEST_WORKFLOW.definition.steps[0].inputs.record.modelId = model._id
|
||||
await createWorkflow()
|
||||
TEST_AUTOMATION.definition.trigger.inputs.modelId = model._id
|
||||
TEST_AUTOMATION.definition.steps[0].inputs.record.modelId = model._id
|
||||
await createAutomation()
|
||||
const res = await request
|
||||
.post(`/api/workflows/${workflow._id}/trigger`)
|
||||
.post(`/api/automations/${automation._id}/trigger`)
|
||||
.send({ name: "Test" })
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
expect(res.body.message).toEqual(`Workflow ${workflow._id} has been triggered.`)
|
||||
expect(res.body.workflow.name).toEqual(TEST_WORKFLOW.name)
|
||||
// wait for workflow to complete in background
|
||||
expect(res.body.message).toEqual(`Automation ${automation._id} has been triggered.`)
|
||||
expect(res.body.automation.name).toEqual(TEST_AUTOMATION.name)
|
||||
// wait for automation to complete in background
|
||||
await delay(500)
|
||||
let elements = await getAllFromModel(request, app._id, instance._id, model._id)
|
||||
expect(elements.length).toEqual(1)
|
||||
|
@ -177,42 +177,42 @@ describe("/workflows", () => {
|
|||
})
|
||||
|
||||
describe("update", () => {
|
||||
it("updates a workflows data", async () => {
|
||||
await createWorkflow()
|
||||
workflow._id = workflow.id
|
||||
workflow._rev = workflow.rev
|
||||
workflow.name = "Updated Name"
|
||||
workflow.type = "workflow"
|
||||
it("updates a automations data", async () => {
|
||||
await createAutomation()
|
||||
automation._id = automation.id
|
||||
automation._rev = automation.rev
|
||||
automation.name = "Updated Name"
|
||||
automation.type = "automation"
|
||||
|
||||
const res = await request
|
||||
.put(`/api/workflows`)
|
||||
.put(`/api/automations`)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.send(workflow)
|
||||
.send(automation)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
expect(res.body.message).toEqual("Workflow Test Workflow updated successfully.")
|
||||
expect(res.body.workflow.name).toEqual("Updated Name")
|
||||
expect(res.body.message).toEqual("Automation Test Automation updated successfully.")
|
||||
expect(res.body.automation.name).toEqual("Updated Name")
|
||||
})
|
||||
})
|
||||
|
||||
describe("fetch", () => {
|
||||
it("return all the workflows for an instance", async () => {
|
||||
await createWorkflow()
|
||||
it("return all the automations for an instance", async () => {
|
||||
await createAutomation()
|
||||
const res = await request
|
||||
.get(`/api/workflows`)
|
||||
.get(`/api/automations`)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
expect(res.body[0]).toEqual(expect.objectContaining(TEST_WORKFLOW))
|
||||
expect(res.body[0]).toEqual(expect.objectContaining(TEST_AUTOMATION))
|
||||
})
|
||||
|
||||
it("should apply authorization to endpoint", async () => {
|
||||
await builderEndpointShouldBlockNormalUsers({
|
||||
request,
|
||||
method: "GET",
|
||||
url: `/api/workflows`,
|
||||
url: `/api/automations`,
|
||||
instanceId: instance._id,
|
||||
appId: app._id,
|
||||
})
|
||||
|
@ -220,23 +220,23 @@ describe("/workflows", () => {
|
|||
})
|
||||
|
||||
describe("destroy", () => {
|
||||
it("deletes a workflow by its ID", async () => {
|
||||
await createWorkflow()
|
||||
it("deletes a automation by its ID", async () => {
|
||||
await createAutomation()
|
||||
const res = await request
|
||||
.delete(`/api/workflows/${workflow.id}/${workflow.rev}`)
|
||||
.delete(`/api/automations/${automation.id}/${automation.rev}`)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
expect(res.body.id).toEqual(TEST_WORKFLOW._id)
|
||||
expect(res.body.id).toEqual(TEST_AUTOMATION._id)
|
||||
})
|
||||
|
||||
it("should apply authorization to endpoint", async () => {
|
||||
await createWorkflow()
|
||||
await createAutomation()
|
||||
await builderEndpointShouldBlockNormalUsers({
|
||||
request,
|
||||
method: "DELETE",
|
||||
url: `/api/workflows/${workflow.id}/${workflow._rev}`,
|
||||
url: `/api/automations/${automation.id}/${automation._rev}`,
|
||||
instanceId: instance._id,
|
||||
appId: app._id,
|
||||
})
|
|
@ -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()
|
||||
})
|
||||
|
|
|
@ -23,7 +23,7 @@ function runWorker(job) {
|
|||
*/
|
||||
module.exports.init = function() {
|
||||
actions.init().then(() => {
|
||||
triggers.workflowQueue.process(async job => {
|
||||
triggers.automationQueue.process(async job => {
|
||||
if (environment.BUDIBASE_ENVIRONMENT === "PRODUCTION") {
|
||||
await runWorker(job)
|
||||
} else {
|
|
@ -82,4 +82,4 @@ module.exports.run = async function({ inputs, instanceId }) {
|
|||
response: err,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@ module.exports.definition = {
|
|||
name: "Delay",
|
||||
icon: "ri-time-fill",
|
||||
tagline: "Delay for {{inputs.time}} milliseconds",
|
||||
description: "Delay the workflow until an amount of time has passed",
|
||||
description: "Delay the automation until an amount of time has passed",
|
||||
stepId: "DELAY",
|
||||
inputs: {},
|
||||
schema: {
|
|
@ -16,7 +16,7 @@ module.exports.definition = {
|
|||
name: "Filter",
|
||||
tagline: "{{inputs.field}} {{inputs.condition}} {{inputs.value}}",
|
||||
icon: "ri-git-branch-line",
|
||||
description: "Filter any workflows which do not meet certain conditions",
|
||||
description: "Filter any automations which do not meet certain conditions",
|
||||
type: "LOGIC",
|
||||
stepId: "FILTER",
|
||||
inputs: {
|
|
@ -28,6 +28,11 @@ module.exports.definition = {
|
|||
},
|
||||
outputs: {
|
||||
properties: {
|
||||
record: {
|
||||
type: "object",
|
||||
customType: "record",
|
||||
description: "The new record",
|
||||
},
|
||||
response: {
|
||||
type: "object",
|
||||
description: "The response from the table",
|
||||
|
@ -69,6 +74,7 @@ module.exports.run = async function({ inputs, instanceId }) {
|
|||
try {
|
||||
await recordController.save(ctx)
|
||||
return {
|
||||
record: inputs.record,
|
||||
response: ctx.body,
|
||||
id: ctx.body._id,
|
||||
revision: ctx.body._rev,
|
|
@ -42,18 +42,18 @@ function recurseMustache(inputs, context) {
|
|||
}
|
||||
|
||||
/**
|
||||
* The workflow orchestrator is a class responsible for executing workflows.
|
||||
* It handles the context of the workflow and makes sure each step gets the correct
|
||||
* The automation orchestrator is a class responsible for executing automations.
|
||||
* It handles the context of the automation and makes sure each step gets the correct
|
||||
* inputs and handles any outputs.
|
||||
*/
|
||||
class Orchestrator {
|
||||
constructor(workflow, triggerOutput) {
|
||||
constructor(automation, triggerOutput) {
|
||||
this._instanceId = triggerOutput.instanceId
|
||||
// remove from context
|
||||
delete triggerOutput.instanceId
|
||||
// step zero is never used as the mustache is zero indexed for customer facing
|
||||
this._context = { steps: [{}], trigger: triggerOutput }
|
||||
this._workflow = workflow
|
||||
this._automation = automation
|
||||
}
|
||||
|
||||
async getStepFunctionality(type, stepId) {
|
||||
|
@ -64,14 +64,14 @@ class Orchestrator {
|
|||
step = logic.getLogic(stepId)
|
||||
}
|
||||
if (step == null) {
|
||||
throw `Cannot find workflow step by name ${stepId}`
|
||||
throw `Cannot find automation step by name ${stepId}`
|
||||
}
|
||||
return step
|
||||
}
|
||||
|
||||
async execute() {
|
||||
let workflow = this._workflow
|
||||
for (let step of workflow.definition.steps) {
|
||||
let automation = this._automation
|
||||
for (let step of automation.definition.steps) {
|
||||
let stepFn = await this.getStepFunctionality(step.type, step.stepId)
|
||||
step.inputs = recurseMustache(step.inputs, this._context)
|
||||
// instanceId is always passed
|
||||
|
@ -90,11 +90,11 @@ class Orchestrator {
|
|||
// callback is required for worker-farm to state that the worker thread has completed
|
||||
module.exports = async (job, cb = null) => {
|
||||
try {
|
||||
const workflowOrchestrator = new Orchestrator(
|
||||
job.data.workflow,
|
||||
const automationOrchestrator = new Orchestrator(
|
||||
job.data.automation,
|
||||
job.data.event
|
||||
)
|
||||
await workflowOrchestrator.execute()
|
||||
await automationOrchestrator.execute()
|
||||
if (cb) {
|
||||
cb()
|
||||
}
|
|
@ -2,7 +2,7 @@ const CouchDB = require("../db")
|
|||
const emitter = require("../events/index")
|
||||
const InMemoryQueue = require("./queue/inMemoryQueue")
|
||||
|
||||
let workflowQueue = new InMemoryQueue()
|
||||
let automationQueue = new InMemoryQueue()
|
||||
|
||||
const FAKE_STRING = "TEST"
|
||||
const FAKE_BOOL = false
|
||||
|
@ -76,28 +76,31 @@ const BUILTIN_DEFINITIONS = {
|
|||
},
|
||||
}
|
||||
|
||||
async function queueRelevantRecordWorkflows(event, eventType) {
|
||||
async function queueRelevantRecordAutomations(event, eventType) {
|
||||
if (event.instanceId == null) {
|
||||
throw `No instanceId specified for ${eventType} - check event emitters.`
|
||||
}
|
||||
const db = new CouchDB(event.instanceId)
|
||||
const workflowsToTrigger = await db.query("database/by_workflow_trigger", {
|
||||
key: [eventType],
|
||||
include_docs: true,
|
||||
})
|
||||
const automationsToTrigger = await db.query(
|
||||
"database/by_automation_trigger",
|
||||
{
|
||||
key: [eventType],
|
||||
include_docs: true,
|
||||
}
|
||||
)
|
||||
|
||||
const workflows = workflowsToTrigger.rows.map(wf => wf.doc)
|
||||
for (let workflow of workflows) {
|
||||
let workflowDef = workflow.definition
|
||||
let workflowTrigger = workflowDef ? workflowDef.trigger : {}
|
||||
const automations = automationsToTrigger.rows.map(wf => wf.doc)
|
||||
for (let automation of automations) {
|
||||
let automationDef = automation.definition
|
||||
let automationTrigger = automationDef ? automationDef.trigger : {}
|
||||
if (
|
||||
!workflow.live ||
|
||||
!workflowTrigger.inputs ||
|
||||
workflowTrigger.inputs.modelId !== event.record.modelId
|
||||
!automation.live ||
|
||||
!automationTrigger.inputs ||
|
||||
automationTrigger.inputs.modelId !== event.record.modelId
|
||||
) {
|
||||
continue
|
||||
}
|
||||
workflowQueue.add({ workflow, event })
|
||||
automationQueue.add({ automation, event })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -105,18 +108,18 @@ emitter.on("record:save", async function(event) {
|
|||
if (!event || !event.record || !event.record.modelId) {
|
||||
return
|
||||
}
|
||||
await queueRelevantRecordWorkflows(event, "record:save")
|
||||
await queueRelevantRecordAutomations(event, "record:save")
|
||||
})
|
||||
|
||||
emitter.on("record:delete", async function(event) {
|
||||
if (!event || !event.record || !event.record.modelId) {
|
||||
return
|
||||
}
|
||||
await queueRelevantRecordWorkflows(event, "record:delete")
|
||||
await queueRelevantRecordAutomations(event, "record:delete")
|
||||
})
|
||||
|
||||
async function fillRecordOutput(workflow, params) {
|
||||
let triggerSchema = workflow.definition.trigger
|
||||
async function fillRecordOutput(automation, params) {
|
||||
let triggerSchema = automation.definition.trigger
|
||||
let modelId = triggerSchema.inputs.modelId
|
||||
const db = new CouchDB(params.instanceId)
|
||||
try {
|
||||
|
@ -147,19 +150,19 @@ async function fillRecordOutput(workflow, params) {
|
|||
return params
|
||||
}
|
||||
|
||||
module.exports.externalTrigger = async function(workflow, params) {
|
||||
module.exports.externalTrigger = async function(automation, params) {
|
||||
// TODO: replace this with allowing user in builder to input values in future
|
||||
if (
|
||||
workflow.definition != null &&
|
||||
workflow.definition.trigger != null &&
|
||||
workflow.definition.trigger.inputs.modelId != null
|
||||
automation.definition != null &&
|
||||
automation.definition.trigger != null &&
|
||||
automation.definition.trigger.inputs.modelId != null
|
||||
) {
|
||||
params = await fillRecordOutput(workflow, params)
|
||||
params = await fillRecordOutput(automation, params)
|
||||
}
|
||||
|
||||
workflowQueue.add({ workflow, event: params })
|
||||
automationQueue.add({ automation, event: params })
|
||||
}
|
||||
|
||||
module.exports.workflowQueue = workflowQueue
|
||||
module.exports.automationQueue = automationQueue
|
||||
|
||||
module.exports.BUILTIN_DEFINITIONS = BUILTIN_DEFINITIONS
|
|
@ -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.
|
||||
*/
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
module.exports.READ_MODEL = "read-model"
|
||||
module.exports.WRITE_MODEL = "write-model"
|
||||
module.exports.READ_VIEW = "read-view"
|
||||
module.exports.EXECUTE_WORKFLOW = "execute-workflow"
|
||||
module.exports.EXECUTE_AUTOMATION = "execute-automation"
|
||||
module.exports.USER_MANAGEMENT = "user-management"
|
||||
module.exports.BUILDER = "builder"
|
||||
module.exports.LIST_USERS = "list-users"
|
||||
|
@ -32,4 +32,5 @@ module.exports.adminPermissions = [
|
|||
// to avoid circular dependencies this is included later, after exporting all enums
|
||||
const permissions = require("./permissions")
|
||||
module.exports.generateAdminPermissions = permissions.generateAdminPermissions
|
||||
module.exports.generatePowerUserPermissions = permissions.generatePowerUserPermissions
|
||||
module.exports.generatePowerUserPermissions =
|
||||
permissions.generatePowerUserPermissions
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
const fs = require("fs")
|
||||
const sharp = require("sharp")
|
||||
const fsPromises = fs.promises
|
||||
|
||||
const FORMATS = {
|
||||
IMAGES: ["png", "jpg", "jpeg", "gif", "svg", "tiff", "raw"],
|
||||
}
|
||||
|
||||
async function processImage(file) {
|
||||
const imgMeta = await sharp(file.path)
|
||||
.resize(300)
|
||||
.toFile(file.outputPath)
|
||||
|
||||
return {
|
||||
...file,
|
||||
...imgMeta,
|
||||
}
|
||||
}
|
||||
|
||||
async function process(file) {
|
||||
if (FORMATS.IMAGES.includes(file.extension.toLowerCase())) {
|
||||
return await processImage(file)
|
||||
}
|
||||
|
||||
// No processing required
|
||||
await fsPromises.copyFile(file.path, file.outputPath)
|
||||
return file
|
||||
}
|
||||
|
||||
exports.process = process
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue