Merge branch 'master' of https://github.com/Budibase/budibase into feature/icon-component

This commit is contained in:
Conor Mack 2020-09-15 13:44:44 +01:00
commit c49e906e5e
80 changed files with 1395 additions and 1092 deletions

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/builder", "name": "@budibase/builder",
"version": "0.1.19", "version": "0.1.21",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"private": true, "private": true,
"scripts": { "scripts": {
@ -63,8 +63,8 @@
} }
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "^1.32.0", "@budibase/bbui": "^1.33.0",
"@budibase/client": "^0.1.19", "@budibase/client": "^0.1.21",
"@budibase/colorpicker": "^1.0.1", "@budibase/colorpicker": "^1.0.1",
"@fortawesome/fontawesome-free": "^5.14.0", "@fortawesome/fontawesome-free": "^5.14.0",
"@sentry/browser": "5.19.1", "@sentry/browser": "5.19.1",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,7 +12,7 @@
<Button secondary small on:click={eventsModal.show}>Define Actions</Button> <Button secondary small on:click={eventsModal.show}>Define Actions</Button>
<Modal bind:this={eventsModal} maxWidth="100vw" hideCloseButton> <Modal bind:this={eventsModal} maxWidth="100vw" hideCloseButton padding="0">
<EventEditorModal <EventEditorModal
event={value} event={value}
eventType={name} eventType={name}

View File

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

View File

