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}\"",
|
"format": "prettier --write \"{,!(node_modules)/**/}*.{js,jsx,svelte}\"",
|
||||||
"test:e2e": "lerna run cy:test",
|
"test:e2e": "lerna run cy:test",
|
||||||
"test:e2e:ci": "lerna run cy:ci"
|
"test:e2e:ci": "lerna run cy:ci"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome": "^1.1.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,6 @@ node_modules_win
|
||||||
package-lock.json
|
package-lock.json
|
||||||
release/
|
release/
|
||||||
dist/
|
dist/
|
||||||
cypress/screenshots
|
|
||||||
cypress/videos
|
|
||||||
routify
|
routify
|
||||||
|
cypress/videos
|
||||||
|
cypress/screenshots
|
||||||
|
|
|
@ -1,27 +1,27 @@
|
||||||
context("Create a workflow", () => {
|
context("Create a automation", () => {
|
||||||
before(() => {
|
before(() => {
|
||||||
cy.server()
|
cy.server()
|
||||||
cy.visit("localhost:4001/_builder")
|
cy.visit("localhost:4001/_builder")
|
||||||
|
|
||||||
cy.createApp(
|
cy.createApp(
|
||||||
"Workflow Test App",
|
"Automation Test App",
|
||||||
"This app is used to test that workflows do in fact work!"
|
"This app is used to test that automations do in fact work!"
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
// https://on.cypress.io/interacting-with-elements
|
// https://on.cypress.io/interacting-with-elements
|
||||||
it("should create a workflow", () => {
|
it("should create a automation", () => {
|
||||||
cy.createTestTableWithData()
|
cy.createTestTableWithData()
|
||||||
|
|
||||||
cy.contains("workflow").click()
|
cy.contains("automate").click()
|
||||||
cy.contains("Create New Workflow").click()
|
cy.contains("Create New Automation").click()
|
||||||
cy.get("input").type("Add Record")
|
cy.get("input").type("Add Record")
|
||||||
cy.contains("Save").click()
|
cy.contains("Save").click()
|
||||||
|
|
||||||
// Add trigger
|
// Add trigger
|
||||||
cy.get("[data-cy=add-workflow-component]").click()
|
cy.get("[data-cy=add-automation-component]").click()
|
||||||
cy.get("[data-cy=RECORD_SAVED]").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")
|
cy.get("select")
|
||||||
.first()
|
.first()
|
||||||
.select("dog")
|
.select("dog")
|
||||||
|
@ -29,7 +29,7 @@ context("Create a workflow", () => {
|
||||||
|
|
||||||
// Create action
|
// Create action
|
||||||
cy.get("[data-cy=SAVE_RECORD]").click()
|
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")
|
cy.get("select")
|
||||||
.first()
|
.first()
|
||||||
.select("dog")
|
.select("dog")
|
||||||
|
@ -42,10 +42,10 @@ context("Create a workflow", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Save
|
// Save
|
||||||
cy.contains("Save Workflow").click()
|
cy.contains("Save Automation").click()
|
||||||
|
|
||||||
// Activate Workflow
|
// Activate Automation
|
||||||
cy.get("[data-cy=activate-workflow]").click()
|
cy.get("[data-cy=activate-automation]").click()
|
||||||
cy.contains("Add Record").should("be.visible")
|
cy.contains("Add Record").should("be.visible")
|
||||||
cy.get(".stop-button.highlighted").should("be.visible")
|
cy.get(".stop-button.highlighted").should("be.visible")
|
||||||
})
|
})
|
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": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "^1.33.0",
|
"@budibase/bbui": "^1.34.2",
|
||||||
"@budibase/client": "^0.1.21",
|
"@budibase/client": "^0.1.21",
|
||||||
"@budibase/colorpicker": "^1.0.1",
|
"@budibase/colorpicker": "^1.0.1",
|
||||||
|
"@fortawesome/fontawesome-free": "^5.14.0",
|
||||||
"@sentry/browser": "5.19.1",
|
"@sentry/browser": "5.19.1",
|
||||||
"@svelteschool/svelte-forms": "^0.7.0",
|
"@svelteschool/svelte-forms": "^0.7.0",
|
||||||
"britecharts": "^2.16.0",
|
"britecharts": "^2.16.0",
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
const apiCall = method => async (url, body) => {
|
const apiCall = method => async (
|
||||||
const headers = {
|
url,
|
||||||
"Content-Type": "application/json",
|
body,
|
||||||
}
|
headers = { "Content-Type": "application/json" }
|
||||||
|
) => {
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: method,
|
method: method,
|
||||||
body: body && JSON.stringify(body),
|
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 { getStore } from "./store"
|
||||||
import { getBackendUiStore } from "./store/backend"
|
import { getBackendUiStore } from "./store/backend"
|
||||||
import { getWorkflowStore } from "./store/workflow/"
|
import { getAutomationStore } from "./store/automation/"
|
||||||
import analytics from "../analytics"
|
import analytics from "../analytics"
|
||||||
|
|
||||||
export const store = getStore()
|
export const store = getStore()
|
||||||
export const backendUiStore = getBackendUiStore()
|
export const backendUiStore = getBackendUiStore()
|
||||||
export const workflowStore = getWorkflowStore()
|
export const automationStore = getAutomationStore()
|
||||||
|
|
||||||
export const initialise = async () => {
|
export const initialise = async () => {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -1,59 +1,59 @@
|
||||||
import { generate } from "shortid"
|
import { generate } from "shortid"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class responsible for the traversing of the workflow definition.
|
* Class responsible for the traversing of the automation definition.
|
||||||
* Workflow definitions are stored in linked lists.
|
* Automation definitions are stored in linked lists.
|
||||||
*/
|
*/
|
||||||
export default class Workflow {
|
export default class Automation {
|
||||||
constructor(workflow) {
|
constructor(automation) {
|
||||||
this.workflow = workflow
|
this.automation = automation
|
||||||
}
|
}
|
||||||
|
|
||||||
hasTrigger() {
|
hasTrigger() {
|
||||||
return this.workflow.definition.trigger
|
return this.automation.definition.trigger
|
||||||
}
|
}
|
||||||
|
|
||||||
addBlock(block) {
|
addBlock(block) {
|
||||||
// Make sure to add trigger if doesn't exist
|
// Make sure to add trigger if doesn't exist
|
||||||
if (!this.hasTrigger() && block.type === "TRIGGER") {
|
if (!this.hasTrigger() && block.type === "TRIGGER") {
|
||||||
const trigger = { id: generate(), ...block }
|
const trigger = { id: generate(), ...block }
|
||||||
this.workflow.definition.trigger = trigger
|
this.automation.definition.trigger = trigger
|
||||||
return trigger
|
return trigger
|
||||||
}
|
}
|
||||||
|
|
||||||
const newBlock = { id: generate(), ...block }
|
const newBlock = { id: generate(), ...block }
|
||||||
this.workflow.definition.steps = [
|
this.automation.definition.steps = [
|
||||||
...this.workflow.definition.steps,
|
...this.automation.definition.steps,
|
||||||
newBlock,
|
newBlock,
|
||||||
]
|
]
|
||||||
return newBlock
|
return newBlock
|
||||||
}
|
}
|
||||||
|
|
||||||
updateBlock(updatedBlock, id) {
|
updateBlock(updatedBlock, id) {
|
||||||
const { steps, trigger } = this.workflow.definition
|
const { steps, trigger } = this.automation.definition
|
||||||
|
|
||||||
if (trigger && trigger.id === id) {
|
if (trigger && trigger.id === id) {
|
||||||
this.workflow.definition.trigger = updatedBlock
|
this.automation.definition.trigger = updatedBlock
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const stepIdx = steps.findIndex(step => step.id === id)
|
const stepIdx = steps.findIndex(step => step.id === id)
|
||||||
if (stepIdx < 0) throw new Error("Block not found.")
|
if (stepIdx < 0) throw new Error("Block not found.")
|
||||||
steps.splice(stepIdx, 1, updatedBlock)
|
steps.splice(stepIdx, 1, updatedBlock)
|
||||||
this.workflow.definition.steps = steps
|
this.automation.definition.steps = steps
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteBlock(id) {
|
deleteBlock(id) {
|
||||||
const { steps, trigger } = this.workflow.definition
|
const { steps, trigger } = this.automation.definition
|
||||||
|
|
||||||
if (trigger && trigger.id === id) {
|
if (trigger && trigger.id === id) {
|
||||||
this.workflow.definition.trigger = null
|
this.automation.definition.trigger = null
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const stepIdx = steps.findIndex(step => step.id === id)
|
const stepIdx = steps.findIndex(step => step.id === id)
|
||||||
if (stepIdx < 0) throw new Error("Block not found.")
|
if (stepIdx < 0) throw new Error("Block not found.")
|
||||||
steps.splice(stepIdx, 1)
|
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 {
|
export default {
|
||||||
name: "Test workflow",
|
name: "Test automation",
|
||||||
definition: {
|
definition: {
|
||||||
steps: [
|
steps: [
|
||||||
{
|
{
|
||||||
|
@ -68,7 +68,7 @@ export default {
|
||||||
stepId: "RECORD_SAVED",
|
stepId: "RECORD_SAVED",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
type: "workflow",
|
type: "automation",
|
||||||
ok: true,
|
ok: true,
|
||||||
id: "b384f861f4754e1693835324a7fcca62",
|
id: "b384f861f4754e1693835324a7fcca62",
|
||||||
rev: "1-aa1c2cbd868ef02e26f8fad531dd7e37",
|
rev: "1-aa1c2cbd868ef02e26f8fad531dd7e37",
|
|
@ -28,6 +28,11 @@ export const getBackendUiStore = () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
records: {
|
records: {
|
||||||
|
save: () =>
|
||||||
|
store.update(state => {
|
||||||
|
state.selectedView = state.selectedView
|
||||||
|
return state
|
||||||
|
}),
|
||||||
delete: () =>
|
delete: () =>
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
state.selectedView = state.selectedView
|
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>
|
<script>
|
||||||
import { afterUpdate } from "svelte"
|
import { afterUpdate } from "svelte"
|
||||||
import { workflowStore, backendUiStore } from "builderStore"
|
import { automationStore, backendUiStore } from "builderStore"
|
||||||
import { notifier } from "builderStore/store/notifications"
|
import { notifier } from "builderStore/store/notifications"
|
||||||
import Flowchart from "./flowchart/FlowChart.svelte"
|
import Flowchart from "./flowchart/FlowChart.svelte"
|
||||||
|
|
||||||
$: workflow = $workflowStore.selectedWorkflow?.workflow
|
$: automation = $automationStore.selectedAutomation?.automation
|
||||||
$: workflowLive = workflow?.live
|
$: automationLive = automation?.live
|
||||||
$: instanceId = $backendUiStore.selectedDatabase._id
|
$: instanceId = $backendUiStore.selectedDatabase._id
|
||||||
|
|
||||||
function onSelect(block) {
|
function onSelect(block) {
|
||||||
workflowStore.update(state => {
|
automationStore.update(state => {
|
||||||
state.selectedBlock = block
|
state.selectedBlock = block
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function setWorkflowLive(live) {
|
function setAutomationLive(live) {
|
||||||
workflow.live = live
|
automation.live = live
|
||||||
workflowStore.actions.save({ instanceId, workflow })
|
automationStore.actions.save({ instanceId, automation })
|
||||||
if (live) {
|
if (live) {
|
||||||
notifier.info(`Workflow ${workflow.name} enabled.`)
|
notifier.info(`Automation ${automation.name} enabled.`)
|
||||||
} else {
|
} else {
|
||||||
notifier.danger(`Workflow ${workflow.name} disabled.`)
|
notifier.danger(`Automation ${automation.name} disabled.`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<Flowchart {workflow} {onSelect} />
|
<Flowchart {automation} {onSelect} />
|
||||||
</section>
|
</section>
|
||||||
<footer>
|
<footer>
|
||||||
{#if workflow}
|
{#if automation}
|
||||||
<button
|
<button
|
||||||
class:highlighted={workflowLive}
|
class:highlighted={automationLive}
|
||||||
class:hoverable={workflowLive}
|
class:hoverable={automationLive}
|
||||||
class="stop-button hoverable">
|
class="stop-button hoverable">
|
||||||
<i class="ri-stop-fill" on:click={() => setWorkflowLive(false)} />
|
<i class="ri-stop-fill" on:click={() => setAutomationLive(false)} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class:highlighted={!workflowLive}
|
class:highlighted={!automationLive}
|
||||||
class:hoverable={!workflowLive}
|
class:hoverable={!automationLive}
|
||||||
class="play-button hoverable"
|
class="play-button hoverable"
|
||||||
data-cy="activate-workflow"
|
data-cy="activate-automation"
|
||||||
on:click={() => setWorkflowLive(true)}>
|
on:click={() => setAutomationLive(true)}>
|
||||||
<i class="ri-play-fill" />
|
<i class="ri-play-fill" />
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
Before Width: | Height: | Size: 290 B After Width: | Height: | Size: 290 B |
|
@ -4,17 +4,17 @@
|
||||||
import { flip } from "svelte/animate"
|
import { flip } from "svelte/animate"
|
||||||
import { fade, fly } from "svelte/transition"
|
import { fade, fly } from "svelte/transition"
|
||||||
|
|
||||||
export let workflow
|
export let automation
|
||||||
export let onSelect
|
export let onSelect
|
||||||
let blocks
|
let blocks
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
blocks = []
|
blocks = []
|
||||||
if (workflow) {
|
if (automation) {
|
||||||
if (workflow.definition.trigger) {
|
if (automation.definition.trigger) {
|
||||||
blocks.push(workflow.definition.trigger)
|
blocks.push(automation.definition.trigger)
|
||||||
}
|
}
|
||||||
blocks = blocks.concat(workflow.definition.steps || [])
|
blocks = blocks.concat(automation.definition.steps || [])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
|
@ -1,13 +1,13 @@
|
||||||
<script>
|
<script>
|
||||||
import { workflowStore } from "builderStore"
|
import { automationStore } from "builderStore"
|
||||||
import WorkflowBlockTagline from "./WorkflowBlockTagline.svelte"
|
import AutomationBlockTagline from "./AutomationBlockTagline.svelte"
|
||||||
|
|
||||||
export let onSelect
|
export let onSelect
|
||||||
export let block
|
export let block
|
||||||
let selected
|
let selected
|
||||||
|
|
||||||
$: selected = $workflowStore.selectedBlock?.id === block.id
|
$: selected = $automationStore.selectedBlock?.id === block.id
|
||||||
$: steps = $workflowStore.selectedWorkflow?.workflow?.definition?.steps ?? []
|
$: steps = $automationStore.selectedAutomation?.automation?.definition?.steps ?? []
|
||||||
$: blockIdx = steps.findIndex(step => step.id === block.id)
|
$: blockIdx = steps.findIndex(step => step.id === block.id)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@
|
||||||
</header>
|
</header>
|
||||||
<hr />
|
<hr />
|
||||||
<p>
|
<p>
|
||||||
<WorkflowBlockTagline {block} />
|
<AutomationBlockTagline {block} />
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,17 +2,17 @@
|
||||||
import Modal from "svelte-simple-modal"
|
import Modal from "svelte-simple-modal"
|
||||||
import { notifier } from "builderStore/store/notifications"
|
import { notifier } from "builderStore/store/notifications"
|
||||||
import { onMount, getContext } from "svelte"
|
import { onMount, getContext } from "svelte"
|
||||||
import { backendUiStore, workflowStore } from "builderStore"
|
import { backendUiStore, automationStore } from "builderStore"
|
||||||
import CreateWorkflowModal from "./CreateWorkflowModal.svelte"
|
import CreateAutomationModal from "./CreateAutomationModal.svelte"
|
||||||
import { Button } from "@budibase/bbui"
|
import { Button } from "@budibase/bbui"
|
||||||
|
|
||||||
const { open, close } = getContext("simple-modal")
|
const { open, close } = getContext("simple-modal")
|
||||||
|
|
||||||
$: selectedWorkflowId = $workflowStore.selectedWorkflow?.workflow?._id
|
$: selectedAutomationId = $automationStore.selectedAutomation?.automation?._id
|
||||||
|
|
||||||
function newWorkflow() {
|
function newAutomation() {
|
||||||
open(
|
open(
|
||||||
CreateWorkflowModal,
|
CreateAutomationModal,
|
||||||
{
|
{
|
||||||
onClosed: close,
|
onClosed: close,
|
||||||
},
|
},
|
||||||
|
@ -21,20 +21,20 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
workflowStore.actions.fetch()
|
automationStore.actions.fetch()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<Button purple wide on:click={newWorkflow}>Create New Workflow</Button>
|
<Button purple wide on:click={newAutomation}>Create New Automation</Button>
|
||||||
<ul>
|
<ul>
|
||||||
{#each $workflowStore.workflows as workflow}
|
{#each $automationStore.automations as automation}
|
||||||
<li
|
<li
|
||||||
class="workflow-item"
|
class="automation-item"
|
||||||
class:selected={workflow._id === selectedWorkflowId}
|
class:selected={automation._id === selectedAutomationId}
|
||||||
on:click={() => workflowStore.actions.select(workflow)}>
|
on:click={() => automationStore.actions.select(automation)}>
|
||||||
<i class="ri-stackshare-line" class:live={workflow.live} />
|
<i class="ri-stackshare-line" class:live={automation.live} />
|
||||||
{workflow.name}
|
{automation.name}
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -68,7 +68,7 @@
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workflow-item {
|
.automation-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
padding-left: 12px;
|
padding-left: 12px;
|
||||||
|
@ -78,21 +78,21 @@
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
}
|
}
|
||||||
|
|
||||||
.workflow-item i {
|
.automation-item i {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workflow-item:hover {
|
.automation-item:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background: var(--grey-1);
|
background: var(--grey-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.workflow-item.selected {
|
.automation-item.selected {
|
||||||
background: var(--grey-2);
|
background: var(--grey-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.new-workflow-button {
|
.new-automation-button {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border: 1px solid var(--grey-4);
|
border: 1px solid var(--grey-4);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
@ -108,7 +108,7 @@
|
||||||
transition: all 2ms;
|
transition: all 2ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
.new-workflow-button:hover {
|
.new-automation-button:hover {
|
||||||
background: var(--grey-1);
|
background: var(--grey-1);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { store, backendUiStore, workflowStore } from "builderStore"
|
import { store, backendUiStore, automationStore } from "builderStore"
|
||||||
import { notifier } from "builderStore/store/notifications"
|
import { notifier } from "builderStore/store/notifications"
|
||||||
import ActionButton from "components/common/ActionButton.svelte"
|
import ActionButton from "components/common/ActionButton.svelte"
|
||||||
import { Input } from "@budibase/bbui"
|
import { Input } from "@budibase/bbui"
|
||||||
|
@ -12,19 +12,19 @@
|
||||||
$: instanceId = $backendUiStore.selectedDatabase._id
|
$: instanceId = $backendUiStore.selectedDatabase._id
|
||||||
$: appId = $store.appId
|
$: appId = $store.appId
|
||||||
|
|
||||||
async function createWorkflow() {
|
async function createAutomation() {
|
||||||
await workflowStore.actions.create({
|
await automationStore.actions.create({
|
||||||
name,
|
name,
|
||||||
instanceId,
|
instanceId,
|
||||||
})
|
})
|
||||||
onClosed()
|
onClosed()
|
||||||
notifier.success(`Workflow ${name} created.`)
|
notifier.success(`Automation ${name} created.`)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<header>
|
<header>
|
||||||
<i class="ri-stackshare-line" />
|
<i class="ri-stackshare-line" />
|
||||||
Create Workflow
|
Create Automation
|
||||||
</header>
|
</header>
|
||||||
<div>
|
<div>
|
||||||
<Input bind:value={name} label="Name" />
|
<Input bind:value={name} label="Name" />
|
||||||
|
@ -32,10 +32,10 @@
|
||||||
<footer>
|
<footer>
|
||||||
<a href="https://docs.budibase.com">
|
<a href="https://docs.budibase.com">
|
||||||
<i class="ri-information-line" />
|
<i class="ri-information-line" />
|
||||||
Learn about workflows
|
Learn about automations
|
||||||
</a>
|
</a>
|
||||||
<ActionButton secondary on:click={onClosed}>Cancel</ActionButton>
|
<ActionButton secondary on:click={onClosed}>Cancel</ActionButton>
|
||||||
<ActionButton disabled={!valid} on:click={createWorkflow}>Save</ActionButton>
|
<ActionButton disabled={!valid} on:click={createAutomation}>Save</ActionButton>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<style>
|
<style>
|
|
@ -1,22 +1,22 @@
|
||||||
<script>
|
<script>
|
||||||
import { workflowStore } from "builderStore"
|
import { automationStore } from "builderStore"
|
||||||
import WorkflowList from "./WorkflowList/WorkflowList.svelte"
|
import AutomationList from "./AutomationList/AutomationList.svelte"
|
||||||
import BlockList from "./BlockList/BlockList.svelte"
|
import BlockList from "./BlockList/BlockList.svelte"
|
||||||
|
|
||||||
let selectedTab = "WORKFLOWS"
|
let selectedTab = "AUTOMATIONS"
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<header>
|
<header>
|
||||||
<span
|
<span
|
||||||
data-cy="workflow-list"
|
data-cy="automation-list"
|
||||||
class="hoverable workflow-header"
|
class="hoverable automation-header"
|
||||||
class:selected={selectedTab === 'WORKFLOWS'}
|
class:selected={selectedTab === 'AUTOMATIONS'}
|
||||||
on:click={() => (selectedTab = 'WORKFLOWS')}>
|
on:click={() => (selectedTab = 'AUTOMATIONS')}>
|
||||||
Workflows
|
Automations
|
||||||
</span>
|
</span>
|
||||||
{#if $workflowStore.selectedWorkflow}
|
{#if $automationStore.selectedAutomation}
|
||||||
<span
|
<span
|
||||||
data-cy="add-workflow-component"
|
data-cy="add-automation-component"
|
||||||
class="hoverable"
|
class="hoverable"
|
||||||
class:selected={selectedTab === 'ADD'}
|
class:selected={selectedTab === 'ADD'}
|
||||||
on:click={() => (selectedTab = 'ADD')}>
|
on:click={() => (selectedTab = 'ADD')}>
|
||||||
|
@ -24,8 +24,8 @@
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</header>
|
</header>
|
||||||
{#if selectedTab === 'WORKFLOWS'}
|
{#if selectedTab === 'AUTOMATIONS'}
|
||||||
<WorkflowList />
|
<AutomationList />
|
||||||
{:else if selectedTab === 'ADD'}
|
{:else if selectedTab === 'ADD'}
|
||||||
<BlockList />
|
<BlockList />
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -40,7 +40,7 @@
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workflow-header {
|
.automation-header {
|
||||||
margin-right: 20px;
|
margin-right: 20px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
<script>
|
<script>
|
||||||
import { workflowStore } from "builderStore"
|
import { automationStore } from "builderStore"
|
||||||
|
|
||||||
export let blockDefinition
|
export let blockDefinition
|
||||||
export let stepId
|
export let stepId
|
||||||
export let blockType
|
export let blockType
|
||||||
|
|
||||||
function addBlockToWorkflow() {
|
function addBlockToAutomation() {
|
||||||
workflowStore.actions.addBlockToWorkflow({
|
automationStore.actions.addBlockToAutomation({
|
||||||
...blockDefinition,
|
...blockDefinition,
|
||||||
args: blockDefinition.args || {},
|
args: blockDefinition.args || {},
|
||||||
stepId,
|
stepId,
|
||||||
|
@ -16,20 +16,20 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="workflow-block hoverable"
|
class="automation-block hoverable"
|
||||||
on:click={addBlockToWorkflow}
|
on:click={addBlockToAutomation}
|
||||||
data-cy={stepId}>
|
data-cy={stepId}>
|
||||||
<div>
|
<div>
|
||||||
<i class={blockDefinition.icon} />
|
<i class={blockDefinition.icon} />
|
||||||
</div>
|
</div>
|
||||||
<div class="workflow-text">
|
<div class="automation-text">
|
||||||
<h4>{blockDefinition.name}</h4>
|
<h4>{blockDefinition.name}</h4>
|
||||||
<p>{blockDefinition.description}</p>
|
<p>{blockDefinition.description}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.workflow-block {
|
.automation-block {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 20px auto;
|
grid-template-columns: 20px auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -38,11 +38,11 @@
|
||||||
border-radius: var(--border-radius-m);
|
border-radius: var(--border-radius-m);
|
||||||
}
|
}
|
||||||
|
|
||||||
.workflow-block:hover {
|
.automation-block:hover {
|
||||||
background-color: var(--grey-1);
|
background-color: var(--grey-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.workflow-text {
|
.automation-text {
|
||||||
margin-left: 16px;
|
margin-left: 16px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
<script>
|
<script>
|
||||||
import { workflowStore } from "builderStore"
|
import { automationStore } from "builderStore"
|
||||||
import WorkflowBlock from "./WorkflowBlock.svelte"
|
import AutomationBlock from "./AutomationBlock.svelte"
|
||||||
import FlatButtonGroup from "components/userInterface/FlatButtonGroup.svelte"
|
import FlatButtonGroup from "components/userInterface/FlatButtonGroup.svelte"
|
||||||
|
|
||||||
let selectedTab = "TRIGGER"
|
let selectedTab = "TRIGGER"
|
||||||
let buttonProps = []
|
let buttonProps = []
|
||||||
$: blocks = Object.entries($workflowStore.blockDefinitions[selectedTab])
|
$: blocks = Object.entries($automationStore.blockDefinitions[selectedTab])
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
if ($workflowStore.selectedWorkflow.hasTrigger()) {
|
if ($automationStore.selectedAutomation.hasTrigger()) {
|
||||||
buttonProps = [
|
buttonProps = [
|
||||||
{ value: "ACTION", text: "Action" },
|
{ value: "ACTION", text: "Action" },
|
||||||
{ value: "LOGIC", text: "Logic" },
|
{ value: "LOGIC", text: "Logic" },
|
||||||
|
@ -33,7 +33,7 @@
|
||||||
<FlatButtonGroup value={selectedTab} {buttonProps} onChange={onChangeTab} />
|
<FlatButtonGroup value={selectedTab} {buttonProps} onChange={onChangeTab} />
|
||||||
<div id="blocklist">
|
<div id="blocklist">
|
||||||
{#each blocks as [stepId, blockDefinition]}
|
{#each blocks as [stepId, blockDefinition]}
|
||||||
<WorkflowBlock {blockDefinition} {stepId} blockType={selectedTab} />
|
<AutomationBlock {blockDefinition} {stepId} blockType={selectedTab} />
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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 ModelSelector from "./ParamInputs/ModelSelector.svelte"
|
||||||
import RecordSelector from "./ParamInputs/RecordSelector.svelte"
|
import RecordSelector from "./ParamInputs/RecordSelector.svelte"
|
||||||
import { Input, TextArea, Select, Label } from "@budibase/bbui"
|
import { Input, TextArea, Select, Label } from "@budibase/bbui"
|
||||||
import { workflowStore } from "builderStore"
|
import { automationStore } from "builderStore"
|
||||||
import BindableInput from "../../userInterface/BindableInput.svelte"
|
import BindableInput from "../../userInterface/BindableInput.svelte"
|
||||||
|
|
||||||
export let block
|
export let block
|
||||||
$: inputs = Object.entries(block.schema?.inputs?.properties || {})
|
$: inputs = Object.entries(block.schema?.inputs?.properties || {})
|
||||||
$: bindings = getAvailableBindings(
|
$: bindings = getAvailableBindings(
|
||||||
block,
|
block,
|
||||||
$workflowStore.selectedWorkflow?.workflow?.definition
|
$automationStore.selectedAutomation?.automation?.definition
|
||||||
)
|
)
|
||||||
|
|
||||||
function getAvailableBindings(block, workflow) {
|
function getAvailableBindings(block, automation) {
|
||||||
if (!block || !workflow) {
|
if (!block || !automation) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find previous steps to the selected one
|
// Find previous steps to the selected one
|
||||||
let allSteps = [...workflow.steps]
|
let allSteps = [...automation.steps]
|
||||||
if (workflow.trigger) {
|
if (automation.trigger) {
|
||||||
allSteps = [workflow.trigger, ...allSteps]
|
allSteps = [automation.trigger, ...allSteps]
|
||||||
}
|
}
|
||||||
const blockIdx = allSteps.findIndex(step => step.id === block.id)
|
const blockIdx = allSteps.findIndex(step => step.id === block.id)
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container" data-cy="workflow-block-setup">
|
<div class="container" data-cy="automation-block-setup">
|
||||||
<div class="block-label">{block.name}</div>
|
<div class="block-label">{block.name}</div>
|
||||||
{#each inputs as [key, value]}
|
{#each inputs as [key, value]}
|
||||||
<div class="bb-margin-xl block-field">
|
<div class="bb-margin-xl block-field">
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { store, backendUiStore, workflowStore } from "builderStore"
|
import { store, backendUiStore, automationStore } from "builderStore"
|
||||||
import { notifier } from "builderStore/store/notifications"
|
import { notifier } from "builderStore/store/notifications"
|
||||||
import ActionButton from "components/common/ActionButton.svelte"
|
import ActionButton from "components/common/ActionButton.svelte"
|
||||||
|
|
||||||
|
@ -10,32 +10,32 @@
|
||||||
$: valid = !!name
|
$: valid = !!name
|
||||||
$: instanceId = $backendUiStore.selectedDatabase._id
|
$: instanceId = $backendUiStore.selectedDatabase._id
|
||||||
|
|
||||||
async function deleteWorkflow() {
|
async function deleteAutomation() {
|
||||||
await workflowStore.actions.delete({
|
await automationStore.actions.delete({
|
||||||
instanceId,
|
instanceId,
|
||||||
workflow: $workflowStore.selectedWorkflow.workflow,
|
automation: $automationStore.selectedAutomation.automation,
|
||||||
})
|
})
|
||||||
onClosed()
|
onClosed()
|
||||||
notifier.danger("Workflow deleted.")
|
notifier.danger("Automation deleted.")
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<header>
|
<header>
|
||||||
<i class="ri-stackshare-line" />
|
<i class="ri-stackshare-line" />
|
||||||
Delete Workflow
|
Delete Automation
|
||||||
</header>
|
</header>
|
||||||
<div>
|
<div>
|
||||||
<p>
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<footer>
|
<footer>
|
||||||
<a href="https://docs.budibase.com">
|
<a href="https://docs.budibase.com">
|
||||||
<i class="ri-information-line" />
|
<i class="ri-information-line" />
|
||||||
Learn about workflows
|
Learn about automations
|
||||||
</a>
|
</a>
|
||||||
<ActionButton on:click={onClosed}>Cancel</ActionButton>
|
<ActionButton on:click={onClosed}>Cancel</ActionButton>
|
||||||
<ActionButton alert on:click={deleteWorkflow}>Delete</ActionButton>
|
<ActionButton alert on:click={deleteAutomation}>Delete</ActionButton>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<style>
|
<style>
|
|
@ -38,7 +38,7 @@
|
||||||
<option value={option}>{option}</option>
|
<option value={option}>{option}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</Select>
|
</Select>
|
||||||
{:else if schema.type === "string"}
|
{:else if schema.type === "string" || schema.type === "number"}
|
||||||
<BindableInput
|
<BindableInput
|
||||||
thin
|
thin
|
||||||
bind:value={value[field]}
|
bind:value={value[field]}
|
|
@ -1,49 +1,49 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import { backendUiStore, workflowStore } from "builderStore"
|
import { backendUiStore, automationStore } from "builderStore"
|
||||||
import { notifier } from "builderStore/store/notifications"
|
import { notifier } from "builderStore/store/notifications"
|
||||||
import WorkflowBlockSetup from "./WorkflowBlockSetup.svelte"
|
import AutomationBlockSetup from "./AutomationBlockSetup.svelte"
|
||||||
import DeleteWorkflowModal from "./DeleteWorkflowModal.svelte"
|
import DeleteAutomationModal from "./DeleteAutomationModal.svelte"
|
||||||
import { Button, Input, Label } from "@budibase/bbui"
|
import { Button, Input, Label } from "@budibase/bbui"
|
||||||
|
|
||||||
const { open, close } = getContext("simple-modal")
|
const { open, close } = getContext("simple-modal")
|
||||||
|
|
||||||
let selectedTab = "SETUP"
|
let selectedTab = "SETUP"
|
||||||
|
|
||||||
$: workflow = $workflowStore.selectedWorkflow?.workflow
|
$: automation = $automationStore.selectedAutomation?.automation
|
||||||
$: allowDeleteBlock =
|
$: allowDeleteBlock =
|
||||||
$workflowStore.selectedBlock?.type !== "TRIGGER" ||
|
$automationStore.selectedBlock?.type !== "TRIGGER" ||
|
||||||
!workflow?.definition?.steps?.length
|
!automation?.definition?.steps?.length
|
||||||
|
|
||||||
function deleteWorkflow() {
|
function deleteAutomation() {
|
||||||
open(
|
open(
|
||||||
DeleteWorkflowModal,
|
DeleteAutomationModal,
|
||||||
{ onClosed: close },
|
{ onClosed: close },
|
||||||
{ styleContent: { padding: "0" } }
|
{ styleContent: { padding: "0" } }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteWorkflowBlock() {
|
function deleteAutomationBlock() {
|
||||||
workflowStore.actions.deleteWorkflowBlock($workflowStore.selectedBlock)
|
automationStore.actions.deleteAutomationBlock($automationStore.selectedBlock)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function testWorkflow() {
|
async function testAutomation() {
|
||||||
const result = await workflowStore.actions.trigger({
|
const result = await automationStore.actions.trigger({
|
||||||
workflow: $workflowStore.selectedWorkflow.workflow,
|
automation: $automationStore.selectedAutomation.automation,
|
||||||
})
|
})
|
||||||
if (result.status === 200) {
|
if (result.status === 200) {
|
||||||
notifier.success(`Workflow ${workflow.name} triggered successfully.`)
|
notifier.success(`Automation ${automation.name} triggered successfully.`)
|
||||||
} else {
|
} else {
|
||||||
notifier.danger(`Failed to trigger workflow ${workflow.name}.`)
|
notifier.danger(`Failed to trigger automation ${automation.name}.`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveWorkflow() {
|
async function saveAutomation() {
|
||||||
await workflowStore.actions.save({
|
await automationStore.actions.save({
|
||||||
instanceId: $backendUiStore.selectedDatabase._id,
|
instanceId: $backendUiStore.selectedDatabase._id,
|
||||||
workflow,
|
automation,
|
||||||
})
|
})
|
||||||
notifier.success(`Workflow ${workflow.name} saved.`)
|
notifier.success(`Automation ${automation.name} saved.`)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -56,38 +56,38 @@
|
||||||
Setup
|
Setup
|
||||||
</span>
|
</span>
|
||||||
</header>
|
</header>
|
||||||
{#if $workflowStore.selectedBlock}
|
{#if $automationStore.selectedBlock}
|
||||||
<WorkflowBlockSetup bind:block={$workflowStore.selectedBlock} />
|
<AutomationBlockSetup bind:block={$automationStore.selectedBlock} />
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<Button green wide data-cy="save-workflow-setup" on:click={saveWorkflow}>
|
<Button green wide data-cy="save-automation-setup" on:click={saveAutomation}>
|
||||||
Save Workflow
|
Save Automation
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
disabled={!allowDeleteBlock}
|
disabled={!allowDeleteBlock}
|
||||||
red
|
red
|
||||||
wide
|
wide
|
||||||
on:click={deleteWorkflowBlock}>
|
on:click={deleteAutomationBlock}>
|
||||||
Delete Block
|
Delete Block
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{:else if $workflowStore.selectedWorkflow}
|
{:else if $automationStore.selectedAutomation}
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<div class="block-label">
|
<div class="block-label">
|
||||||
Workflow
|
Automation
|
||||||
<b>{workflow.name}</b>
|
<b>{automation.name}</b>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button secondary wide on:click={testWorkflow}>Test Workflow</Button>
|
<Button secondary wide on:click={testAutomation}>Test Automation</Button>
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<Button
|
<Button
|
||||||
green
|
green
|
||||||
wide
|
wide
|
||||||
data-cy="save-workflow-setup"
|
data-cy="save-automation-setup"
|
||||||
on:click={saveWorkflow}>
|
on:click={saveAutomation}>
|
||||||
Save Workflow
|
Save Automation
|
||||||
</Button>
|
</Button>
|
||||||
<Button red wide on:click={deleteWorkflow}>Delete Workflow</Button>
|
<Button red wide on:click={deleteAutomation}>Delete Automation</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/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>
|
<script>
|
||||||
import { Circle } from "svelte-loading-spinners"
|
import { Circle } from "svelte-loading-spinners"
|
||||||
|
|
||||||
|
export let size = "60"
|
||||||
</script>
|
</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 { Button, Icon } from "@budibase/bbui"
|
||||||
import ActionButton from "components/common/ActionButton.svelte"
|
import ActionButton from "components/common/ActionButton.svelte"
|
||||||
import LinkedRecord from "./LinkedRecord.svelte"
|
import LinkedRecord from "./LinkedRecord.svelte"
|
||||||
|
import AttachmentList from "./AttachmentList.svelte"
|
||||||
import TablePagination from "./TablePagination.svelte"
|
import TablePagination from "./TablePagination.svelte"
|
||||||
import { DeleteRecordModal, CreateEditRecordModal } from "./modals"
|
import { DeleteRecordModal, CreateEditRecordModal } from "./modals"
|
||||||
import RowPopover from "./popovers/Row.svelte"
|
import RowPopover from "./popovers/Row.svelte"
|
||||||
|
@ -90,6 +91,8 @@
|
||||||
<td>
|
<td>
|
||||||
{#if schema[header].type === 'link'}
|
{#if schema[header].type === 'link'}
|
||||||
<LinkedRecord field={schema[header]} ids={row[header]} />
|
<LinkedRecord field={schema[header]} ids={row[header]} />
|
||||||
|
{:else if schema[header].type === 'attachment'}
|
||||||
|
<AttachmentList files={row[header] || []} />
|
||||||
{:else}{getOr('', header, row)}{/if}
|
{:else}{getOr('', header, row)}{/if}
|
||||||
</td>
|
</td>
|
||||||
{/each}
|
{/each}
|
||||||
|
@ -108,6 +111,7 @@
|
||||||
section {
|
section {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
import api from "builderStore/api"
|
import api from "builderStore/api"
|
||||||
import { Button, Icon } from "@budibase/bbui"
|
import { Button, Icon } from "@budibase/bbui"
|
||||||
import ActionButton from "components/common/ActionButton.svelte"
|
import ActionButton from "components/common/ActionButton.svelte"
|
||||||
import LinkedRecord from "./LinkedRecord.svelte"
|
import AttachmentList from "./AttachmentList.svelte"
|
||||||
import TablePagination from "./TablePagination.svelte"
|
import TablePagination from "./TablePagination.svelte"
|
||||||
import { DeleteRecordModal, CreateEditRecordModal } from "./modals"
|
import { DeleteRecordModal, CreateEditRecordModal } from "./modals"
|
||||||
import RowPopover from "./popovers/Row.svelte"
|
import RowPopover from "./popovers/Row.svelte"
|
||||||
|
@ -59,7 +59,11 @@
|
||||||
{#each paginatedData as row}
|
{#each paginatedData as row}
|
||||||
<tr>
|
<tr>
|
||||||
{#each columns as header}
|
{#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}
|
{/each}
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { onMount } from "svelte"
|
import { onMount, tick } from "svelte"
|
||||||
import { store, backendUiStore } from "builderStore"
|
import { store, backendUiStore } from "builderStore"
|
||||||
import { notifier } from "builderStore/store/notifications"
|
import { notifier } from "builderStore/store/notifications"
|
||||||
import { compose, map, get, flatten } from "lodash/fp"
|
import { compose, map, get, flatten } from "lodash/fp"
|
||||||
|
@ -34,12 +34,9 @@
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
backendUiStore.update(state => {
|
onClosed()
|
||||||
state.selectedView = state.selectedView
|
notifier.success("Record saved successfully.")
|
||||||
onClosed()
|
backendUiStore.actions.records.save(recordResponse)
|
||||||
notifier.success("Record created successfully.")
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
<script>
|
<script>
|
||||||
import { Input, Select } from "@budibase/bbui"
|
import { Input, Select, Label, DatePicker } from "@budibase/bbui"
|
||||||
import DatePicker from "components/common/DatePicker.svelte"
|
import Dropzone from "components/common/Dropzone.svelte"
|
||||||
|
|
||||||
export let meta
|
export let meta
|
||||||
export let value = meta.type === "boolean" ? false : ""
|
export let value = meta.type === "boolean" ? false : ""
|
||||||
|
export let originalValue
|
||||||
|
|
||||||
let isSelect =
|
let isSelect =
|
||||||
meta.type === "string" &&
|
meta.type === "string" &&
|
||||||
|
@ -17,6 +18,7 @@
|
||||||
if (meta.type === "datetime") return "date"
|
if (meta.type === "datetime") return "date"
|
||||||
if (meta.type === "number") return "number"
|
if (meta.type === "number") return "number"
|
||||||
if (meta.type === "boolean") return "checkbox"
|
if (meta.type === "boolean") return "checkbox"
|
||||||
|
if (meta.type === "attachment") return "file"
|
||||||
if (isSelect) return "select"
|
if (isSelect) return "select"
|
||||||
|
|
||||||
return "text"
|
return "text"
|
||||||
|
@ -45,7 +47,11 @@
|
||||||
{/each}
|
{/each}
|
||||||
</Select>
|
</Select>
|
||||||
{:else if type === 'date'}
|
{: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}
|
{:else}
|
||||||
{#if type === 'checkbox'}
|
{#if type === 'checkbox'}
|
||||||
<label>{meta.name}</label>
|
<label>{meta.name}</label>
|
||||||
|
@ -64,7 +70,6 @@
|
||||||
label {
|
label {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: var(--font-size-s);
|
font-size: var(--font-size-s);
|
||||||
float: left;
|
margin-bottom: 12px;
|
||||||
margin-right: 8px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,7 +1,14 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import { backendUiStore } from "builderStore"
|
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 { FIELDS } from "constants/backend"
|
||||||
import CreateEditRecord from "../modals/CreateEditRecord.svelte"
|
import CreateEditRecord from "../modals/CreateEditRecord.svelte"
|
||||||
import DeleteRecordModal from "../modals/DeleteRecord.svelte"
|
import DeleteRecordModal from "../modals/DeleteRecord.svelte"
|
||||||
|
@ -48,11 +55,11 @@
|
||||||
<ul>
|
<ul>
|
||||||
<li data-cy="edit-row" on:click={showEditor}>
|
<li data-cy="edit-row" on:click={showEditor}>
|
||||||
<Icon name="edit" />
|
<Icon name="edit" />
|
||||||
Edit
|
<span>Edit</span>
|
||||||
</li>
|
</li>
|
||||||
<li data-cy="delete-row" on:click={deleteRow}>
|
<li data-cy="delete-row" on:click={deleteRow}>
|
||||||
<Icon name="delete" />
|
<Icon name="delete" />
|
||||||
Delete
|
<span>Delete</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -79,7 +86,6 @@
|
||||||
li {
|
li {
|
||||||
display: flex;
|
display: flex;
|
||||||
font-family: var(--font-sans);
|
font-family: var(--font-sans);
|
||||||
font-size: var(--font-size-xs);
|
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
padding: var(--spacing-s) var(--spacing-m);
|
padding: var(--spacing-s) var(--spacing-m);
|
||||||
margin: auto 0px;
|
margin: auto 0px;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { writable } from "svelte/store"
|
import { writable } from "svelte/store"
|
||||||
|
|
||||||
import { store, workflowStore, backendUiStore } from "builderStore"
|
import { store, automationStore, backendUiStore } from "builderStore"
|
||||||
import { string, object } from "yup"
|
import { string, object } from "yup"
|
||||||
import api, { get } from "builderStore/api"
|
import api, { get } from "builderStore/api"
|
||||||
import Form from "@svelteschool/svelte-forms"
|
import Form from "@svelteschool/svelte-forms"
|
||||||
|
@ -133,7 +133,7 @@
|
||||||
if (applicationPkg.ok) {
|
if (applicationPkg.ok) {
|
||||||
backendUiStore.actions.reset()
|
backendUiStore.actions.reset()
|
||||||
await store.setPackage(pkg)
|
await store.setPackage(pkg)
|
||||||
workflowStore.actions.fetch()
|
automationStore.actions.fetch()
|
||||||
} else {
|
} else {
|
||||||
throw new Error(pkg)
|
throw new Error(pkg)
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,8 +35,6 @@
|
||||||
c => c._component === componentInstance._component
|
c => c._component === componentInstance._component
|
||||||
) || {}
|
) || {}
|
||||||
|
|
||||||
let panelDefinition = {}
|
|
||||||
|
|
||||||
$: panelDefinition =
|
$: panelDefinition =
|
||||||
componentPropDefinition.properties &&
|
componentPropDefinition.properties &&
|
||||||
componentPropDefinition.properties[selectedCategory.value]
|
componentPropDefinition.properties[selectedCategory.value]
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
import { find, map, keys, reduce, keyBy } from "lodash/fp"
|
import { find, map, keys, reduce, keyBy } from "lodash/fp"
|
||||||
import { pipe } from "components/common/core"
|
import { pipe } from "components/common/core"
|
||||||
import { EVENT_TYPE_MEMBER_NAME } from "components/common/eventHandlers"
|
import { EVENT_TYPE_MEMBER_NAME } from "components/common/eventHandlers"
|
||||||
import { store, workflowStore } from "builderStore"
|
import { store, automationStore } from "builderStore"
|
||||||
import { ArrowDownIcon } from "components/common/Icons/"
|
import { ArrowDownIcon } from "components/common/Icons/"
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
|
@ -18,14 +18,14 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="handler-option">
|
<div class="handler-option">
|
||||||
{#if parameter.name === 'workflow'}
|
{#if parameter.name === 'automation'}
|
||||||
<span>{parameter.name}</span>
|
<span>{parameter.name}</span>
|
||||||
{/if}
|
{/if}
|
||||||
{#if parameter.name === 'workflow'}
|
{#if parameter.name === 'automation'}
|
||||||
<Select on:change bind:value={parameter.value}>
|
<Select on:change bind:value={parameter.value}>
|
||||||
<option value="" />
|
<option value="" />
|
||||||
{#each $workflowStore.workflows.filter(wf => wf.live) as workflow}
|
{#each $automationStore.automations.filter(wf => wf.live) as automation}
|
||||||
<option value={workflow._id}>{workflow.name}</option>
|
<option value={automation._id}>{automation.name}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</Select>
|
</Select>
|
||||||
{:else if parameter.name === 'url'}
|
{: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)
|
let temp = runtimeToReadableBinding(bindableProperties, value)
|
||||||
|
|
||||||
return value === undefined && props.defaultValue !== undefined
|
return !value && props.defaultValue !== undefined
|
||||||
? props.defaultValue
|
? props.defaultValue
|
||||||
: temp
|
: temp
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import ModelViewSelect from "components/userInterface/ModelViewSelect.svelte"
|
||||||
import ModelViewFieldSelect from "components/userInterface/ModelViewFieldSelect.svelte"
|
import ModelViewFieldSelect from "components/userInterface/ModelViewFieldSelect.svelte"
|
||||||
import Event from "components/userInterface/EventsEditor/EventPropertyControl.svelte"
|
import Event from "components/userInterface/EventsEditor/EventPropertyControl.svelte"
|
||||||
import ScreenSelect from "components/userInterface/ScreenSelect.svelte"
|
import ScreenSelect from "components/userInterface/ScreenSelect.svelte"
|
||||||
|
import { IconSelect } from "components/userInterface/IconSelect"
|
||||||
import Colorpicker from "@budibase/colorpicker"
|
import Colorpicker from "@budibase/colorpicker"
|
||||||
|
|
||||||
import { all } from "./propertyCategories.js"
|
import { all } from "./propertyCategories.js"
|
||||||
|
@ -221,16 +222,41 @@ export default {
|
||||||
settings: [{ label: "URL", key: "url", control: Input }],
|
settings: [{ label: "URL", key: "url", control: Input }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// {
|
{
|
||||||
// _component: "@budibase/standard-components/icon",
|
_component: "@budibase/standard-components/icon",
|
||||||
// name: "Icon",
|
name: "Icon",
|
||||||
// description: "A basic component for displaying icons",
|
description: "A basic component for displaying icons",
|
||||||
// icon: "ri-sun-fill",
|
icon: "ri-sun-fill",
|
||||||
// children: [],
|
children: [],
|
||||||
// properties: {
|
properties: {
|
||||||
// design: { ...all },
|
design: {},
|
||||||
// },
|
settings: [
|
||||||
// },
|
{ label: "Icon", key: "icon", control: IconSelect },
|
||||||
|
{
|
||||||
|
label: "Size",
|
||||||
|
key: "size",
|
||||||
|
control: OptionSelect,
|
||||||
|
defaultValue: "fa-lg",
|
||||||
|
options: [
|
||||||
|
{ value: "fa-xs", label: "xs" },
|
||||||
|
{ value: "fa-sm", label: "sm" },
|
||||||
|
{ value: "fa-lg", label: "lg" },
|
||||||
|
{ value: "fa-2x", label: "2x" },
|
||||||
|
{ value: "fa-3x", label: "3x" },
|
||||||
|
{ value: "fa-5x", label: "5x" },
|
||||||
|
{ value: "fa-7x", label: "7x" },
|
||||||
|
{ value: "fa-10x", label: "10x" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Color",
|
||||||
|
key: "color",
|
||||||
|
control: Colorpicker,
|
||||||
|
defaultValue: "#000",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
_component: "@budibase/standard-components/link",
|
_component: "@budibase/standard-components/link",
|
||||||
name: "Link",
|
name: "Link",
|
||||||
|
|
|
@ -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: {
|
ATTACHMENT: {
|
||||||
// name: "File",
|
name: "Attachment",
|
||||||
// icon: "ri-image-line",
|
icon: "ri-file-line",
|
||||||
// type: "file",
|
type: "attachment",
|
||||||
// constraints: {
|
constraints: {
|
||||||
// type: "string",
|
type: "array",
|
||||||
// presence: { allowEmpty: true },
|
presence: { allowEmpty: true },
|
||||||
// },
|
},
|
||||||
// },
|
},
|
||||||
// FILE: {
|
|
||||||
// name: "Image",
|
|
||||||
// icon: "ri-file-line",
|
|
||||||
// type: "file",
|
|
||||||
// constraints: {
|
|
||||||
// type: "string",
|
|
||||||
// presence: { allowEmpty: true },
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// LINKED_FIELDS: {
|
// LINKED_FIELDS: {
|
||||||
// name: "Linked Fields",
|
// name: "Linked Fields",
|
||||||
// icon: "ri-link",
|
// 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>
|
<script>
|
||||||
import Modal from "svelte-simple-modal"
|
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 SettingsLink from "components/settings/Link.svelte"
|
||||||
import { get } from "builderStore/api"
|
import { get } from "builderStore/api"
|
||||||
|
|
||||||
|
@ -21,17 +21,17 @@
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
backendUiStore.actions.reset()
|
backendUiStore.actions.reset()
|
||||||
await store.setPackage(pkg)
|
await store.setPackage(pkg)
|
||||||
workflowStore.actions.fetch()
|
await automationStore.actions.fetch()
|
||||||
return pkg
|
return pkg
|
||||||
} else {
|
} else {
|
||||||
throw new Error(pkg)
|
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
|
// this remembers your last place on each of the sections
|
||||||
// e.g. if one of your screens is selected on front end, then
|
// 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
|
// brought back to the same screen
|
||||||
const topItemNavigate = path => () => {
|
const topItemNavigate = path => () => {
|
||||||
const activeTopNav = $layout.children.find(c => $isActive(c.path))
|
const activeTopNav = $layout.children.find(c => $isActive(c.path))
|
||||||
|
|
|
@ -1,18 +1,19 @@
|
||||||
|
<!-- routify:options index=3 -->
|
||||||
<script>
|
<script>
|
||||||
import { workflowStore } from "builderStore"
|
import { automationStore } from "builderStore"
|
||||||
import { WorkflowPanel, SetupPanel } from "components/workflow"
|
import { AutomationPanel, SetupPanel } from "components/automation"
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="root">
|
<div class="root">
|
||||||
<div class="nav">
|
<div class="nav">
|
||||||
<div class="inner">
|
<div class="inner">
|
||||||
<WorkflowPanel />
|
<AutomationPanel />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
{#if $workflowStore.selectedWorkflow}
|
{#if $automationStore.selectedAutomation}
|
||||||
<div class="nav">
|
<div class="nav">
|
||||||
<div class="inner">
|
<div class="inner">
|
||||||
<SetupPanel />
|
<SetupPanel />
|
|
@ -0,0 +1,5 @@
|
||||||
|
<script>
|
||||||
|
import { AutomationBuilder } from "components/automation"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AutomationBuilder />
|
|
@ -1,3 +1,4 @@
|
||||||
|
<!-- routify:options index=1 -->
|
||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import { store, backendUiStore } from "builderStore"
|
import { store, backendUiStore } from "builderStore"
|
||||||
|
|
|
@ -48,12 +48,14 @@
|
||||||
<Button secondary medium on:click={deployApp}>
|
<Button secondary medium on:click={deployApp}>
|
||||||
Deploy App
|
Deploy App
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<Spinner ratio={'0.5'} />
|
<Spinner size="10" />
|
||||||
{/if}
|
{/if}
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<img src="/_builder/assets/deploy-rocket.jpg" />
|
<img
|
||||||
|
src="/_builder/assets/deploy-rocket.jpg"
|
||||||
|
alt="Rocket flying through sky" />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
<!-- routify:options index=1 -->
|
||||||
<script>
|
<script>
|
||||||
import { store, backendUiStore } from "builderStore"
|
import { store, backendUiStore } from "builderStore"
|
||||||
import { goto } from "@sveltech/routify"
|
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"
|
lodash "^4.17.13"
|
||||||
to-fast-properties "^2.0.0"
|
to-fast-properties "^2.0.0"
|
||||||
|
|
||||||
"@budibase/bbui@^1.33.0":
|
"@budibase/bbui@^1.34.2":
|
||||||
version "1.33.0"
|
version "1.34.2"
|
||||||
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.33.0.tgz#216b24dd815f45880e9795e66b04848329b0390f"
|
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.34.2.tgz#e4fcc728dc8d51a918f8ebd5c3f0b0afacfa4047"
|
||||||
integrity sha512-Rrt5eLbea014TIfAbT40kP0D0AWNUi8Q0kDr3UZO6Aq4UXgjc0f53ZuJ7Kb66YRDWrqiucjf1FtvOUs3/YaD6g==
|
integrity sha512-6RusGPZnEpHx1gtGcjk/lFLgMgFdDpSIxB8v2MiA+kp+uP1pFlzegbaDh+/JXyqFwK7HO91I0yXXBoPjibi7Aw==
|
||||||
dependencies:
|
dependencies:
|
||||||
sirv-cli "^0.4.6"
|
sirv-cli "^0.4.6"
|
||||||
svelte-flatpickr "^2.4.0"
|
svelte-flatpickr "^2.4.0"
|
||||||
|
|
||||||
|
"@budibase/client@^0.1.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":
|
"@budibase/colorpicker@^1.0.1":
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@budibase/colorpicker/-/colorpicker-1.0.1.tgz#940c180e7ebba0cb0756c4c8ef13f5dfab58e810"
|
resolved "https://registry.yarnpkg.com/@budibase/colorpicker/-/colorpicker-1.0.1.tgz#940c180e7ebba0cb0756c4c8ef13f5dfab58e810"
|
||||||
|
@ -769,6 +778,11 @@
|
||||||
debug "^3.1.0"
|
debug "^3.1.0"
|
||||||
lodash.once "^4.1.1"
|
lodash.once "^4.1.1"
|
||||||
|
|
||||||
|
"@fortawesome/fontawesome-free@^5.14.0":
|
||||||
|
version "5.14.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.14.0.tgz#a371e91029ebf265015e64f81bfbf7d228c9681f"
|
||||||
|
integrity sha512-OfdMsF+ZQgdKHP9jUbmDcRrP0eX90XXrsXIdyjLbkmSBzmMXPABB8eobUJtivaupucYaByz6WNe1PI1JuYm3qA==
|
||||||
|
|
||||||
"@hapi/address@^2.1.2":
|
"@hapi/address@^2.1.2":
|
||||||
version "2.1.4"
|
version "2.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5"
|
resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5"
|
||||||
|
@ -1388,6 +1402,11 @@ array-equal@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93"
|
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:
|
array-union@^2.1.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
|
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"
|
version "2.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
|
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:
|
aws-sign2@~0.7.0:
|
||||||
version "0.7.0"
|
version "0.7.0"
|
||||||
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
|
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"
|
version "0.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
|
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:
|
deep-is@~0.1.3:
|
||||||
version "0.1.3"
|
version "0.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
|
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.trimleft "^2.1.1"
|
||||||
string.prototype.trimright "^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:
|
es-to-primitive@^1.2.1:
|
||||||
version "1.2.1"
|
version "1.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a"
|
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"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
|
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"
|
version "2.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99"
|
resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99"
|
||||||
integrity sha1-C+4AUBiusmDQo6865ljdATbsG5k=
|
integrity sha1-C+4AUBiusmDQo6865ljdATbsG5k=
|
||||||
|
@ -3240,16 +3334,31 @@ is-accessor-descriptor@^1.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
kind-of "^6.0.0"
|
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:
|
is-arrayish@^0.2.1:
|
||||||
version "0.2.1"
|
version "0.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
|
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:
|
is-binary-path@~2.1.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
|
resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
|
||||||
dependencies:
|
dependencies:
|
||||||
binary-extensions "^2.0.0"
|
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:
|
is-buffer@^1.1.5:
|
||||||
version "1.1.6"
|
version "1.1.6"
|
||||||
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
|
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"
|
version "1.1.5"
|
||||||
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.5.tgz#f7e46b596890456db74e7f6e976cb3273d06faab"
|
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:
|
is-ci@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c"
|
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:
|
dependencies:
|
||||||
kind-of "^6.0.0"
|
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"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e"
|
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"
|
global-dirs "^2.0.1"
|
||||||
is-path-inside "^3.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:
|
is-module@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591"
|
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:
|
is-number@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195"
|
resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195"
|
||||||
|
@ -3401,6 +3530,18 @@ is-regex@^1.0.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
has "^1.0.3"
|
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:
|
is-stream@^1.1.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
|
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"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3"
|
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:
|
is-symbol@^1.0.2:
|
||||||
version "1.0.3"
|
version "1.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937"
|
resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937"
|
||||||
dependencies:
|
dependencies:
|
||||||
has-symbols "^1.0.1"
|
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:
|
is-typedarray@~1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
|
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:
|
is-windows@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
|
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"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
|
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:
|
isbuffer@~0.0.0:
|
||||||
version "0.0.0"
|
version "0.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/isbuffer/-/isbuffer-0.0.0.tgz#38c146d9df528b8bf9b0701c3d43cf12df3fc39b"
|
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"
|
version "1.7.0"
|
||||||
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67"
|
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:
|
object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1:
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
|
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"
|
extend-shallow "^3.0.2"
|
||||||
safe-regex "^1.1.0"
|
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:
|
regexpu-core@^4.7.0:
|
||||||
version "4.7.0"
|
version "4.7.0"
|
||||||
resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.7.0.tgz#fcbf458c50431b0bb7b45d6967b8192d91f3d938"
|
resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.7.0.tgz#fcbf458c50431b0bb7b45d6967b8192d91f3d938"
|
||||||
|
@ -5475,6 +5672,14 @@ shortid@^2.2.15:
|
||||||
dependencies:
|
dependencies:
|
||||||
nanoid "^2.1.0"
|
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:
|
signal-exit@^3.0.0, signal-exit@^3.0.2:
|
||||||
version "3.0.3"
|
version "3.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
|
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"
|
is-fullwidth-code-point "^3.0.0"
|
||||||
strip-ansi "^6.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"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913"
|
resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -5724,7 +5929,7 @@ string.prototype.trimright@^2.1.1:
|
||||||
es-abstract "^1.17.5"
|
es-abstract "^1.17.5"
|
||||||
string.prototype.trimend "^1.0.0"
|
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"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz#14af6d9f34b053f7cfc89b72f8f2ee14b9039a54"
|
resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz#14af6d9f34b053f7cfc89b72f8f2ee14b9039a54"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -6179,10 +6384,43 @@ whatwg-url@^8.0.0:
|
||||||
tr46 "^2.0.2"
|
tr46 "^2.0.2"
|
||||||
webidl-conversions "^5.0.0"
|
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:
|
which-module@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
|
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:
|
which@^1.2.9, which@^1.3.0:
|
||||||
version "1.3.1"
|
version "1.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
|
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
|
||||||
|
|
|
@ -71,6 +71,7 @@
|
||||||
"pino-pretty": "^4.0.0",
|
"pino-pretty": "^4.0.0",
|
||||||
"pouchdb": "^7.2.1",
|
"pouchdb": "^7.2.1",
|
||||||
"pouchdb-all-dbs": "^1.0.2",
|
"pouchdb-all-dbs": "^1.0.2",
|
||||||
|
"sharp": "^0.26.0",
|
||||||
"squirrelly": "^7.5.0",
|
"squirrelly": "^7.5.0",
|
||||||
"tar-fs": "^2.1.0",
|
"tar-fs": "^2.1.0",
|
||||||
"uuid": "^3.3.2",
|
"uuid": "^3.3.2",
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
const CouchDB = require("../../db")
|
const CouchDB = require("../../db")
|
||||||
const newid = require("../../db/newid")
|
const newid = require("../../db/newid")
|
||||||
const actions = require("../../workflows/actions")
|
const actions = require("../../automations/actions")
|
||||||
const logic = require("../../workflows/logic")
|
const logic = require("../../automations/logic")
|
||||||
const triggers = require("../../workflows/triggers")
|
const triggers = require("../../automations/triggers")
|
||||||
|
|
||||||
/*************************
|
/*************************
|
||||||
* *
|
* *
|
||||||
|
@ -10,12 +10,12 @@ const triggers = require("../../workflows/triggers")
|
||||||
* *
|
* *
|
||||||
*************************/
|
*************************/
|
||||||
|
|
||||||
function cleanWorkflowInputs(workflow) {
|
function cleanAutomationInputs(automation) {
|
||||||
if (workflow == null) {
|
if (automation == null) {
|
||||||
return workflow
|
return automation
|
||||||
}
|
}
|
||||||
let steps = workflow.definition.steps
|
let steps = automation.definition.steps
|
||||||
let trigger = workflow.definition.trigger
|
let trigger = automation.definition.trigger
|
||||||
let allSteps = [...steps, trigger]
|
let allSteps = [...steps, trigger]
|
||||||
for (let step of allSteps) {
|
for (let step of allSteps) {
|
||||||
if (step == null) {
|
if (step == null) {
|
||||||
|
@ -27,25 +27,25 @@ function cleanWorkflowInputs(workflow) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return workflow
|
return automation
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.create = async function(ctx) {
|
exports.create = async function(ctx) {
|
||||||
const db = new CouchDB(ctx.user.instanceId)
|
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"
|
automation.type = "automation"
|
||||||
workflow = cleanWorkflowInputs(workflow)
|
automation = cleanAutomationInputs(automation)
|
||||||
const response = await db.post(workflow)
|
const response = await db.post(automation)
|
||||||
workflow._rev = response.rev
|
automation._rev = response.rev
|
||||||
|
|
||||||
ctx.status = 200
|
ctx.status = 200
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
message: "Workflow created successfully",
|
message: "Automation created successfully",
|
||||||
workflow: {
|
automation: {
|
||||||
...workflow,
|
...automation,
|
||||||
...response,
|
...response,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -53,17 +53,17 @@ exports.create = async function(ctx) {
|
||||||
|
|
||||||
exports.update = async function(ctx) {
|
exports.update = async function(ctx) {
|
||||||
const db = new CouchDB(ctx.user.instanceId)
|
const db = new CouchDB(ctx.user.instanceId)
|
||||||
let workflow = ctx.request.body
|
let automation = ctx.request.body
|
||||||
|
|
||||||
workflow = cleanWorkflowInputs(workflow)
|
automation = cleanAutomationInputs(automation)
|
||||||
const response = await db.put(workflow)
|
const response = await db.put(automation)
|
||||||
workflow._rev = response.rev
|
automation._rev = response.rev
|
||||||
|
|
||||||
ctx.status = 200
|
ctx.status = 200
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
message: `Workflow ${workflow._id} updated successfully.`,
|
message: `Automation ${automation._id} updated successfully.`,
|
||||||
workflow: {
|
automation: {
|
||||||
...workflow,
|
...automation,
|
||||||
_rev: response.rev,
|
_rev: response.rev,
|
||||||
_id: response.id,
|
_id: response.id,
|
||||||
},
|
},
|
||||||
|
@ -73,7 +73,7 @@ exports.update = async function(ctx) {
|
||||||
exports.fetch = async function(ctx) {
|
exports.fetch = async function(ctx) {
|
||||||
const db = new CouchDB(ctx.user.instanceId)
|
const db = new CouchDB(ctx.user.instanceId)
|
||||||
const response = await db.query(`database/by_type`, {
|
const response = await db.query(`database/by_type`, {
|
||||||
key: ["workflow"],
|
key: ["automation"],
|
||||||
include_docs: true,
|
include_docs: true,
|
||||||
})
|
})
|
||||||
ctx.body = response.rows.map(row => row.doc)
|
ctx.body = response.rows.map(row => row.doc)
|
||||||
|
@ -117,14 +117,14 @@ module.exports.getDefinitionList = async function(ctx) {
|
||||||
|
|
||||||
exports.trigger = async function(ctx) {
|
exports.trigger = async function(ctx) {
|
||||||
const db = new CouchDB(ctx.user.instanceId)
|
const db = new CouchDB(ctx.user.instanceId)
|
||||||
let workflow = await db.get(ctx.params.id)
|
let automation = await db.get(ctx.params.id)
|
||||||
await triggers.externalTrigger(workflow, {
|
await triggers.externalTrigger(automation, {
|
||||||
...ctx.request.body,
|
...ctx.request.body,
|
||||||
instanceId: ctx.user.instanceId,
|
instanceId: ctx.user.instanceId,
|
||||||
})
|
})
|
||||||
ctx.status = 200
|
ctx.status = 200
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
message: `Workflow ${workflow._id} has been triggered.`,
|
message: `Automation ${automation._id} has been triggered.`,
|
||||||
workflow,
|
automation,
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -2,6 +2,7 @@ const fs = require("fs")
|
||||||
const AWS = require("aws-sdk")
|
const AWS = require("aws-sdk")
|
||||||
const fetch = require("node-fetch")
|
const fetch = require("node-fetch")
|
||||||
const { budibaseAppsDir } = require("../../../utilities/budibaseDir")
|
const { budibaseAppsDir } = require("../../../utilities/budibaseDir")
|
||||||
|
const PouchDB = require("../../../db")
|
||||||
|
|
||||||
async function invalidateCDN(cfDistribution, appId) {
|
async function invalidateCDN(cfDistribution, appId) {
|
||||||
const cf = new AWS.CloudFront({})
|
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({
|
exports.uploadAppAssets = async function({
|
||||||
appId,
|
appId,
|
||||||
|
instanceId,
|
||||||
credentials,
|
credentials,
|
||||||
bucket,
|
bucket,
|
||||||
cfDistribution,
|
cfDistribution,
|
||||||
|
@ -86,30 +101,47 @@ exports.uploadAppAssets = async function({
|
||||||
|
|
||||||
const appPages = fs.readdirSync(appAssetsPath)
|
const appPages = fs.readdirSync(appAssetsPath)
|
||||||
|
|
||||||
const uploads = []
|
let uploads = []
|
||||||
|
|
||||||
for (let page of appPages) {
|
for (let page of appPages) {
|
||||||
walkDir(`${appAssetsPath}/${page}`, function prepareUploadsForS3(filePath) {
|
// Upload HTML, CSS and JS for each page of the web app
|
||||||
const fileExtension = [...filePath.split(".")].pop()
|
walkDir(`${appAssetsPath}/${page}`, function(filePath) {
|
||||||
const fileBytes = fs.readFileSync(filePath)
|
const appAssetUpload = prepareUploadForS3({
|
||||||
|
filePath,
|
||||||
const upload = s3
|
s3Key: filePath.replace(appAssetsPath, `assets/${appId}`),
|
||||||
.upload({
|
s3,
|
||||||
Key: filePath.replace(appAssetsPath, `assets/${appId}`),
|
metadata: { accountId },
|
||||||
Body: fileBytes,
|
})
|
||||||
ContentType: CONTENT_TYPE_MAP[fileExtension],
|
uploads.push(appAssetUpload)
|
||||||
Metadata: {
|
|
||||||
accountId,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.promise()
|
|
||||||
|
|
||||||
uploads.push(upload)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 {
|
try {
|
||||||
await Promise.all(uploads)
|
await Promise.all(uploads)
|
||||||
|
// TODO: update dynamoDB with a synopsis of the app deployment for historical purposes
|
||||||
await invalidateCDN(cfDistribution, appId)
|
await invalidateCDN(cfDistribution, appId)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error uploading budibase app assets to s3", err)
|
console.error("Error uploading budibase app assets to s3", err)
|
||||||
|
|
|
@ -42,6 +42,7 @@ exports.deployApp = async function(ctx) {
|
||||||
await uploadAppAssets({
|
await uploadAppAssets({
|
||||||
clientId,
|
clientId,
|
||||||
appId: ctx.user.appId,
|
appId: ctx.user.appId,
|
||||||
|
instanceId: ctx.user.instanceId,
|
||||||
...credentials,
|
...credentials,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -31,9 +31,9 @@ exports.create = async function(ctx) {
|
||||||
emit([doc.type], doc._id)
|
emit([doc.type], doc._id)
|
||||||
}.toString(),
|
}.toString(),
|
||||||
},
|
},
|
||||||
by_workflow_trigger: {
|
by_automation_trigger: {
|
||||||
map: function(doc) {
|
map: function(doc) {
|
||||||
if (doc.type === "workflow") {
|
if (doc.type === "automation") {
|
||||||
const trigger = doc.definition.trigger
|
const trigger = doc.definition.trigger
|
||||||
if (trigger) {
|
if (trigger) {
|
||||||
emit([trigger.event], 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.body = await db.remove(ctx.params.recordId, ctx.params.revId)
|
||||||
ctx.status = 200
|
ctx.status = 200
|
||||||
// for workflows
|
// for automations
|
||||||
ctx.record = record
|
ctx.record = record
|
||||||
emitEvent(`record:delete`, ctx, record)
|
emitEvent(`record:delete`, ctx, record)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,18 @@
|
||||||
const send = require("koa-send")
|
const send = require("koa-send")
|
||||||
const { resolve, join } = require("path")
|
const { resolve, join } = require("path")
|
||||||
|
const jwt = require("jsonwebtoken")
|
||||||
|
const fetch = require("node-fetch")
|
||||||
|
const fs = require("fs")
|
||||||
|
const uuid = require("uuid")
|
||||||
|
|
||||||
const {
|
const {
|
||||||
budibaseAppsDir,
|
budibaseAppsDir,
|
||||||
budibaseTempDir,
|
budibaseTempDir,
|
||||||
} = require("../../utilities/budibaseDir")
|
} = require("../../utilities/budibaseDir")
|
||||||
|
const CouchDB = require("../../db")
|
||||||
const setBuilderToken = require("../../utilities/builder/setBuilderToken")
|
const setBuilderToken = require("../../utilities/builder/setBuilderToken")
|
||||||
const { ANON_LEVEL_ID } = require("../../utilities/accessLevels")
|
const { ANON_LEVEL_ID } = require("../../utilities/accessLevels")
|
||||||
const jwt = require("jsonwebtoken")
|
const fileProcessor = require("../../utilities/fileProcessor")
|
||||||
const fetch = require("node-fetch")
|
|
||||||
|
|
||||||
exports.serveBuilder = async function(ctx) {
|
exports.serveBuilder = async function(ctx) {
|
||||||
let builderPath = resolve(__dirname, "../../../builder")
|
let builderPath = resolve(__dirname, "../../../builder")
|
||||||
|
@ -17,6 +22,66 @@ exports.serveBuilder = async function(ctx) {
|
||||||
await send(ctx, ctx.file, { root: ctx.devPath || builderPath })
|
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) {
|
exports.serveApp = async function(ctx) {
|
||||||
const mainOrAuth = ctx.isAuthenticated ? "main" : "unauthenticated"
|
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 })
|
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) {
|
exports.serveAppAsset = async function(ctx) {
|
||||||
// default to homedir
|
// default to homedir
|
||||||
const mainOrAuth = ctx.isAuthenticated ? "main" : "unauthenticated"
|
const mainOrAuth = ctx.isAuthenticated ? "main" : "unauthenticated"
|
||||||
|
|
|
@ -12,7 +12,7 @@ const controller = {
|
||||||
!name.startsWith("all") &&
|
!name.startsWith("all") &&
|
||||||
name !== "by_type" &&
|
name !== "by_type" &&
|
||||||
name !== "by_username" &&
|
name !== "by_username" &&
|
||||||
name !== "by_workflow_trigger"
|
name !== "by_automation_trigger"
|
||||||
) {
|
) {
|
||||||
response.push({
|
response.push({
|
||||||
name,
|
name,
|
||||||
|
|
|
@ -16,7 +16,7 @@ const {
|
||||||
viewRoutes,
|
viewRoutes,
|
||||||
staticRoutes,
|
staticRoutes,
|
||||||
componentRoutes,
|
componentRoutes,
|
||||||
workflowRoutes,
|
automationRoutes,
|
||||||
accesslevelRoutes,
|
accesslevelRoutes,
|
||||||
apiKeysRoutes,
|
apiKeysRoutes,
|
||||||
} = require("./routes")
|
} = require("./routes")
|
||||||
|
@ -84,8 +84,8 @@ router.use(userRoutes.allowedMethods())
|
||||||
router.use(instanceRoutes.routes())
|
router.use(instanceRoutes.routes())
|
||||||
router.use(instanceRoutes.allowedMethods())
|
router.use(instanceRoutes.allowedMethods())
|
||||||
|
|
||||||
router.use(workflowRoutes.routes())
|
router.use(automationRoutes.routes())
|
||||||
router.use(workflowRoutes.allowedMethods())
|
router.use(automationRoutes.allowedMethods())
|
||||||
|
|
||||||
router.use(deployRoutes.routes())
|
router.use(deployRoutes.routes())
|
||||||
router.use(deployRoutes.allowedMethods())
|
router.use(deployRoutes.allowedMethods())
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
const Router = require("@koa/router")
|
const Router = require("@koa/router")
|
||||||
const controller = require("../controllers/workflow")
|
const controller = require("../controllers/automation")
|
||||||
const authorized = require("../../middleware/authorized")
|
const authorized = require("../../middleware/authorized")
|
||||||
const joiValidator = require("../../middleware/joi-validator")
|
const joiValidator = require("../../middleware/joi-validator")
|
||||||
const { BUILDER } = require("../../utilities/accessLevels")
|
const { BUILDER } = require("../../utilities/accessLevels")
|
||||||
|
@ -30,7 +30,7 @@ function generateValidator(existing = false) {
|
||||||
_id: existing ? Joi.string().required() : Joi.string(),
|
_id: existing ? Joi.string().required() : Joi.string(),
|
||||||
_rev: existing ? Joi.string().required() : Joi.string(),
|
_rev: existing ? Joi.string().required() : Joi.string(),
|
||||||
name: Joi.string().required(),
|
name: Joi.string().required(),
|
||||||
type: Joi.string().valid("workflow").required(),
|
type: Joi.string().valid("automation").required(),
|
||||||
definition: Joi.object({
|
definition: Joi.object({
|
||||||
steps: Joi.array().required().items(generateStepSchema(["ACTION", "LOGIC"])),
|
steps: Joi.array().required().items(generateStepSchema(["ACTION", "LOGIC"])),
|
||||||
trigger: generateStepSchema(["TRIGGER"]),
|
trigger: generateStepSchema(["TRIGGER"]),
|
||||||
|
@ -40,40 +40,40 @@ function generateValidator(existing = false) {
|
||||||
|
|
||||||
router
|
router
|
||||||
.get(
|
.get(
|
||||||
"/api/workflows/trigger/list",
|
"/api/automations/trigger/list",
|
||||||
authorized(BUILDER),
|
authorized(BUILDER),
|
||||||
controller.getTriggerList
|
controller.getTriggerList
|
||||||
)
|
)
|
||||||
.get(
|
.get(
|
||||||
"/api/workflows/action/list",
|
"/api/automations/action/list",
|
||||||
authorized(BUILDER),
|
authorized(BUILDER),
|
||||||
controller.getActionList
|
controller.getActionList
|
||||||
)
|
)
|
||||||
.get(
|
.get(
|
||||||
"/api/workflows/logic/list",
|
"/api/automations/logic/list",
|
||||||
authorized(BUILDER),
|
authorized(BUILDER),
|
||||||
controller.getLogicList
|
controller.getLogicList
|
||||||
)
|
)
|
||||||
.get(
|
.get(
|
||||||
"/api/workflows/definitions/list",
|
"/api/automations/definitions/list",
|
||||||
authorized(BUILDER),
|
authorized(BUILDER),
|
||||||
controller.getDefinitionList
|
controller.getDefinitionList
|
||||||
)
|
)
|
||||||
.get("/api/workflows", authorized(BUILDER), controller.fetch)
|
.get("/api/automations", authorized(BUILDER), controller.fetch)
|
||||||
.get("/api/workflows/:id", authorized(BUILDER), controller.find)
|
.get("/api/automations/:id", authorized(BUILDER), controller.find)
|
||||||
.put(
|
.put(
|
||||||
"/api/workflows",
|
"/api/automations",
|
||||||
authorized(BUILDER),
|
authorized(BUILDER),
|
||||||
generateValidator(true),
|
generateValidator(true),
|
||||||
controller.update
|
controller.update
|
||||||
)
|
)
|
||||||
.post(
|
.post(
|
||||||
"/api/workflows",
|
"/api/automations",
|
||||||
authorized(BUILDER),
|
authorized(BUILDER),
|
||||||
generateValidator(false),
|
generateValidator(false),
|
||||||
controller.create
|
controller.create
|
||||||
)
|
)
|
||||||
.post("/api/workflows/:id/trigger", controller.trigger)
|
.post("/api/automations/:id/trigger", controller.trigger)
|
||||||
.delete("/api/workflows/:id/:rev", authorized(BUILDER), controller.destroy)
|
.delete("/api/automations/:id/:rev", authorized(BUILDER), controller.destroy)
|
||||||
|
|
||||||
module.exports = router
|
module.exports = router
|
|
@ -9,7 +9,7 @@ const recordRoutes = require("./record")
|
||||||
const viewRoutes = require("./view")
|
const viewRoutes = require("./view")
|
||||||
const staticRoutes = require("./static")
|
const staticRoutes = require("./static")
|
||||||
const componentRoutes = require("./component")
|
const componentRoutes = require("./component")
|
||||||
const workflowRoutes = require("./workflow")
|
const automationRoutes = require("./automation")
|
||||||
const accesslevelRoutes = require("./accesslevel")
|
const accesslevelRoutes = require("./accesslevel")
|
||||||
const deployRoutes = require("./deploy")
|
const deployRoutes = require("./deploy")
|
||||||
const apiKeysRoutes = require("./apikeys")
|
const apiKeysRoutes = require("./apikeys")
|
||||||
|
@ -27,7 +27,7 @@ module.exports = {
|
||||||
viewRoutes,
|
viewRoutes,
|
||||||
staticRoutes,
|
staticRoutes,
|
||||||
componentRoutes,
|
componentRoutes,
|
||||||
workflowRoutes,
|
automationRoutes,
|
||||||
accesslevelRoutes,
|
accesslevelRoutes,
|
||||||
apiKeysRoutes,
|
apiKeysRoutes,
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,8 @@ const Router = require("@koa/router")
|
||||||
const controller = require("../controllers/static")
|
const controller = require("../controllers/static")
|
||||||
const { budibaseTempDir } = require("../../utilities/budibaseDir")
|
const { budibaseTempDir } = require("../../utilities/budibaseDir")
|
||||||
const env = require("../../environment")
|
const env = require("../../environment")
|
||||||
|
const authorized = require("../../middleware/authorized")
|
||||||
|
const { BUILDER } = require("../../utilities/accessLevels")
|
||||||
|
|
||||||
const router = Router()
|
const router = Router()
|
||||||
|
|
||||||
|
@ -21,8 +23,14 @@ if (env.NODE_ENV !== "production") {
|
||||||
}
|
}
|
||||||
|
|
||||||
router
|
router
|
||||||
|
.post(
|
||||||
|
"/api/attachments/process",
|
||||||
|
authorized(BUILDER),
|
||||||
|
controller.processLocalFileUpload
|
||||||
|
)
|
||||||
.get("/componentlibrary", controller.serveComponentLibrary)
|
.get("/componentlibrary", controller.serveComponentLibrary)
|
||||||
.get("/assets/:file*", controller.serveAppAsset)
|
.get("/assets/:file*", controller.serveAppAsset)
|
||||||
|
.get("/attachments/:file*", controller.serveAttachment)
|
||||||
.get("/:appId/:path*", controller.serveApp)
|
.get("/:appId/:path*", controller.serveApp)
|
||||||
|
|
||||||
module.exports = router
|
module.exports = router
|
||||||
|
|
|
@ -13,9 +13,9 @@ const {
|
||||||
|
|
||||||
const { delay } = require("./testUtils")
|
const { delay } = require("./testUtils")
|
||||||
|
|
||||||
const TEST_WORKFLOW = {
|
const TEST_AUTOMATION = {
|
||||||
_id: "Test Workflow",
|
_id: "Test Automation",
|
||||||
name: "My Workflow",
|
name: "My Automation",
|
||||||
pageId: "123123123",
|
pageId: "123123123",
|
||||||
screenId: "kasdkfldsafkl",
|
screenId: "kasdkfldsafkl",
|
||||||
live: true,
|
live: true,
|
||||||
|
@ -27,20 +27,20 @@ const TEST_WORKFLOW = {
|
||||||
steps: [
|
steps: [
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
type: "workflow",
|
type: "automation",
|
||||||
}
|
}
|
||||||
|
|
||||||
let ACTION_DEFINITIONS = {}
|
let ACTION_DEFINITIONS = {}
|
||||||
let TRIGGER_DEFINITIONS = {}
|
let TRIGGER_DEFINITIONS = {}
|
||||||
let LOGIC_DEFINITIONS = {}
|
let LOGIC_DEFINITIONS = {}
|
||||||
|
|
||||||
describe("/workflows", () => {
|
describe("/automations", () => {
|
||||||
let request
|
let request
|
||||||
let server
|
let server
|
||||||
let app
|
let app
|
||||||
let instance
|
let instance
|
||||||
let workflow
|
let automation
|
||||||
let workflowId
|
let automationId
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
({ request, server } = await supertest())
|
({ request, server } = await supertest())
|
||||||
|
@ -49,7 +49,7 @@ describe("/workflows", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
if (workflow) await destroyDocument(workflow.id)
|
if (automation) await destroyDocument(automation.id)
|
||||||
instance = await createInstance(request, app._id)
|
instance = await createInstance(request, app._id)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -57,18 +57,18 @@ describe("/workflows", () => {
|
||||||
server.close()
|
server.close()
|
||||||
})
|
})
|
||||||
|
|
||||||
const createWorkflow = async () => {
|
const createAutomation = async () => {
|
||||||
workflow = await insertDocument(instance._id, {
|
automation = await insertDocument(instance._id, {
|
||||||
type: "workflow",
|
type: "automation",
|
||||||
...TEST_WORKFLOW
|
...TEST_AUTOMATION
|
||||||
})
|
})
|
||||||
workflow = { ...workflow, ...TEST_WORKFLOW }
|
automation = { ...automation, ...TEST_AUTOMATION }
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("get definitions", () => {
|
describe("get definitions", () => {
|
||||||
it("returns a list of definitions for actions", async () => {
|
it("returns a list of definitions for actions", async () => {
|
||||||
const res = await request
|
const res = await request
|
||||||
.get(`/api/workflows/action/list`)
|
.get(`/api/automations/action/list`)
|
||||||
.set(defaultHeaders(app._id, instance._id))
|
.set(defaultHeaders(app._id, instance._id))
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
|
@ -79,7 +79,7 @@ describe("/workflows", () => {
|
||||||
|
|
||||||
it("returns a list of definitions for triggers", async () => {
|
it("returns a list of definitions for triggers", async () => {
|
||||||
const res = await request
|
const res = await request
|
||||||
.get(`/api/workflows/trigger/list`)
|
.get(`/api/automations/trigger/list`)
|
||||||
.set(defaultHeaders(app._id, instance._id))
|
.set(defaultHeaders(app._id, instance._id))
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
|
@ -90,7 +90,7 @@ describe("/workflows", () => {
|
||||||
|
|
||||||
it("returns a list of definitions for actions", async () => {
|
it("returns a list of definitions for actions", async () => {
|
||||||
const res = await request
|
const res = await request
|
||||||
.get(`/api/workflows/logic/list`)
|
.get(`/api/automations/logic/list`)
|
||||||
.set(defaultHeaders(app._id, instance._id))
|
.set(defaultHeaders(app._id, instance._id))
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
|
@ -101,7 +101,7 @@ describe("/workflows", () => {
|
||||||
|
|
||||||
it("returns all of the definitions in one", async () => {
|
it("returns all of the definitions in one", async () => {
|
||||||
const res = await request
|
const res = await request
|
||||||
.get(`/api/workflows/definitions/list`)
|
.get(`/api/automations/definitions/list`)
|
||||||
.set(defaultHeaders(app._id, instance._id))
|
.set(defaultHeaders(app._id, instance._id))
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
|
@ -113,7 +113,7 @@ describe("/workflows", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("create", () => {
|
describe("create", () => {
|
||||||
it("should setup the workflow fully", () => {
|
it("should setup the automation fully", () => {
|
||||||
let trigger = TRIGGER_DEFINITIONS["RECORD_SAVED"]
|
let trigger = TRIGGER_DEFINITIONS["RECORD_SAVED"]
|
||||||
trigger.id = "wadiawdo34"
|
trigger.id = "wadiawdo34"
|
||||||
let saveAction = ACTION_DEFINITIONS["SAVE_RECORD"]
|
let saveAction = ACTION_DEFINITIONS["SAVE_RECORD"]
|
||||||
|
@ -123,51 +123,51 @@ describe("/workflows", () => {
|
||||||
}
|
}
|
||||||
saveAction.id = "awde444wk"
|
saveAction.id = "awde444wk"
|
||||||
|
|
||||||
TEST_WORKFLOW.definition.steps.push(saveAction)
|
TEST_AUTOMATION.definition.steps.push(saveAction)
|
||||||
TEST_WORKFLOW.definition.trigger = trigger
|
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
|
const res = await request
|
||||||
.post(`/api/workflows`)
|
.post(`/api/automations`)
|
||||||
.set(defaultHeaders(app._id, instance._id))
|
.set(defaultHeaders(app._id, instance._id))
|
||||||
.send(TEST_WORKFLOW)
|
.send(TEST_AUTOMATION)
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
|
|
||||||
expect(res.body.message).toEqual("Workflow created successfully")
|
expect(res.body.message).toEqual("Automation created successfully")
|
||||||
expect(res.body.workflow.name).toEqual("My Workflow")
|
expect(res.body.automation.name).toEqual("My Automation")
|
||||||
expect(res.body.workflow._id).not.toEqual(null)
|
expect(res.body.automation._id).not.toEqual(null)
|
||||||
workflowId = res.body.workflow._id
|
automationId = res.body.automation._id
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should apply authorization to endpoint", async () => {
|
it("should apply authorization to endpoint", async () => {
|
||||||
await builderEndpointShouldBlockNormalUsers({
|
await builderEndpointShouldBlockNormalUsers({
|
||||||
request,
|
request,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: `/api/workflows`,
|
url: `/api/automations`,
|
||||||
instanceId: instance._id,
|
instanceId: instance._id,
|
||||||
appId: app._id,
|
appId: app._id,
|
||||||
body: TEST_WORKFLOW
|
body: TEST_AUTOMATION
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("trigger", () => {
|
describe("trigger", () => {
|
||||||
it("trigger the workflow successfully", async () => {
|
it("trigger the automation successfully", async () => {
|
||||||
let model = await createModel(request, app._id, instance._id)
|
let model = await createModel(request, app._id, instance._id)
|
||||||
TEST_WORKFLOW.definition.trigger.inputs.modelId = model._id
|
TEST_AUTOMATION.definition.trigger.inputs.modelId = model._id
|
||||||
TEST_WORKFLOW.definition.steps[0].inputs.record.modelId = model._id
|
TEST_AUTOMATION.definition.steps[0].inputs.record.modelId = model._id
|
||||||
await createWorkflow()
|
await createAutomation()
|
||||||
const res = await request
|
const res = await request
|
||||||
.post(`/api/workflows/${workflow._id}/trigger`)
|
.post(`/api/automations/${automation._id}/trigger`)
|
||||||
.send({ name: "Test" })
|
.send({ name: "Test" })
|
||||||
.set(defaultHeaders(app._id, instance._id))
|
.set(defaultHeaders(app._id, instance._id))
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
expect(res.body.message).toEqual(`Workflow ${workflow._id} has been triggered.`)
|
expect(res.body.message).toEqual(`Automation ${automation._id} has been triggered.`)
|
||||||
expect(res.body.workflow.name).toEqual(TEST_WORKFLOW.name)
|
expect(res.body.automation.name).toEqual(TEST_AUTOMATION.name)
|
||||||
// wait for workflow to complete in background
|
// wait for automation to complete in background
|
||||||
await delay(500)
|
await delay(500)
|
||||||
let elements = await getAllFromModel(request, app._id, instance._id, model._id)
|
let elements = await getAllFromModel(request, app._id, instance._id, model._id)
|
||||||
expect(elements.length).toEqual(1)
|
expect(elements.length).toEqual(1)
|
||||||
|
@ -177,42 +177,42 @@ describe("/workflows", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("update", () => {
|
describe("update", () => {
|
||||||
it("updates a workflows data", async () => {
|
it("updates a automations data", async () => {
|
||||||
await createWorkflow()
|
await createAutomation()
|
||||||
workflow._id = workflow.id
|
automation._id = automation.id
|
||||||
workflow._rev = workflow.rev
|
automation._rev = automation.rev
|
||||||
workflow.name = "Updated Name"
|
automation.name = "Updated Name"
|
||||||
workflow.type = "workflow"
|
automation.type = "automation"
|
||||||
|
|
||||||
const res = await request
|
const res = await request
|
||||||
.put(`/api/workflows`)
|
.put(`/api/automations`)
|
||||||
.set(defaultHeaders(app._id, instance._id))
|
.set(defaultHeaders(app._id, instance._id))
|
||||||
.send(workflow)
|
.send(automation)
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
|
|
||||||
expect(res.body.message).toEqual("Workflow Test Workflow updated successfully.")
|
expect(res.body.message).toEqual("Automation Test Automation updated successfully.")
|
||||||
expect(res.body.workflow.name).toEqual("Updated Name")
|
expect(res.body.automation.name).toEqual("Updated Name")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("fetch", () => {
|
describe("fetch", () => {
|
||||||
it("return all the workflows for an instance", async () => {
|
it("return all the automations for an instance", async () => {
|
||||||
await createWorkflow()
|
await createAutomation()
|
||||||
const res = await request
|
const res = await request
|
||||||
.get(`/api/workflows`)
|
.get(`/api/automations`)
|
||||||
.set(defaultHeaders(app._id, instance._id))
|
.set(defaultHeaders(app._id, instance._id))
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(200)
|
.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 () => {
|
it("should apply authorization to endpoint", async () => {
|
||||||
await builderEndpointShouldBlockNormalUsers({
|
await builderEndpointShouldBlockNormalUsers({
|
||||||
request,
|
request,
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: `/api/workflows`,
|
url: `/api/automations`,
|
||||||
instanceId: instance._id,
|
instanceId: instance._id,
|
||||||
appId: app._id,
|
appId: app._id,
|
||||||
})
|
})
|
||||||
|
@ -220,23 +220,23 @@ describe("/workflows", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("destroy", () => {
|
describe("destroy", () => {
|
||||||
it("deletes a workflow by its ID", async () => {
|
it("deletes a automation by its ID", async () => {
|
||||||
await createWorkflow()
|
await createAutomation()
|
||||||
const res = await request
|
const res = await request
|
||||||
.delete(`/api/workflows/${workflow.id}/${workflow.rev}`)
|
.delete(`/api/automations/${automation.id}/${automation.rev}`)
|
||||||
.set(defaultHeaders(app._id, instance._id))
|
.set(defaultHeaders(app._id, instance._id))
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(200)
|
.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 () => {
|
it("should apply authorization to endpoint", async () => {
|
||||||
await createWorkflow()
|
await createAutomation()
|
||||||
await builderEndpointShouldBlockNormalUsers({
|
await builderEndpointShouldBlockNormalUsers({
|
||||||
request,
|
request,
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
url: `/api/workflows/${workflow.id}/${workflow._rev}`,
|
url: `/api/automations/${automation.id}/${automation._rev}`,
|
||||||
instanceId: instance._id,
|
instanceId: instance._id,
|
||||||
appId: app._id,
|
appId: app._id,
|
||||||
})
|
})
|
|
@ -6,7 +6,7 @@ const http = require("http")
|
||||||
const api = require("./api")
|
const api = require("./api")
|
||||||
const env = require("./environment")
|
const env = require("./environment")
|
||||||
const eventEmitter = require("./events")
|
const eventEmitter = require("./events")
|
||||||
const workflows = require("./workflows/index")
|
const automations = require("./automations/index")
|
||||||
const Sentry = require("@sentry/node")
|
const Sentry = require("@sentry/node")
|
||||||
|
|
||||||
const app = new Koa()
|
const app = new Koa()
|
||||||
|
@ -50,5 +50,5 @@ process.on("SIGINT", () => process.exit(1))
|
||||||
|
|
||||||
module.exports = server.listen(env.PORT || 4001, () => {
|
module.exports = server.listen(env.PORT || 4001, () => {
|
||||||
console.log(`Budibase running on ${JSON.stringify(server.address())}`)
|
console.log(`Budibase running on ${JSON.stringify(server.address())}`)
|
||||||
workflows.init()
|
automations.init()
|
||||||
})
|
})
|
||||||
|
|
|
@ -23,7 +23,7 @@ function runWorker(job) {
|
||||||
*/
|
*/
|
||||||
module.exports.init = function() {
|
module.exports.init = function() {
|
||||||
actions.init().then(() => {
|
actions.init().then(() => {
|
||||||
triggers.workflowQueue.process(async job => {
|
triggers.automationQueue.process(async job => {
|
||||||
if (environment.BUDIBASE_ENVIRONMENT === "PRODUCTION") {
|
if (environment.BUDIBASE_ENVIRONMENT === "PRODUCTION") {
|
||||||
await runWorker(job)
|
await runWorker(job)
|
||||||
} else {
|
} else {
|
|
@ -82,4 +82,4 @@ module.exports.run = async function({ inputs, instanceId }) {
|
||||||
response: err,
|
response: err,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -4,7 +4,7 @@ module.exports.definition = {
|
||||||
name: "Delay",
|
name: "Delay",
|
||||||
icon: "ri-time-fill",
|
icon: "ri-time-fill",
|
||||||
tagline: "Delay for {{inputs.time}} milliseconds",
|
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",
|
stepId: "DELAY",
|
||||||
inputs: {},
|
inputs: {},
|
||||||
schema: {
|
schema: {
|
|
@ -16,7 +16,7 @@ module.exports.definition = {
|
||||||
name: "Filter",
|
name: "Filter",
|
||||||
tagline: "{{inputs.field}} {{inputs.condition}} {{inputs.value}}",
|
tagline: "{{inputs.field}} {{inputs.condition}} {{inputs.value}}",
|
||||||
icon: "ri-git-branch-line",
|
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",
|
type: "LOGIC",
|
||||||
stepId: "FILTER",
|
stepId: "FILTER",
|
||||||
inputs: {
|
inputs: {
|
|
@ -28,6 +28,11 @@ module.exports.definition = {
|
||||||
},
|
},
|
||||||
outputs: {
|
outputs: {
|
||||||
properties: {
|
properties: {
|
||||||
|
record: {
|
||||||
|
type: "object",
|
||||||
|
customType: "record",
|
||||||
|
description: "The new record",
|
||||||
|
},
|
||||||
response: {
|
response: {
|
||||||
type: "object",
|
type: "object",
|
||||||
description: "The response from the table",
|
description: "The response from the table",
|
||||||
|
@ -69,6 +74,7 @@ module.exports.run = async function({ inputs, instanceId }) {
|
||||||
try {
|
try {
|
||||||
await recordController.save(ctx)
|
await recordController.save(ctx)
|
||||||
return {
|
return {
|
||||||
|
record: inputs.record,
|
||||||
response: ctx.body,
|
response: ctx.body,
|
||||||
id: ctx.body._id,
|
id: ctx.body._id,
|
||||||
revision: ctx.body._rev,
|
revision: ctx.body._rev,
|
|
@ -42,18 +42,18 @@ function recurseMustache(inputs, context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The workflow orchestrator is a class responsible for executing workflows.
|
* The automation orchestrator is a class responsible for executing automations.
|
||||||
* It handles the context of the workflow and makes sure each step gets the correct
|
* It handles the context of the automation and makes sure each step gets the correct
|
||||||
* inputs and handles any outputs.
|
* inputs and handles any outputs.
|
||||||
*/
|
*/
|
||||||
class Orchestrator {
|
class Orchestrator {
|
||||||
constructor(workflow, triggerOutput) {
|
constructor(automation, triggerOutput) {
|
||||||
this._instanceId = triggerOutput.instanceId
|
this._instanceId = triggerOutput.instanceId
|
||||||
// remove from context
|
// remove from context
|
||||||
delete triggerOutput.instanceId
|
delete triggerOutput.instanceId
|
||||||
// step zero is never used as the mustache is zero indexed for customer facing
|
// step zero is never used as the mustache is zero indexed for customer facing
|
||||||
this._context = { steps: [{}], trigger: triggerOutput }
|
this._context = { steps: [{}], trigger: triggerOutput }
|
||||||
this._workflow = workflow
|
this._automation = automation
|
||||||
}
|
}
|
||||||
|
|
||||||
async getStepFunctionality(type, stepId) {
|
async getStepFunctionality(type, stepId) {
|
||||||
|
@ -64,14 +64,14 @@ class Orchestrator {
|
||||||
step = logic.getLogic(stepId)
|
step = logic.getLogic(stepId)
|
||||||
}
|
}
|
||||||
if (step == null) {
|
if (step == null) {
|
||||||
throw `Cannot find workflow step by name ${stepId}`
|
throw `Cannot find automation step by name ${stepId}`
|
||||||
}
|
}
|
||||||
return step
|
return step
|
||||||
}
|
}
|
||||||
|
|
||||||
async execute() {
|
async execute() {
|
||||||
let workflow = this._workflow
|
let automation = this._automation
|
||||||
for (let step of workflow.definition.steps) {
|
for (let step of automation.definition.steps) {
|
||||||
let stepFn = await this.getStepFunctionality(step.type, step.stepId)
|
let stepFn = await this.getStepFunctionality(step.type, step.stepId)
|
||||||
step.inputs = recurseMustache(step.inputs, this._context)
|
step.inputs = recurseMustache(step.inputs, this._context)
|
||||||
// instanceId is always passed
|
// instanceId is always passed
|
||||||
|
@ -90,11 +90,11 @@ class Orchestrator {
|
||||||
// callback is required for worker-farm to state that the worker thread has completed
|
// callback is required for worker-farm to state that the worker thread has completed
|
||||||
module.exports = async (job, cb = null) => {
|
module.exports = async (job, cb = null) => {
|
||||||
try {
|
try {
|
||||||
const workflowOrchestrator = new Orchestrator(
|
const automationOrchestrator = new Orchestrator(
|
||||||
job.data.workflow,
|
job.data.automation,
|
||||||
job.data.event
|
job.data.event
|
||||||
)
|
)
|
||||||
await workflowOrchestrator.execute()
|
await automationOrchestrator.execute()
|
||||||
if (cb) {
|
if (cb) {
|
||||||
cb()
|
cb()
|
||||||
}
|
}
|
|
@ -2,7 +2,7 @@ const CouchDB = require("../db")
|
||||||
const emitter = require("../events/index")
|
const emitter = require("../events/index")
|
||||||
const InMemoryQueue = require("./queue/inMemoryQueue")
|
const InMemoryQueue = require("./queue/inMemoryQueue")
|
||||||
|
|
||||||
let workflowQueue = new InMemoryQueue()
|
let automationQueue = new InMemoryQueue()
|
||||||
|
|
||||||
const FAKE_STRING = "TEST"
|
const FAKE_STRING = "TEST"
|
||||||
const FAKE_BOOL = false
|
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) {
|
if (event.instanceId == null) {
|
||||||
throw `No instanceId specified for ${eventType} - check event emitters.`
|
throw `No instanceId specified for ${eventType} - check event emitters.`
|
||||||
}
|
}
|
||||||
const db = new CouchDB(event.instanceId)
|
const db = new CouchDB(event.instanceId)
|
||||||
const workflowsToTrigger = await db.query("database/by_workflow_trigger", {
|
const automationsToTrigger = await db.query(
|
||||||
key: [eventType],
|
"database/by_automation_trigger",
|
||||||
include_docs: true,
|
{
|
||||||
})
|
key: [eventType],
|
||||||
|
include_docs: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const workflows = workflowsToTrigger.rows.map(wf => wf.doc)
|
const automations = automationsToTrigger.rows.map(wf => wf.doc)
|
||||||
for (let workflow of workflows) {
|
for (let automation of automations) {
|
||||||
let workflowDef = workflow.definition
|
let automationDef = automation.definition
|
||||||
let workflowTrigger = workflowDef ? workflowDef.trigger : {}
|
let automationTrigger = automationDef ? automationDef.trigger : {}
|
||||||
if (
|
if (
|
||||||
!workflow.live ||
|
!automation.live ||
|
||||||
!workflowTrigger.inputs ||
|
!automationTrigger.inputs ||
|
||||||
workflowTrigger.inputs.modelId !== event.record.modelId
|
automationTrigger.inputs.modelId !== event.record.modelId
|
||||||
) {
|
) {
|
||||||
continue
|
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) {
|
if (!event || !event.record || !event.record.modelId) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await queueRelevantRecordWorkflows(event, "record:save")
|
await queueRelevantRecordAutomations(event, "record:save")
|
||||||
})
|
})
|
||||||
|
|
||||||
emitter.on("record:delete", async function(event) {
|
emitter.on("record:delete", async function(event) {
|
||||||
if (!event || !event.record || !event.record.modelId) {
|
if (!event || !event.record || !event.record.modelId) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await queueRelevantRecordWorkflows(event, "record:delete")
|
await queueRelevantRecordAutomations(event, "record:delete")
|
||||||
})
|
})
|
||||||
|
|
||||||
async function fillRecordOutput(workflow, params) {
|
async function fillRecordOutput(automation, params) {
|
||||||
let triggerSchema = workflow.definition.trigger
|
let triggerSchema = automation.definition.trigger
|
||||||
let modelId = triggerSchema.inputs.modelId
|
let modelId = triggerSchema.inputs.modelId
|
||||||
const db = new CouchDB(params.instanceId)
|
const db = new CouchDB(params.instanceId)
|
||||||
try {
|
try {
|
||||||
|
@ -147,19 +150,19 @@ async function fillRecordOutput(workflow, params) {
|
||||||
return 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
|
// TODO: replace this with allowing user in builder to input values in future
|
||||||
if (
|
if (
|
||||||
workflow.definition != null &&
|
automation.definition != null &&
|
||||||
workflow.definition.trigger != null &&
|
automation.definition.trigger != null &&
|
||||||
workflow.definition.trigger.inputs.modelId != 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
|
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
|
* 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.
|
* future.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
module.exports.READ_MODEL = "read-model"
|
module.exports.READ_MODEL = "read-model"
|
||||||
module.exports.WRITE_MODEL = "write-model"
|
module.exports.WRITE_MODEL = "write-model"
|
||||||
module.exports.READ_VIEW = "read-view"
|
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.USER_MANAGEMENT = "user-management"
|
||||||
module.exports.BUILDER = "builder"
|
module.exports.BUILDER = "builder"
|
||||||
module.exports.LIST_USERS = "list-users"
|
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
|
// to avoid circular dependencies this is included later, after exporting all enums
|
||||||
const permissions = require("./permissions")
|
const permissions = require("./permissions")
|
||||||
module.exports.generateAdminPermissions = permissions.generateAdminPermissions
|
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