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(() => {
cy.server()
cy.visit("localhost:4001/_builder")
before(() => { cy.createApp(
cy.server() "Workflow Test App",
cy.visit('localhost:4001/_builder') "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
}) it("should create a workflow", () => {
cy.createTestTableWithData()
// https://on.cypress.io/interacting-with-elements cy.contains("workflow").click()
it('should create a workflow', () => { cy.contains("Create New Workflow").click()
cy.createTestTableWithData() cy.get("input").type("Add Record")
cy.contains("Save").click()
cy.contains('workflow').click() // Add trigger
cy.contains('Create New Workflow').click() cy.get("[data-cy=add-workflow-component]").click()
cy.get('input').type('Add Record') cy.get("[data-cy=RECORD_SAVED]").click()
cy.contains('Save').click() cy.get(".budibase__input").select("dog")
// Add trigger // Create action
cy.get('[data-cy=add-workflow-component]').click() cy.get("[data-cy=SAVE_RECORD]").click()
cy.get('[data-cy=RECORD_SAVED]').click() cy.get(".budibase__input").select("dog")
cy.get('.budibase__input').select('dog') cy.get(".container input")
.first()
.type("goodboy")
cy.get(".container input")
.eq(1)
.type("11")
// Create action // Save
cy.get('[data-cy=SAVE_RECORD]').click() cy.contains("Save Workflow").click()
cy.get('.container input').first().type('goodboy')
cy.get('.container input').eq(1).type('11')
// Save // Activate Workflow
cy.contains('Save Workflow').click() cy.get("[data-cy=activate-workflow]").click()
cy.contains("Add Record").should("be.visible")
cy.get(".stop-button.highlighted").should("be.visible")
})
// Activate Workflow it("should add record when a new record is added", () => {
cy.get('[data-cy=activate-workflow]').click() cy.contains("backend").click()
cy.contains("Add Record").should("be.visible")
cy.get(".stop-button.highlighted").should("be.visible")
})
it('should add record when a new record is added', () => { cy.addRecord(["Rover", 15])
cy.contains('backend').click() cy.reload()
cy.contains("goodboy").should("have.text", "goodboy")
cy.addRecord(["Rover", 15]) })
cy.reload() })
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,57 +1,48 @@
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)
}) })
it("deletes a workflow block successfully", () => { it("deletes a workflow block successfully", () => {
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>
<Input {#if filter.key && isMultipleChoice(filter.key)}
thin <Select secondary thin bind:value={filter.value}>
placeholder={filter.key || fields[0]} {#each viewModel.schema[filter.key].constraints.inclusion as option}
bind:value={filter.value} /> <option value={option}>{option}</option>
{/each}
</Select>
{:else}
<Input
thin
placeholder={filter.key || fields[0]}
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,38 +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} <div class="buttons">
<button <Button green wide data-cy="save-workflow-setup" on:click={saveWorkflow}>
transition:fade Save Workflow
class:passed={testResult === 'PASSED'} </Button>
class:failed={testResult === 'FAILED'} <Button red wide on:click={deleteWorkflowBlock}>Delete Block</Button>
class="test-result">
{testResult}
</button>
{/if}
<Button secondary wide on:click={testWorkflow}>Test Workflow</Button>
</div> </div>
{/if} {:else if $workflowStore.selectedWorkflow}
{#if selectedTab === 'SETUP'} <div class="panel">
{#if workflowBlock} <div class="panel-body">
<WorkflowBlockSetup {workflowBlock} /> <div class="block-label">
Workflow
<b>{workflow.name}</b>
</div>
</div>
<Button secondary wide on:click={testWorkflow}>Test Workflow</Button>
<div class="buttons"> <div class="buttons">
<Button <Button
green green
@ -105,40 +79,9 @@
on:click={saveWorkflow}> on:click={saveWorkflow}>
Save Workflow Save Workflow
</Button> </Button>
<Button red wide on:click={deleteWorkflowBlock}>Delete Block</Button> <Button red wide on:click={deleteWorkflow}>Delete Workflow</Button>
</div> </div>
{:else if $workflowStore.currentWorkflow} </div>
<div class="panel">
<div class="panel-body">
<div class="block-label">Workflow: {workflow.name}</div>
<div class="config-item">
<Label small forAttr={'useraccess'}>User Access</Label>
<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 class="buttons">
<Button
green
wide
data-cy="save-workflow-setup"
on:click={saveWorkflow}>
Save Workflow
</Button>
<Button red wide on:click={deleteWorkflow}>Delete Workflow</Button>
</div>
</div>
{/if}
{/if} {/if}
</section> </section>
@ -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>
<div class="block-field"> {#each params as [parameter, type]}
<label class="label">{parameter}</label> <div class="block-field">
{#if Array.isArray(type)} <label class="label">{parameter}</label>
<Select bind:value={workflowBlock.args[parameter]} thin> {#if Array.isArray(type)}
{#each type as option} <Select bind:value={block.args[parameter]} thin secondary>
<option value={option}>{option}</option> <option value="">Choose an option</option>
{/each} {#each type as option}
</Select> <option value={option}>{option}</option>
{:else if type === 'component'} {/each}
<ComponentSelector bind:value={workflowBlock.args[parameter]} /> </Select>
{: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" {:else if type === 'model'}
thin <ModelSelector bind:value={block.args[parameter]} />
bind:value={workflowBlock.args[parameter]} {:else if type === 'record'}
label="" /> <RecordSelector bind:value={block.args[parameter]} />
{:else if type === 'model'} {:else if type === 'string'}
<ModelSelector bind:value={workflowBlock.args[parameter]} /> <Input type="text" thin bind:value={block.args[parameter]} />
{:else if type === 'record'} {/if}
<RecordSelector value={workflowBlock.args[parameter]} /> </div>
{:else if type === 'string'} {/each}
<Input type="text" thin bind:value={workflowBlock.args[parameter]} /> </div>
{/if}
</div>
{/each}
<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,32 +28,42 @@
</script> </script>
<section> <section>
<Flowchart blocks={uiTree} {onSelect} /> <Flowchart {workflow} {onSelect} />
<footer>
{#if selectedWorkflow}
<button
class:highlighted={workflowLive}
class:hoverable={workflowLive}
class="stop-button hoverable">
<i class="ri-stop-fill" on:click={() => setWorkflowLive(false)} />
</button>
<button
class:highlighted={!workflowLive}
class:hoverable={!workflowLive}
class="play-button hoverable"
data-cy="activate-workflow"
on:click={() => setWorkflowLive(true)}>
<i class="ri-play-fill" />
</button>
{/if}
</footer>
</section> </section>
<footer>
{#if workflow}
<button
class:highlighted={workflowLive}
class:hoverable={workflowLive}
class="stop-button hoverable">
<i class="ri-stop-fill" on:click={() => setWorkflowLive(false)} />
</button>
<button
class:highlighted={!workflowLive}
class:hoverable={!workflowLive}
class="play-button hoverable"
data-cy="activate-workflow"
on:click={() => setWorkflowLive(true)}>
<i class="ri-play-fill" />
</button>
{/if}
</footer>
<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)}
<FlowItem {onSelect} {block} /> <div
{#if idx !== blocks.length - 1} class="block"
<Arrow /> animate:flip={{ duration: 600 }}
{/if} in:fade|local
out:fly|local={{ x: 100 }}>
<FlowItem {onSelect} {block} />
{#if idx !== blocks.length - 1}
<Arrow />
{/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" },
selectedTab = "ACTION" ]
if (selectedTab === "TRIGGER") {
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>
<div class="nav"> {#if $workflowStore.selectedWorkflow}
<div class="inner"> <div class="nav">
<SetupPanel /> <div class="inner">
<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", () => {
@ -111,7 +111,7 @@ describe("/accesslevels", () => {
await request await request
.get(`/api/accesslevels/${customLevel._id}`) .get(`/api/accesslevels/${customLevel._id}`)
.set(defaultHeaders(appId, instanceId)) .set(defaultHeaders(appId, instanceId))
.expect(404) .expect(404)
}) })
}) })

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"