@ -1,5 +1,5 @@
<script> <script>
import { Select, Label } from "@budibase/bbui" import { DataList, Label } from "@budibase/bbui"
import { store } from "builderStore" import { store } from "builderStore"
export let parameters export let parameters
@ -7,12 +7,12 @@
<div class="root"> <div class="root">
<Label size="m" color="dark">Screen</Label> <Label size="m" color="dark">Screen</Label>
<Select secondary bind:value={parameters.url}> <DataList secondary bind:value={parameters.url}>
<option value="" /> <option value="" />
{#each $store.screens as screen} {#each $store.screens as screen}
<option value={screen.route}>{screen.props._instanceName}</option> <option value={screen.route}>{screen.props._instanceName}</option>
{/each} {/each}
</Select> </DataList>
</div> </div>
<style> <style>

View File

@ -1,6 +1,6 @@
<script> <script>
// accepts an array of field names, and outputs an object of { FieldName: value } // accepts an array of field names, and outputs an object of { FieldName: value }
import { Select, Label, TextButton, Spacer } from "@budibase/bbui" import { DataList, Label, TextButton, Spacer, Select } from "@budibase/bbui"
import { store, backendUiStore } from "builderStore" import { store, backendUiStore } from "builderStore"
import fetchBindableProperties from "builderStore/fetchBindableProperties" import fetchBindableProperties from "builderStore/fetchBindableProperties"
import { CloseCircleIcon, AddIcon } from "components/common/Icons" import { CloseCircleIcon, AddIcon } from "components/common/Icons"
@ -81,18 +81,14 @@
{/each} {/each}
</Select> </Select>
<Label size="m" color="dark">Value</Label> <Label size="m" color="dark">Value</Label>
<Select <DataList secondary bind:value={field.value} on:blur={rebuildParameters}>
editable
secondary
bind:value={field.value}
on:blur={rebuildParameters}>
<option value="" /> <option value="" />
{#each bindableProperties as bindableProp} {#each bindableProperties as bindableProp}
<option value={toBindingExpression(bindableProp.readableBinding)}> <option value={toBindingExpression(bindableProp.readableBinding)}>
{bindableProp.readableBinding} {bindableProp.readableBinding}
</option> </option>
{/each} {/each}
</Select> </DataList>
<div class="remove-field-container"> <div class="remove-field-container">
<TextButton text small on:click={removeField(field)}> <TextButton text small on:click={removeField(field)}>
<CloseCircleIcon /> <CloseCircleIcon />

View File

@ -93,7 +93,7 @@
{...props} {...props}
name={key} /> name={key} />
</div> </div>
{#if control == Input} {#if control === Input && !key.startsWith('_')}
<button data-cy={`${key}-binding-button`} on:click={dropdown.show}> <button data-cy={`${key}-binding-button`} on:click={dropdown.show}>
<Icon name="edit" /> <Icon name="edit" />
</button> </button>

View File

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

View File

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

View File

@ -7,9 +7,9 @@ import ModelViewFieldSelect from "components/userInterface/ModelViewFieldSelect.
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 { IconSelect } from "components/userInterface/IconSelect"
import Colorpicker from "@budibase/colorpicker"
import { all } from "./propertyCategories.js" import { all } from "./propertyCategories.js"
import Colorpicker from "@budibase/colorpicker"
/* /*
{ label: "N/A ", value: "N/A" }, { label: "N/A ", value: "N/A" },
{ label: "Flex", value: "flex" }, { label: "Flex", value: "flex" },
@ -542,10 +542,30 @@ export default {
key: "datasource", key: "datasource",
control: ModelViewSelect, control: ModelViewSelect,
}, },
{ label: "Stripe Color", key: "stripeColor", control: Input }, {
{ label: "Border Color", key: "borderColor", control: Input }, label: "Stripe Color",
{ label: "TH Color", key: "backgroundColor", control: Input }, key: "stripeColor",
{ label: "TH Font Color", key: "color", control: Input }, control: Colorpicker,
defaultValue: "#FFFFFF",
},
{
label: "Border Color",
key: "borderColor",
control: Colorpicker,
defaultValue: "#FFFFFF",
},
{
label: "TH Color",
key: "backgroundColor",
control: Colorpicker,
defaultValue: "#FFFFFF",
},
{
label: "TH Font Color",
key: "color",
control: Colorpicker,
defaultValue: "#FFFFFF",
},
{ label: "Table", key: "model", control: ModelSelect }, { label: "Table", key: "model", control: ModelSelect },
], ],
}, },

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Before

Width:  |  Height:  |  Size: 241 B

After

Width:  |  Height:  |  Size: 290 B

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -688,9 +688,10 @@
lodash "^4.17.13" lodash "^4.17.13"
to-fast-properties "^2.0.0" to-fast-properties "^2.0.0"
"@budibase/bbui@^1.32.0": "@budibase/bbui@^1.33.0":
version "1.32.0" version "1.33.0"
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.32.0.tgz#4b099e51cf8aebfc963a763bb9687994a2ee26a8" resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.33.0.tgz#216b24dd815f45880e9795e66b04848329b0390f"
integrity sha512-Rrt5eLbea014TIfAbT40kP0D0AWNUi8Q0kDr3UZO6Aq4UXgjc0f53ZuJ7Kb66YRDWrqiucjf1FtvOUs3/YaD6g==
dependencies: dependencies:
sirv-cli "^0.4.6" sirv-cli "^0.4.6"
svelte-flatpickr "^2.4.0" svelte-flatpickr "^2.4.0"

View File

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

View File

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

View File

@ -1,5 +1,4 @@
import { authenticate } from "./authenticate" import { authenticate } from "./authenticate"
import { triggerWorkflow } from "./workflow"
import appStore from "../state/store" import appStore from "../state/store"
const apiCall = method => async ({ url, body }) => { const apiCall = method => async ({ url, body }) => {
@ -96,7 +95,6 @@ const makeRecordRequestBody = parameters => {
export default { export default {
authenticate: authenticate(apiOpts), authenticate: authenticate(apiOpts),
triggerWorkflow: triggerWorkflow(apiOpts),
createRecord, createRecord,
updateRecord, updateRecord,
} }

View File

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

View File

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

View File

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

View File

@ -47,6 +47,7 @@ export const bbFactory = ({
setBinding: setBindableComponentProp(treeNode), setBinding: setBindableComponentProp(treeNode),
api, api,
parent, parent,
store: store.getStore(treeNode.contextStoreKey),
// these parameters are populated by screenRouter // these parameters are populated by screenRouter
routeParams: () => store.getState()["##routeParams"], routeParams: () => store.getState()["##routeParams"],
} }

View File

@ -1,4 +1,3 @@
import api from "../api"
import renderTemplateString from "./renderTemplateString" import renderTemplateString from "./renderTemplateString"
export const EVENT_TYPE_MEMBER_NAME = "##eventHandlerType" export const EVENT_TYPE_MEMBER_NAME = "##eventHandlerType"
@ -6,9 +5,6 @@ export const EVENT_TYPE_MEMBER_NAME = "##eventHandlerType"
export const eventHandlers = routeTo => { export const eventHandlers = routeTo => {
const handlers = { const handlers = {
"Navigate To": param => routeTo(param && param.url), "Navigate To": param => routeTo(param && param.url),
"Create Record": api.createRecord,
"Update Record": api.updateRecord,
"Trigger Workflow": api.triggerWorkflow,
} }
// when an event is called, this is what gets run // when an event is called, this is what gets run

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,6 +2,16 @@ const CouchDB = require("../../db")
const validateJs = require("validate.js") const validateJs = require("validate.js")
const newid = require("../../db/newid") const newid = require("../../db/newid")
function emitEvent(eventType, ctx, record) {
ctx.eventEmitter &&
ctx.eventEmitter.emit(eventType, {
args: {
record,
},
instanceId: ctx.user.instanceId,
})
}
validateJs.extend(validateJs.validators.datetime, { validateJs.extend(validateJs.validators.datetime, {
parse: function(value) { parse: function(value) {
return new Date(value).getTime() return new Date(value).getTime()
@ -110,13 +120,7 @@ exports.save = async function(ctx) {
} }
} }
ctx.eventEmitter && emitEvent(`record:save`, ctx, record)
ctx.eventEmitter.emit(`record:save`, {
args: {
record,
},
instanceId: ctx.user.instanceId,
})
ctx.body = record ctx.body = record
ctx.status = 200 ctx.status = 200
ctx.message = `${model.name} created successfully` ctx.message = `${model.name} created successfully`
@ -179,7 +183,7 @@ exports.destroy = async function(ctx) {
return return
} }
ctx.body = await db.remove(ctx.params.recordId, ctx.params.revId) ctx.body = await db.remove(ctx.params.recordId, ctx.params.revId)
ctx.eventEmitter && ctx.eventEmitter.emit(`record:delete`, record) emitEvent(`record:delete`, ctx, record)
} }
exports.validate = async function(ctx) { exports.validate = async function(ctx) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,33 +1,11 @@
const EventEmitter = require("events").EventEmitter const EventEmitter = require("events").EventEmitter
const CouchDB = require("../db")
const { Orchestrator, serverStrategy } = require("./workflow") /**
* keeping event emitter in one central location as it might be used for things other than
* workflows (what it was for originally) - having a central emitter will be useful in the
* future.
*/
const emitter = new EventEmitter() const emitter = new EventEmitter()
async function executeRelevantWorkflows(event, eventType) {
const db = new CouchDB(event.instanceId)
const workflowsToTrigger = await db.query("database/by_workflow_trigger", {
key: [eventType],
include_docs: true,
})
const workflows = workflowsToTrigger.rows.map(wf => wf.doc)
// Create orchestrator
const workflowOrchestrator = new Orchestrator()
workflowOrchestrator.strategy = serverStrategy
for (let workflow of workflows) {
workflowOrchestrator.execute(workflow, event)
}
}
emitter.on("record:save", async function(event) {
await executeRelevantWorkflows(event, "record:save")
})
emitter.on("record:delete", async function(event) {
await executeRelevantWorkflows(event, "record:delete")
})
module.exports = emitter module.exports = emitter

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,68 @@
const mustache = require("mustache")
const actions = require("./actions")
const logic = require("./logic")
/**
* The workflow orchestrator is a class responsible for executing workflows.
* It handles the context of the workflow and makes sure each step gets the correct
* inputs and handles any outputs.
*/
class Orchestrator {
constructor(workflow) {
this._context = {}
this._workflow = workflow
}
async getStep(type, stepId) {
let step = null
if (type === "ACTION") {
step = await actions.getAction(stepId)
} else if (type === "LOGIC") {
step = logic.getLogic(stepId)
}
if (step == null) {
throw `Cannot find workflow step by name ${stepId}`
}
return step
}
async execute(context) {
let workflow = this._workflow
for (let block of workflow.definition.steps) {
let step = await this.getStep(block.type, block.stepId)
let args = { ...block.args }
// bind the workflow action args to the workflow context, if required
for (let arg of Object.keys(args)) {
const argValue = args[arg]
// We don't want to render mustache templates on non-strings
if (typeof argValue !== "string") continue
args[arg] = mustache.render(argValue, { context: this._context })
}
const response = await step({
args,
context,
})
this._context = {
...this._context,
[block.id]: response,
}
}
}
}
// callback is required for worker-farm to state that the worker thread has completed
module.exports = async (job, cb = null) => {
try {
const workflowOrchestrator = new Orchestrator(job.data.workflow)
await workflowOrchestrator.execute(job.data.event)
if (cb) {
cb()
}
} catch (err) {
if (cb) {
cb(err)
}
}
}

View File

@ -0,0 +1,38 @@
const CouchDB = require("../db")
const emitter = require("../events/index")
const InMemoryQueue = require("./queue/inMemoryQueue")
let workflowQueue = new InMemoryQueue()
async function queueRelevantWorkflows(event, eventType) {
if (event.instanceId == null) {
throw `No instanceId specified for ${eventType} - check event emitters.`
}
const db = new CouchDB(event.instanceId)
const workflowsToTrigger = await db.query("database/by_workflow_trigger", {
key: [eventType],
include_docs: true,
})
const workflows = workflowsToTrigger.rows.map(wf => wf.doc)
for (let workflow of workflows) {
if (!workflow.live) {
continue
}
workflowQueue.add({ workflow, event })
}
}
emitter.on("record:save", async function(event) {
await queueRelevantWorkflows(event, "record:save")
})
emitter.on("record:delete", async function(event) {
await queueRelevantWorkflows(event, "record:delete")
})
module.exports.externalTrigger = async function(workflow, params) {
workflowQueue.add({ workflow, event: params })
}
module.exports.workflowQueue = workflowQueue

View File

@ -172,6 +172,21 @@
lodash "^4.17.13" lodash "^4.17.13"
to-fast-properties "^2.0.0" to-fast-properties "^2.0.0"
"@budibase/client@^0.1.19":
version "0.1.19"
resolved "https://registry.yarnpkg.com/@budibase/client/-/client-0.1.19.tgz#3906781423ab4626118c981657ecf7a4578c547c"
integrity sha512-crxnLgebsh6CR0aMleDahY/1vFPbveG6JuSS/EVZeoBmckzK8hwiUQYQhIlf68nZfzWsCE/M9TX7SJxsrKY3bQ==
dependencies:
"@nx-js/compiler-util" "^2.0.0"
bcryptjs "^2.4.3"
deep-equal "^2.0.1"
lodash "^4.17.15"
lunr "^2.3.5"
mustache "^4.0.1"
regexparam "^1.3.0"
shortid "^2.2.8"
svelte "^3.9.2"
"@cnakazawa/watch@^1.0.3": "@cnakazawa/watch@^1.0.3":
version "1.0.4" version "1.0.4"
resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.4.tgz#f864ae85004d0fcab6f50be9141c4da368d1656a" resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.4.tgz#f864ae85004d0fcab6f50be9141c4da368d1656a"
@ -201,10 +216,39 @@
global-agent "^2.0.2" global-agent "^2.0.2"
global-tunnel-ng "^2.7.1" global-tunnel-ng "^2.7.1"
"@hapi/address@^4.1.0":
version "4.1.0"
resolved "https://registry.yarnpkg.com/@hapi/address/-/address-4.1.0.tgz#d60c5c0d930e77456fdcde2598e77302e2955e1d"
integrity sha512-SkszZf13HVgGmChdHo/PxchnSaCJ6cetVqLzyciudzZRT0jcOouIF/Q93mgjw8cce+D+4F4C1Z/WrfFN+O3VHQ==
dependencies:
"@hapi/hoek" "^9.0.0"
"@hapi/bourne@^2.0.0": "@hapi/bourne@^2.0.0":
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/@hapi/bourne/-/bourne-2.0.0.tgz#5bb2193eb685c0007540ca61d166d4e1edaf918d" resolved "https://registry.yarnpkg.com/@hapi/bourne/-/bourne-2.0.0.tgz#5bb2193eb685c0007540ca61d166d4e1edaf918d"
"@hapi/formula@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@hapi/formula/-/formula-2.0.0.tgz#edade0619ed58c8e4f164f233cda70211e787128"
integrity sha512-V87P8fv7PI0LH7LiVi8Lkf3x+KCO7pQozXRssAHNXXL9L1K+uyu4XypLXwxqVDKgyQai6qj3/KteNlrqDx4W5A==
"@hapi/hoek@^9.0.0":
version "9.1.0"
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.1.0.tgz#6c9eafc78c1529248f8f4d92b0799a712b6052c6"
integrity sha512-i9YbZPN3QgfighY/1X1Pu118VUz2Fmmhd6b2n0/O8YVgGGfw0FbUYoA97k7FkpGJ+pLCFEDLUmAPPV4D1kpeFw==
"@hapi/pinpoint@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@hapi/pinpoint/-/pinpoint-2.0.0.tgz#805b40d4dbec04fc116a73089494e00f073de8df"
integrity sha512-vzXR5MY7n4XeIvLpfl3HtE3coZYO4raKXW766R6DZw/6aLqR26iuZ109K7a0NtF2Db0jxqh7xz2AxkUwpUFybw==
"@hapi/topo@^5.0.0":
version "5.0.0"
resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-5.0.0.tgz#c19af8577fa393a06e9c77b60995af959be721e7"
integrity sha512-tFJlT47db0kMqVm3H4nQYgn6Pwg10GTZHb1pwmSiv1K4ks6drQOtfEF5ZnPjkvC+y4/bUPHK+bc87QvLcL+WMw==
dependencies:
"@hapi/hoek" "^9.0.0"
"@jest/console@^24.7.1", "@jest/console@^24.9.0": "@jest/console@^24.7.1", "@jest/console@^24.9.0":
version "24.9.0" version "24.9.0"
resolved "https://registry.yarnpkg.com/@jest/console/-/console-24.9.0.tgz#79b1bc06fb74a8cfb01cbdedf945584b1b9707f0" resolved "https://registry.yarnpkg.com/@jest/console/-/console-24.9.0.tgz#79b1bc06fb74a8cfb01cbdedf945584b1b9707f0"
@ -354,6 +398,11 @@
path-to-regexp "1.x" path-to-regexp "1.x"
urijs "^1.19.2" urijs "^1.19.2"
"@nx-js/compiler-util@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@nx-js/compiler-util/-/compiler-util-2.0.0.tgz#c74c12165fa2f017a292bb79af007e8fce0af297"
integrity sha512-AxSQbwj9zqt8DYPZ6LwZdytqnwfiOEdcFdq4l8sdjkZmU2clTht7RDLCI8xvkp7KqgcNaOGlTeCM55TULWruyQ==
"@sendgrid/client@^7.1.1": "@sendgrid/client@^7.1.1":
version "7.1.1" version "7.1.1"
resolved "https://registry.yarnpkg.com/@sendgrid/client/-/client-7.1.1.tgz#09a25e58ac7e5321d66807e7110ff0fb61bb534f" resolved "https://registry.yarnpkg.com/@sendgrid/client/-/client-7.1.1.tgz#09a25e58ac7e5321d66807e7110ff0fb61bb534f"
@ -811,6 +860,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-unique@^0.3.2: array-unique@^0.3.2:
version "0.3.2" version "0.3.2"
resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
@ -869,6 +923,13 @@ atomic-sleep@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b" resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b"
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-sdk@^2.706.0: aws-sdk@^2.706.0:
version "2.706.0" version "2.706.0"
resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.706.0.tgz#09f65e9a91ecac5a635daf934082abae30eca953" resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.706.0.tgz#09f65e9a91ecac5a635daf934082abae30eca953"
@ -1537,6 +1598,26 @@ decompress-response@^3.3.0:
dependencies: dependencies:
mimic-response "^1.0.0" mimic-response "^1.0.0"
deep-equal@^2.0.1:
version "2.0.3"
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.0.3.tgz#cad1c15277ad78a5c01c49c2dee0f54de8a6a7b0"
integrity sha512-Spqdl4H+ky45I9ByyJtXteOm9CaIrPmnIPmOhrkKGNYWeDgCvJ8jNYVCTjChxW4FqGuZnLHADc8EKRMX6+CgvA==
dependencies:
es-abstract "^1.17.5"
es-get-iterator "^1.1.0"
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-equal@~1.0.1: deep-equal@~1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
@ -1831,7 +1912,7 @@ env-paths@^2.2.0:
version "2.2.0" version "2.2.0"
resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.0.tgz#cdca557dc009152917d6166e2febe1f039685e43" resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.0.tgz#cdca557dc009152917d6166e2febe1f039685e43"
errno@~0.1.1: errno@~0.1.1, errno@~0.1.7:
version "0.1.7" version "0.1.7"
resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618" resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618"
dependencies: dependencies:
@ -1863,6 +1944,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"
@ -2753,16 +2882,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"
@ -2771,6 +2915,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"
@ -2793,7 +2942,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"
@ -2856,10 +3005,25 @@ is-installed-globally@^0.3.1:
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-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-npm@^4.0.0: is-npm@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-4.0.0.tgz#c90dd8380696df87a7a6d823c20d0b12bbe3c84d" resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-4.0.0.tgz#c90dd8380696df87a7a6d823c20d0b12bbe3c84d"
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"
@ -2890,10 +3054,27 @@ 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"
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"
@ -2908,10 +3089,30 @@ is-type-of@^1.0.0:
is-class-hotfix "~0.0.6" is-class-hotfix "~0.0.6"
isstream "~0.1.2" isstream "~0.1.2"
is-typed-array@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.3.tgz#a4ff5a5e672e1a55f99c7f54e59597af5c1df04d"
integrity sha512-BSYUBOK/HJibQ30wWkWold5txYwMUXQct9YHAQJr8fSwvZoiglcqB0pd7vEN23+Tsi9IUEjztdOSzl4qLVYGTQ==
dependencies:
available-typed-arrays "^1.0.0"
es-abstract "^1.17.4"
foreach "^2.0.5"
has-symbols "^1.0.1"
is-typedarray@^1.0.0, is-typedarray@~1.0.0: 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"
@ -2932,6 +3133,11 @@ isarray@1.0.0, 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==
isbinaryfile@^4.0.6: isbinaryfile@^4.0.6:
version "4.0.6" version "4.0.6"
resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.6.tgz#edcb62b224e2b4710830b67498c8e4e5a4d2610b" resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.6.tgz#edcb62b224e2b4710830b67498c8e4e5a4d2610b"
@ -3333,6 +3539,17 @@ jmespath@0.15.0, jmespath@^0.15.0:
version "0.15.0" version "0.15.0"
resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.15.0.tgz#a3f222a9aae9f966f5d27c796510e28091764217" resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.15.0.tgz#a3f222a9aae9f966f5d27c796510e28091764217"
joi@^17.2.1:
version "17.2.1"
resolved "https://registry.yarnpkg.com/joi/-/joi-17.2.1.tgz#e5140fdf07e8fecf9bc977c2832d1bdb1e3f2a0a"
integrity sha512-YT3/4Ln+5YRpacdmfEfrrKh50/kkgX3LgBltjqnlMPIYiZ4hxXZuVJcxmsvxsdeHg9soZfE3qXxHC2tMpCCBOA==
dependencies:
"@hapi/address" "^4.1.0"
"@hapi/formula" "^2.0.0"
"@hapi/hoek" "^9.0.0"
"@hapi/pinpoint" "^2.0.0"
"@hapi/topo" "^5.0.0"
joycon@^2.2.5: joycon@^2.2.5:
version "2.2.5" version "2.2.5"
resolved "https://registry.yarnpkg.com/joycon/-/joycon-2.2.5.tgz#8d4cf4cbb2544d7b7583c216fcdfec19f6be1615" resolved "https://registry.yarnpkg.com/joycon/-/joycon-2.2.5.tgz#8d4cf4cbb2544d7b7583c216fcdfec19f6be1615"
@ -3871,6 +4088,11 @@ ltgt@~2.1.3:
version "2.1.3" version "2.1.3"
resolved "https://registry.yarnpkg.com/ltgt/-/ltgt-2.1.3.tgz#10851a06d9964b971178441c23c9e52698eece34" resolved "https://registry.yarnpkg.com/ltgt/-/ltgt-2.1.3.tgz#10851a06d9964b971178441c23c9e52698eece34"
lunr@^2.3.5:
version "2.3.9"
resolved "https://registry.yarnpkg.com/lunr/-/lunr-2.3.9.tgz#18b123142832337dd6e964df1a5a7707b25d35e1"
integrity sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==
make-dir@^2.1.0: make-dir@^2.1.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5"
@ -4035,6 +4257,11 @@ nan@^2.12.1:
version "2.14.0" version "2.14.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c"
nanoid@^2.1.0:
version "2.1.11"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-2.1.11.tgz#ec24b8a758d591561531b4176a01e3ab4f0f0280"
integrity sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA==
nanomatch@^1.2.9: nanomatch@^1.2.9:
version "1.2.13" version "1.2.13"
resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
@ -4182,6 +4409,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.0.6, object-keys@^1.1.1: object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.0.6, 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"
@ -4828,6 +5068,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==
regexpp@^2.0.1: regexpp@^2.0.1:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f"
@ -5123,6 +5376,21 @@ shellwords@^0.1.1:
version "0.1.1" version "0.1.1"
resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b"
shortid@^2.2.8:
version "2.2.15"
resolved "https://registry.yarnpkg.com/shortid/-/shortid-2.2.15.tgz#2b902eaa93a69b11120373cd42a1f1fe4437c122"
integrity sha512-5EaCy2mx2Jgc/Fdn9uuDuNIIfWBpzY4XIlhoqtXF6qsf+/+SGZ+FxDdX/ZsMZiWupIWNqAEmiNY4RC+LSmCeOw==
dependencies:
nanoid "^2.1.0"
side-channel@^1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.3.tgz#cdc46b057550bbab63706210838df5d4c19519c3"
integrity sha512-A6+ByhlLkksFoUepsGxfj5x1gTSrs+OydsRptUxeNCabQpCFUvcwIczgOigI8vhY/OJCnPnyE9rGiwgvr9cS1g==
dependencies:
es-abstract "^1.18.0-next.0"
object-inspect "^1.8.0"
signal-exit@^3.0.0, signal-exit@^3.0.2: 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"
@ -5339,7 +5607,7 @@ string-width@^4.0.0, string-width@^4.1.0, 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:
@ -5362,7 +5630,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:
@ -5480,6 +5748,11 @@ supports-color@^7.1.0:
dependencies: dependencies:
has-flag "^4.0.0" has-flag "^4.0.0"
svelte@^3.9.2:
version "3.24.1"
resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.24.1.tgz#aca364937dd1df27fe131e2a4c234acb6061db4b"
integrity sha512-OX/IBVUJSFo1rnznXdwf9rv6LReJ3qQ0PwRjj76vfUWyTfbHbR9OXqJBnUrpjyis2dwYcbT2Zm1DFjOOF1ZbbQ==
symbol-tree@^3.2.2: symbol-tree@^3.2.2:
version "3.2.4" version "3.2.4"
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
@ -5912,10 +6185,43 @@ whatwg-url@^7.0.0:
tr46 "^1.0.1" tr46 "^1.0.1"
webidl-conversions "^4.0.2" webidl-conversions "^4.0.2"
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"
@ -5932,6 +6238,13 @@ word-wrap@~1.2.3:
version "1.2.3" version "1.2.3"
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
worker-farm@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.7.0.tgz#26a94c5391bbca926152002f69b84a4bf772e5a8"
integrity sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==
dependencies:
errno "~0.1.7"
wrap-ansi@^5.1.0: wrap-ansi@^5.1.0:
version "5.1.0" version "5.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09"

File diff suppressed because one or more lines are too long

View File

@ -13,7 +13,7 @@
"dev:builder": "rollup -cw" "dev:builder": "rollup -cw"
}, },
"devDependencies": { "devDependencies": {
"@budibase/client": "^0.1.19", "@budibase/client": "^0.1.21",
"@rollup/plugin-commonjs": "^11.1.0", "@rollup/plugin-commonjs": "^11.1.0",
"lodash": "^4.17.15", "lodash": "^4.17.15",
"rollup": "^1.11.0", "rollup": "^1.11.0",
@ -31,7 +31,7 @@
"keywords": [ "keywords": [
"svelte" "svelte"
], ],
"version": "0.1.19", "version": "0.1.21",
"license": "MIT", "license": "MIT",
"gitHead": "284cceb9b703c38566c6e6363c022f79a08d5691", "gitHead": "284cceb9b703c38566c6e6363c022f79a08d5691",
"dependencies": { "dependencies": {

View File

@ -41,7 +41,6 @@
([field, message]) => `${field} ${message}` ([field, message]) => `${field} ${message}`
) )
async function fetchModel() { async function fetchModel() {
const FETCH_MODEL_URL = `/api/models/${model}` const FETCH_MODEL_URL = `/api/models/${model}`
const response = await _bb.api.get(FETCH_MODEL_URL) const response = await _bb.api.get(FETCH_MODEL_URL)

View File

@ -1,3 +1,5 @@
import "@budibase/bbui/dist/bbui.css"
export { default as container } from "./Container.svelte" export { default as container } from "./Container.svelte"
export { default as text } from "./Text.svelte" export { default as text } from "./Text.svelte"
export { default as heading } from "./Heading.svelte" export { default as heading } from "./Heading.svelte"