commit
3f2cb90340
|
@ -38,16 +38,19 @@
|
|||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@beyonk/svelte-notifications": "^2.0.3",
|
||||
"@budibase/bbui": "^0.3.5",
|
||||
"@budibase/client": "^0.0.32",
|
||||
"@nx-js/compiler-util": "^2.0.0",
|
||||
"codemirror": "^5.51.0",
|
||||
"date-fns": "^1.29.0",
|
||||
"deepmerge": "^4.2.2",
|
||||
"feather-icons": "^4.21.0",
|
||||
"flatpickr": "^4.5.7",
|
||||
"lodash": "^4.17.13",
|
||||
"logrocket": "^1.0.6",
|
||||
"lunr": "^2.3.5",
|
||||
"mustache": "^4.0.1",
|
||||
"safe-buffer": "^5.1.2",
|
||||
"shortid": "^2.2.8",
|
||||
"string_decoder": "^1.2.0",
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
import AppNotification, {
|
||||
showAppNotification,
|
||||
} from "components/common/AppNotification.svelte"
|
||||
import { NotificationDisplay } from "@beyonk/svelte-notifications"
|
||||
|
||||
function showErrorBanner() {
|
||||
showAppNotification({
|
||||
|
@ -26,4 +27,7 @@
|
|||
|
||||
<AppNotification />
|
||||
|
||||
<!-- svelte-notifications -->
|
||||
<NotificationDisplay />
|
||||
|
||||
<Router {routes} />
|
||||
|
|
|
@ -77,7 +77,8 @@
|
|||
}
|
||||
|
||||
.budibase__input {
|
||||
width: 250px;
|
||||
width: 100%;
|
||||
max-width: 250px;
|
||||
height: 35px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #DBDBDB;
|
||||
|
|
|
@ -18,10 +18,12 @@ const post = apiCall("POST")
|
|||
const get = apiCall("GET")
|
||||
const patch = apiCall("PATCH")
|
||||
const del = apiCall("DELETE")
|
||||
const put = apiCall("PUT")
|
||||
|
||||
export default {
|
||||
post,
|
||||
get,
|
||||
patch,
|
||||
delete: del,
|
||||
put,
|
||||
}
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import { getStore } from "./store"
|
||||
import { getBackendUiStore } from "./store/backend"
|
||||
import { getWorkflowStore } from "./store/workflow/"
|
||||
import LogRocket from "logrocket"
|
||||
|
||||
export const store = getStore()
|
||||
export const backendUiStore = getBackendUiStore()
|
||||
export const workflowStore = getWorkflowStore()
|
||||
|
||||
export const initialise = async () => {
|
||||
try {
|
||||
|
|
|
@ -156,7 +156,6 @@ const createScreen = store => (screenName, route, layoutComponentName) => {
|
|||
description: "",
|
||||
url: "",
|
||||
_css: "",
|
||||
uiFunctions: "",
|
||||
props: createProps(rootComponent).props,
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
import mustache from "mustache"
|
||||
import blockDefinitions from "components/workflow/WorkflowPanel/blockDefinitions"
|
||||
import { generate } from "shortid"
|
||||
|
||||
/**
|
||||
* Class responsible for the traversing of the workflow definition.
|
||||
* Workflow definitions are stored in linked lists.
|
||||
*/
|
||||
export default class Workflow {
|
||||
constructor(workflow) {
|
||||
this.workflow = workflow
|
||||
}
|
||||
|
||||
hasTrigger() {
|
||||
return this.workflow.definition.trigger
|
||||
}
|
||||
|
||||
addBlock(block) {
|
||||
// Make sure to add trigger if doesn't exist
|
||||
if (!this.hasTrigger() && block.type === "TRIGGER") {
|
||||
this.workflow.definition.trigger = { id: generate(), ...block }
|
||||
return
|
||||
}
|
||||
|
||||
this.workflow.definition.steps.push({
|
||||
id: generate(),
|
||||
...block,
|
||||
})
|
||||
}
|
||||
|
||||
updateBlock(updatedBlock, id) {
|
||||
const { steps, trigger } = this.workflow.definition
|
||||
|
||||
if (trigger && trigger.id === id) {
|
||||
this.workflow.definition.trigger = null
|
||||
return
|
||||
}
|
||||
|
||||
const stepIdx = steps.findIndex(step => step.id === id)
|
||||
if (stepIdx < 0) throw new Error("Block not found.")
|
||||
steps.splice(stepIdx, 1, updatedBlock)
|
||||
}
|
||||
|
||||
deleteBlock(id) {
|
||||
const { steps, trigger } = this.workflow.definition
|
||||
|
||||
if (trigger && trigger.id === id) {
|
||||
this.workflow.definition.trigger = null
|
||||
return
|
||||
}
|
||||
|
||||
const stepIdx = steps.findIndex(step => step.id === id)
|
||||
if (stepIdx < 0) throw new Error("Block not found.")
|
||||
steps.splice(stepIdx, 1)
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
import { writable } from "svelte/store"
|
||||
import api from "../../api"
|
||||
import Workflow from "./Workflow"
|
||||
|
||||
const workflowActions = store => ({
|
||||
fetch: async instanceId => {
|
||||
const WORKFLOWS_URL = `/api/${instanceId}/workflows`
|
||||
const workflowResponse = await api.get(WORKFLOWS_URL)
|
||||
const json = await workflowResponse.json()
|
||||
store.update(state => {
|
||||
state.workflows = json
|
||||
return state
|
||||
})
|
||||
},
|
||||
create: async ({ instanceId, name }) => {
|
||||
const workflow = {
|
||||
name,
|
||||
definition: {
|
||||
steps: [],
|
||||
},
|
||||
}
|
||||
const CREATE_WORKFLOW_URL = `/api/${instanceId}/workflows`
|
||||
const response = await api.post(CREATE_WORKFLOW_URL, workflow)
|
||||
const json = await response.json()
|
||||
store.update(state => {
|
||||
state.workflows = state.workflows.concat(json.workflow)
|
||||
state.currentWorkflow = new Workflow(json.workflow)
|
||||
return state
|
||||
})
|
||||
},
|
||||
save: async ({ instanceId, workflow }) => {
|
||||
const UPDATE_WORKFLOW_URL = `/api/${instanceId}/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
|
||||
state.currentWorkflow = new Workflow(json.workflow)
|
||||
return state
|
||||
})
|
||||
},
|
||||
update: async ({ instanceId, workflow }) => {
|
||||
const UPDATE_WORKFLOW_URL = `/api/${instanceId}/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
|
||||
})
|
||||
},
|
||||
delete: async ({ instanceId, workflow }) => {
|
||||
const { _id, _rev } = workflow
|
||||
const DELETE_WORKFLOW_URL = `/api/${instanceId}/workflows/${_id}/${_rev}`
|
||||
await api.delete(DELETE_WORKFLOW_URL)
|
||||
|
||||
store.update(state => {
|
||||
const existingIdx = state.workflows.findIndex(
|
||||
existing => existing._id === _id
|
||||
)
|
||||
state.workflows.splice(existingIdx, 1)
|
||||
state.workflows = state.workflows
|
||||
state.currentWorkflow = null
|
||||
return state
|
||||
})
|
||||
},
|
||||
select: workflow => {
|
||||
store.update(state => {
|
||||
state.currentWorkflow = new Workflow(workflow)
|
||||
state.selectedWorkflowBlock = null
|
||||
return state
|
||||
})
|
||||
},
|
||||
addBlockToWorkflow: block => {
|
||||
store.update(state => {
|
||||
state.currentWorkflow.addBlock(block)
|
||||
state.selectedWorkflowBlock = block
|
||||
return state
|
||||
})
|
||||
},
|
||||
deleteWorkflowBlock: block => {
|
||||
store.update(state => {
|
||||
state.currentWorkflow.deleteBlock(block.id)
|
||||
state.selectedWorkflowBlock = null
|
||||
return state
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export const getWorkflowStore = () => {
|
||||
const INITIAL_WORKFLOW_STATE = {
|
||||
workflows: [],
|
||||
}
|
||||
|
||||
const store = writable(INITIAL_WORKFLOW_STATE)
|
||||
|
||||
store.actions = workflowActions(store)
|
||||
|
||||
return store
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
import Workflow from "../Workflow";
|
||||
import TEST_WORKFLOW from "./testWorkflow";
|
||||
|
||||
const TEST_BLOCK = {
|
||||
id: "VFWeZcIPx",
|
||||
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: "started...",
|
||||
},
|
||||
actionId: "SET_STATE",
|
||||
type: "ACTION",
|
||||
}
|
||||
|
||||
describe("Workflow Data Object", () => {
|
||||
let workflow
|
||||
|
||||
beforeEach(() => {
|
||||
workflow = new Workflow({ ...TEST_WORKFLOW });
|
||||
});
|
||||
|
||||
it("adds a workflow block to the workflow", () => {
|
||||
workflow.addBlock(TEST_BLOCK);
|
||||
expect(workflow.workflow.definition)
|
||||
})
|
||||
|
||||
it("updates a workflow block with new attributes", () => {
|
||||
const firstBlock = workflow.workflow.definition.steps[0];
|
||||
const updatedBlock = {
|
||||
...firstBlock,
|
||||
name: "UPDATED"
|
||||
};
|
||||
workflow.updateBlock(updatedBlock, firstBlock.id);
|
||||
expect(workflow.workflow.definition.steps[0]).toEqual(updatedBlock)
|
||||
})
|
||||
|
||||
it("deletes a workflow block successfully", () => {
|
||||
const { steps } = workflow.workflow.definition
|
||||
const originalLength = steps.length
|
||||
|
||||
const lastBlock = steps[steps.length - 1];
|
||||
workflow.deleteBlock(lastBlock.id);
|
||||
expect(workflow.workflow.definition.steps.length).toBeLessThan(originalLength);
|
||||
})
|
||||
|
||||
it("builds a tree that gets rendered in the flowchart builder", () => {
|
||||
expect(Workflow.buildUiTree(TEST_WORKFLOW.definition)).toMatchSnapshot();
|
||||
})
|
||||
})
|
|
@ -0,0 +1,49 @@
|
|||
// 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",
|
||||
},
|
||||
]
|
||||
`;
|
|
@ -0,0 +1,63 @@
|
|||
export default {
|
||||
_id: "53b6148c65d1429c987e046852d11611",
|
||||
_rev: "4-02c6659734934895812fa7be0215ee59",
|
||||
name: "Test Workflow",
|
||||
definition: {
|
||||
steps: [
|
||||
{
|
||||
id: "VFWeZcIPx",
|
||||
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: "started...",
|
||||
},
|
||||
actionId: "SET_STATE",
|
||||
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: {
|
||||
time: 3000,
|
||||
},
|
||||
actionId: "DELAY",
|
||||
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",
|
||||
},
|
||||
],
|
||||
},
|
||||
type: "workflow",
|
||||
live: true,
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
import { isString } from "lodash/fp"
|
||||
|
||||
import {
|
||||
BB_STATE_BINDINGPATH,
|
||||
BB_STATE_FALLBACK,
|
||||
BB_STATE_BINDINGSOURCE,
|
||||
isBound,
|
||||
parseBinding,
|
||||
} from "@budibase/client/src/state/parseBinding"
|
||||
|
||||
export const isBinding = isBound
|
||||
|
||||
export const setBinding = ({ path, fallback, source }, binding = {}) => {
|
||||
if (isNonEmptyString(path)) binding[BB_STATE_BINDINGPATH] = path
|
||||
if (isNonEmptyString(fallback)) binding[BB_STATE_FALLBACK] = fallback
|
||||
binding[BB_STATE_BINDINGSOURCE] = source || "store"
|
||||
return binding
|
||||
}
|
||||
|
||||
export const getBinding = val => {
|
||||
const binding = parseBinding(val)
|
||||
return binding
|
||||
? binding
|
||||
: {
|
||||
path: "",
|
||||
source: "store",
|
||||
fallback: "",
|
||||
}
|
||||
}
|
||||
|
||||
const isNonEmptyString = s => isString(s) && s.length > 0
|
|
@ -1,13 +1,8 @@
|
|||
import { eventHandlers } from "../../../../client/src/state/eventHandlers"
|
||||
import { writable } from "svelte/store"
|
||||
export { EVENT_TYPE_MEMBER_NAME } from "../../../../client/src/state/eventHandlers"
|
||||
|
||||
export const allHandlers = user => {
|
||||
const store = writable({
|
||||
_bbuser: user,
|
||||
})
|
||||
|
||||
const handlersObj = eventHandlers(store)
|
||||
export const allHandlers = () => {
|
||||
const handlersObj = eventHandlers()
|
||||
|
||||
const handlers = Object.keys(handlersObj).map(name => ({
|
||||
name,
|
||||
|
|
|
@ -43,13 +43,6 @@
|
|||
)
|
||||
}
|
||||
|
||||
async function selectRecord(record) {
|
||||
return await api.loadRecord(record.key, {
|
||||
appname: $store.appname,
|
||||
instanceId: $backendUiStore.selectedDatabase._id,
|
||||
})
|
||||
}
|
||||
|
||||
const ITEMS_PER_PAGE = 10
|
||||
// Internal headers we want to hide from the user
|
||||
const INTERNAL_HEADERS = ["_id", "_rev", "modelId", "type"]
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import api from "builderStore/api"
|
||||
|
||||
export async function createUser(user, appId, instanceId) {
|
||||
export async function createUser(user, instanceId) {
|
||||
const CREATE_USER_URL = `/api/${instanceId}/users`
|
||||
const response = await api.post(CREATE_USER_URL, user)
|
||||
return await response.json()
|
||||
|
@ -28,7 +28,7 @@ export async function saveRecord(record, instanceId, modelId) {
|
|||
}
|
||||
|
||||
export async function fetchDataForView(viewName, instanceId) {
|
||||
const FETCH_RECORDS_URL = `/api/${instanceId}/${viewName}/records`
|
||||
const FETCH_RECORDS_URL = `/api/${instanceId}/views/${viewName}`
|
||||
|
||||
const response = await api.get(FETCH_RECORDS_URL)
|
||||
return await response.json()
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<heading>
|
||||
<header>
|
||||
{#if !showFieldView}
|
||||
<i class="ri-list-settings-line button--toggled" />
|
||||
<h3 class="budibase__title--3">Create / Edit Model</h3>
|
||||
|
@ -43,7 +43,7 @@
|
|||
<i class="ri-file-list-line button--toggled" />
|
||||
<h3 class="budibase__title--3">Create / Edit Field</h3>
|
||||
{/if}
|
||||
</heading>
|
||||
</header>
|
||||
{#if !showFieldView}
|
||||
<div class="padding">
|
||||
<h4 class="budibase__label--big">Settings</h4>
|
||||
|
|
|
@ -64,7 +64,6 @@
|
|||
<NumberBox label="Max Length" bind:value={constraints.length.maximum} />
|
||||
<ValuesList label="Categories" bind:values={constraints.inclusion} />
|
||||
{:else if type === 'datetime'}
|
||||
<!-- TODO: revisit and fix with JSON schema -->
|
||||
<DatePicker
|
||||
label="Min Value"
|
||||
bind:value={constraints.datetime.earliest} />
|
||||
|
|
|
@ -7,14 +7,15 @@
|
|||
|
||||
let username
|
||||
let password
|
||||
let accessLevelId
|
||||
|
||||
$: valid = username && password
|
||||
$: valid = username && password && accessLevelId
|
||||
$: instanceId = $backendUiStore.selectedDatabase._id
|
||||
$: appId = $store.appId
|
||||
|
||||
async function createUser() {
|
||||
const user = { name: username, username, password }
|
||||
const response = await api.createUser(user, appId, instanceId)
|
||||
const user = { name: username, username, password, accessLevelId }
|
||||
const response = await api.createUser(user, instanceId)
|
||||
backendUiStore.actions.users.create(response)
|
||||
onClosed()
|
||||
}
|
||||
|
@ -30,6 +31,14 @@
|
|||
<label class="uk-form-label" for="form-stacked-text">Password</label>
|
||||
<input class="uk-input" type="password" bind:value={password} />
|
||||
</div>
|
||||
<div class="uk-margin">
|
||||
<label class="uk-form-label" for="form-stacked-text">Access Level</label>
|
||||
<select class="uk-select" bind:value={accessLevelId}>
|
||||
<option value="" />
|
||||
<option value="POWER_USER">Power User</option>
|
||||
<option value="ADMIN">Admin</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<footer>
|
||||
<ActionButton alert on:click={onClosed}>Cancel</ActionButton>
|
||||
|
|
|
@ -117,7 +117,6 @@
|
|||
selectedComponentType,
|
||||
selectedComponentId,
|
||||
frontendDefinition: JSON.stringify(frontendDefinition),
|
||||
currentPageFunctions: $store.currentPageFunctions,
|
||||
})} />
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
@ -4,7 +4,6 @@ export default ({
|
|||
selectedComponentType,
|
||||
selectedComponentId,
|
||||
frontendDefinition,
|
||||
currentPageFunctions,
|
||||
}) => `<html>
|
||||
<head>
|
||||
${stylesheetLinks}
|
||||
|
@ -36,7 +35,6 @@ export default ({
|
|||
</style>
|
||||
<script>
|
||||
window["##BUDIBASE_FRONTEND_DEFINITION##"] = ${frontendDefinition};
|
||||
window["##BUDIBASE_FRONTEND_FUNCTIONS##"] = ${currentPageFunctions};
|
||||
|
||||
import('/_builder/budibase-client.esm.mjs')
|
||||
.then(module => {
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
let categories = [
|
||||
{ value: "design", name: "Design" },
|
||||
{ value: "settings", name: "Settings" },
|
||||
{ value: "actions", name: "Actions" },
|
||||
{ value: "events", name: "Events" },
|
||||
]
|
||||
let selectedCategory = categories[0]
|
||||
|
||||
|
@ -93,6 +93,8 @@
|
|||
{componentDefinition}
|
||||
{panelDefinition}
|
||||
onChange={onPropChanged} />
|
||||
{:else if selectedCategory.value === 'events'}
|
||||
<EventsEditor component={componentInstance} />
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
|
|
|
@ -25,16 +25,15 @@
|
|||
export const EVENT_TYPE = "event"
|
||||
|
||||
export let component
|
||||
export let components
|
||||
|
||||
let modalOpen = false
|
||||
let events = []
|
||||
let selectedEvent = null
|
||||
|
||||
$: {
|
||||
const componentDefinition = components[component._component]
|
||||
events = Object.keys(componentDefinition.props)
|
||||
.filter(propName => componentDefinition.props[propName] === EVENT_TYPE)
|
||||
events = Object.keys(component)
|
||||
// TODO: use real events
|
||||
.filter(propName => ["onChange", "onClick", "onLoad"].includes(propName))
|
||||
.map(propName => ({
|
||||
name: propName,
|
||||
handlers: component[propName] || [],
|
||||
|
|
|
@ -5,12 +5,8 @@
|
|||
import Input from "components/common/Input.svelte"
|
||||
import { find, map, keys, reduce, keyBy } from "lodash/fp"
|
||||
import { pipe } from "components/common/core"
|
||||
import {
|
||||
EVENT_TYPE_MEMBER_NAME,
|
||||
allHandlers,
|
||||
} from "components/common/eventHandlers"
|
||||
import { store } from "builderStore"
|
||||
import StateBindingOptions from "../PropertyCascader/StateBindingOptions.svelte"
|
||||
import { EVENT_TYPE_MEMBER_NAME } from "components/common/eventHandlers"
|
||||
import { store, workflowStore } from "builderStore"
|
||||
import { ArrowDownIcon } from "components/common/Icons/"
|
||||
|
||||
export let parameter
|
||||
|
@ -22,18 +18,22 @@
|
|||
<div class="handler-option">
|
||||
<span>{parameter.name}</span>
|
||||
<div class="handler-input">
|
||||
{#if parameter.name === 'workflow'}
|
||||
<select
|
||||
class="budibase__input"
|
||||
on:change={onChange}
|
||||
bind:value={parameter.value}>
|
||||
{#each $workflowStore.workflows.filter(wf => wf.live) as workflow}
|
||||
<option value={workflow._id}>{workflow.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{:else}
|
||||
<Input on:change={onChange} value={parameter.value} />
|
||||
<button on:click={() => (isOpen = !isOpen)}>
|
||||
<div class="icon" style={`transform: rotate(${isOpen ? 0 : 90}deg);`}>
|
||||
<ArrowDownIcon size={36} />
|
||||
</div>
|
||||
</button>
|
||||
{#if isOpen}
|
||||
<StateBindingOptions
|
||||
onSelect={option => {
|
||||
onChange(option)
|
||||
isOpen = false
|
||||
}} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,121 +0,0 @@
|
|||
<script>
|
||||
import { ArrowDownIcon } from "components/common/Icons/"
|
||||
import { store } from "builderStore"
|
||||
import { buildStateOrigins } from "builderStore/buildStateOrigins"
|
||||
import { isBinding, getBinding, setBinding } from "components/common/binding"
|
||||
import StateBindingOptions from "./StateBindingOptions.svelte"
|
||||
|
||||
export let onChanged = () => {}
|
||||
export let value = ""
|
||||
|
||||
let isOpen = false
|
||||
let stateBindings = []
|
||||
|
||||
let bindingPath = ""
|
||||
let bindingFallbackValue = ""
|
||||
let bindingSource = "store"
|
||||
let bindingValue = ""
|
||||
|
||||
const bindValueToSource = (path, fallback, source) => {
|
||||
if (!path) {
|
||||
onChanged(fallback)
|
||||
return
|
||||
}
|
||||
const binding = setBinding({ path, fallback, source })
|
||||
onChanged(binding)
|
||||
}
|
||||
|
||||
const setBindingPath = value =>
|
||||
bindValueToSource(value, bindingFallbackValue, bindingSource)
|
||||
|
||||
const setBindingFallback = value =>
|
||||
bindValueToSource(bindingPath, value, bindingSource)
|
||||
|
||||
const setBindingSource = source =>
|
||||
bindValueToSource(bindingPath, bindingFallbackValue, source)
|
||||
|
||||
$: {
|
||||
const binding = getBinding(value)
|
||||
if (bindingPath !== binding.path) isOpen = false
|
||||
bindingPath = binding.path
|
||||
bindingValue = typeof value === "object" ? "" : value
|
||||
bindingFallbackValue = binding.fallback || bindingValue
|
||||
|
||||
const currentScreen = $store.screens.find(
|
||||
({ name }) => name === $store.currentPreviewItem.name
|
||||
)
|
||||
stateBindings = currentScreen
|
||||
? Object.keys(buildStateOrigins(currentScreen))
|
||||
: []
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="cascader">
|
||||
<div class="input-box">
|
||||
<input
|
||||
class:bold={!bindingFallbackValue && bindingPath}
|
||||
class="uk-input uk-form-small"
|
||||
value={bindingFallbackValue || bindingPath}
|
||||
on:change={e => {
|
||||
setBindingFallback(e.target.value)
|
||||
onChanged(e.target.value)
|
||||
}} />
|
||||
<button on:click={() => (isOpen = !isOpen)}>
|
||||
<div
|
||||
class="icon"
|
||||
class:highlighted={bindingPath}
|
||||
style={`transform: rotate(${isOpen ? 0 : 90}deg);`}>
|
||||
<ArrowDownIcon size={36} />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{#if isOpen}
|
||||
<StateBindingOptions
|
||||
onSelect={option => {
|
||||
onChanged(option)
|
||||
isOpen = false
|
||||
}} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.highlighted {
|
||||
color: rgba(0, 85, 255, 0.8);
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
font-size: 1.6rem;
|
||||
font-weight: 700;
|
||||
color: rgba(22, 48, 87, 1);
|
||||
}
|
||||
|
||||
.cascader {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.input-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
input {
|
||||
margin-right: 5px;
|
||||
border: 1px solid #dbdbdb;
|
||||
border-radius: 2px;
|
||||
opacity: 0.5;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 24px;
|
||||
}
|
||||
</style>
|
|
@ -1,63 +0,0 @@
|
|||
<script>
|
||||
export let onSelect = () => {}
|
||||
|
||||
let options = [
|
||||
{
|
||||
name: "state",
|
||||
description: "Front-end client state.",
|
||||
},
|
||||
{
|
||||
name: "context",
|
||||
description: "The component context object.",
|
||||
},
|
||||
{
|
||||
name: "event",
|
||||
description: "DOM event handler arguments.",
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<ul class="options">
|
||||
{#each options as option}
|
||||
<li on:click={() => onSelect(`${option.name}.`)}>
|
||||
<span class="name">{option.name}</span>
|
||||
<span class="description">{option.description}</span>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<style>
|
||||
.options {
|
||||
width: 172px;
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
top: 35px;
|
||||
padding: 10px;
|
||||
z-index: 1;
|
||||
background: rgba(249, 249, 249, 1);
|
||||
min-height: 50px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.name {
|
||||
color: rgba(22, 48, 87, 0.6);
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
margin-top: 5px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.name:hover {
|
||||
cursor: pointer;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
li {
|
||||
list-style-type: none;
|
||||
}
|
||||
</style>
|
|
@ -1 +0,0 @@
|
|||
export { default } from "./PropertyCascader.svelte"
|
|
@ -2,8 +2,6 @@
|
|||
import { backendUiStore } from "builderStore"
|
||||
import IconButton from "../common/IconButton.svelte"
|
||||
import Input from "../common/Input.svelte"
|
||||
import PropertyCascader from "./PropertyCascader"
|
||||
import { isBinding, getBinding, setBinding } from "../common/binding"
|
||||
import Colorpicker from "../common/Colorpicker.svelte"
|
||||
|
||||
export let value = ""
|
||||
|
@ -49,8 +47,6 @@
|
|||
{/if}
|
||||
{/each}
|
||||
</select>
|
||||
{:else}
|
||||
<PropertyCascader {onChanged} {value} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
<script>
|
||||
import { store, backendUiStore, workflowStore } from "builderStore"
|
||||
import { notifier } from "@beyonk/svelte-notifications"
|
||||
import api from "builderStore/api"
|
||||
import ActionButton from "components/common/ActionButton.svelte"
|
||||
|
||||
export let onClosed
|
||||
|
||||
let name
|
||||
|
||||
$: valid = !!name
|
||||
$: instanceId = $backendUiStore.selectedDatabase._id
|
||||
|
||||
async function deleteWorkflow() {
|
||||
await workflowStore.actions.delete({
|
||||
instanceId,
|
||||
workflow: $workflowStore.currentWorkflow.workflow,
|
||||
})
|
||||
onClosed()
|
||||
notifier.danger("Workflow deleted.")
|
||||
}
|
||||
</script>
|
||||
|
||||
<header>
|
||||
<i class="ri-stackshare-line" />
|
||||
Delete Workflow
|
||||
</header>
|
||||
<div>
|
||||
<p>
|
||||
Are you sure you want to delete this workflow? This action can't be undone.
|
||||
</p>
|
||||
</div>
|
||||
<footer>
|
||||
<a href="https://docs.budibase.com">
|
||||
<i class="ri-information-line" />
|
||||
Learn about workflows
|
||||
</a>
|
||||
<ActionButton on:click={onClosed}>Cancel</ActionButton>
|
||||
<ActionButton alert on:click={deleteWorkflow}>Delete</ActionButton>
|
||||
</footer>
|
||||
|
||||
<style>
|
||||
header {
|
||||
font-size: 24px;
|
||||
color: var(--font);
|
||||
font-weight: bold;
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
header i {
|
||||
margin-right: 10px;
|
||||
font-size: 20px;
|
||||
background: var(--secondary);
|
||||
color: var(--dark-grey);
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
div {
|
||||
padding: 0 30px 30px 30px;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
footer {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-gap: 5px;
|
||||
grid-auto-columns: 3fr 1fr 1fr;
|
||||
padding: 20px;
|
||||
background: #fafafa;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
footer a {
|
||||
color: var(--primary);
|
||||
font-size: 14px;
|
||||
vertical-align: middle;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
footer i {
|
||||
font-size: 20px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,45 @@
|
|||
<script>
|
||||
import { store } from "builderStore"
|
||||
import deepmerge from "deepmerge"
|
||||
|
||||
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="uk-margin block-field">
|
||||
<label class="uk-form-label">Page</label>
|
||||
<div class="uk-form-controls">
|
||||
<select class="budibase__input" bind:value={pageName}>
|
||||
{#each Object.keys(pages) as page}
|
||||
<option value={page}>{page}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
{#if components.length > 0}
|
||||
<label class="uk-form-label">Component</label>
|
||||
<div class="uk-form-controls">
|
||||
<select class="budibase__input" bind:value>
|
||||
{#each components as component}
|
||||
<option value={component._id}>{component._id}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
|
@ -0,0 +1,16 @@
|
|||
<script>
|
||||
import { backendUiStore } from "builderStore"
|
||||
|
||||
export let value
|
||||
</script>
|
||||
|
||||
<div class="uk-margin block-field">
|
||||
<label class="uk-form-label">Model</label>
|
||||
<div class="uk-form-controls">
|
||||
<select class="budibase__input" bind:value>
|
||||
{#each $backendUiStore.models as model}
|
||||
<option value={model}>{model.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,33 @@
|
|||
<script>
|
||||
import { backendUiStore } from "builderStore"
|
||||
|
||||
export let value
|
||||
</script>
|
||||
|
||||
<div class="uk-margin block-field">
|
||||
<div class="uk-form-controls">
|
||||
<select class="budibase__input" bind:value={value.model}>
|
||||
{#each $backendUiStore.models as model}
|
||||
<option value={model}>{model.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if value.model}
|
||||
<div class="uk-margin block-field">
|
||||
<label class="uk-form-label fields">Fields</label>
|
||||
{#each Object.keys(value.model.schema) as field}
|
||||
<div class="uk-form-controls uk-margin">
|
||||
<label class="uk-form-label">{field}</label>
|
||||
<input type="text" class="budibase__input" bind:value={value[field]} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.fields {
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,207 @@
|
|||
<script>
|
||||
import { fade } from "svelte/transition"
|
||||
import { onMount, getContext } from "svelte"
|
||||
import { backendUiStore, workflowStore } from "builderStore"
|
||||
import { notifier } from "@beyonk/svelte-notifications"
|
||||
import api from "builderStore/api"
|
||||
import WorkflowBlockSetup from "./WorkflowBlockSetup.svelte"
|
||||
import DeleteWorkflowModal from "./DeleteWorkflowModal.svelte"
|
||||
|
||||
const { open, close } = getContext("simple-modal")
|
||||
|
||||
const ACCESS_LEVELS = [
|
||||
{
|
||||
name: "Admin",
|
||||
key: "ADMIN"
|
||||
},
|
||||
{
|
||||
name: "Power User",
|
||||
key: "POWER_USER"
|
||||
}
|
||||
];
|
||||
|
||||
let selectedTab = "SETUP"
|
||||
let testResult
|
||||
|
||||
$: workflow =
|
||||
$workflowStore.currentWorkflow && $workflowStore.currentWorkflow.workflow
|
||||
$: workflowBlock = $workflowStore.selectedWorkflowBlock
|
||||
|
||||
function deleteWorkflow() {
|
||||
open(
|
||||
DeleteWorkflowModal,
|
||||
{
|
||||
onClosed: close,
|
||||
},
|
||||
{ styleContent: { padding: "0" } }
|
||||
)
|
||||
}
|
||||
|
||||
function deleteWorkflowBlock() {
|
||||
workflowStore.actions.deleteWorkflowBlock(workflowBlock)
|
||||
notifier.info("Workflow block deleted.")
|
||||
}
|
||||
|
||||
function testWorkflow() {
|
||||
testResult = "PASSED"
|
||||
}
|
||||
</script>
|
||||
|
||||
<section>
|
||||
<header>
|
||||
<span
|
||||
class="hoverable"
|
||||
class:selected={selectedTab === 'SETUP'}
|
||||
on:click={() => {
|
||||
selectedTab = 'SETUP'
|
||||
testResult = null
|
||||
}}>
|
||||
Setup
|
||||
</span>
|
||||
{#if !workflowBlock}
|
||||
<span
|
||||
class="hoverable"
|
||||
class:selected={selectedTab === 'TEST'}
|
||||
on:click={() => (selectedTab = 'TEST')}>
|
||||
Test
|
||||
</span>
|
||||
{/if}
|
||||
</header>
|
||||
{#if selectedTab === 'TEST'}
|
||||
<div class="uk-margin config-item">
|
||||
{#if testResult}
|
||||
<button
|
||||
transition:fade
|
||||
class:passed={testResult === 'PASSED'}
|
||||
class:failed={testResult === 'FAILED'}
|
||||
class="test-result">
|
||||
{testResult}
|
||||
</button>
|
||||
{/if}
|
||||
<button class="workflow-button hoverable" on:click={testWorkflow}>
|
||||
Test
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
{#if selectedTab === 'SETUP'}
|
||||
{#if workflowBlock}
|
||||
<WorkflowBlockSetup {workflowBlock} />
|
||||
<button class="workflow-button hoverable" on:click={deleteWorkflowBlock}>
|
||||
Delete Block
|
||||
</button>
|
||||
{:else if $workflowStore.currentWorkflow}
|
||||
<div class="panel-body">
|
||||
<label class="uk-form-label">Workflow: {workflow.name}</label>
|
||||
<div class="uk-margin config-item">
|
||||
<label class="uk-form-label">Name</label>
|
||||
<div class="uk-form-controls">
|
||||
<input
|
||||
type="text"
|
||||
class="budibase__input"
|
||||
bind:value={workflow.name} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="uk-margin config-item">
|
||||
<label class="uk-form-label">User Access</label>
|
||||
<div class="access-levels">
|
||||
{#each ACCESS_LEVELS as { name, key }}
|
||||
<span class="access-level">
|
||||
<label>{name}</label>
|
||||
<input class="uk-checkbox" type="checkbox" />
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="workflow-button hoverable" on:click={deleteWorkflow}>
|
||||
Delete Workflow
|
||||
</button>
|
||||
{/if}
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<style>
|
||||
section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
header {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.selected {
|
||||
color: var(--font);
|
||||
}
|
||||
|
||||
.config-item {
|
||||
padding: 20px;
|
||||
background: var(--light-grey);
|
||||
}
|
||||
|
||||
header > span {
|
||||
color: var(--dark-grey);
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
color: var(--font);
|
||||
}
|
||||
|
||||
.workflow-button {
|
||||
font-family: Roboto;
|
||||
width: 100%;
|
||||
border: solid 1px #f2f2f2;
|
||||
border-radius: 2px;
|
||||
background: var(--white);
|
||||
height: 32px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.workflow-button:hover {
|
||||
background: var(--light-grey);
|
||||
}
|
||||
|
||||
.access-level {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.access-level label {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.test-result {
|
||||
border: none;
|
||||
width: 100%;
|
||||
border-radius: 2px;
|
||||
height: 32px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--white);
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.passed {
|
||||
background: #84c991;
|
||||
}
|
||||
|
||||
.failed {
|
||||
background: var(--coral);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,85 @@
|
|||
<script>
|
||||
import { backendUiStore, store } from "builderStore"
|
||||
import ComponentSelector from "./ParamInputs/ComponentSelector.svelte"
|
||||
import ModelSelector from "./ParamInputs/ModelSelector.svelte"
|
||||
import RecordSelector from "./ParamInputs/RecordSelector.svelte"
|
||||
|
||||
export let workflowBlock
|
||||
|
||||
let params
|
||||
|
||||
$: workflowParams = workflowBlock.params
|
||||
? Object.entries(workflowBlock.params)
|
||||
: []
|
||||
</script>
|
||||
|
||||
<label class="uk-form-label">{workflowBlock.type}: {workflowBlock.name}</label>
|
||||
{#each workflowParams as [parameter, type]}
|
||||
<div class="uk-margin block-field">
|
||||
<label class="uk-form-label">{parameter}</label>
|
||||
<div class="uk-form-controls">
|
||||
{#if Array.isArray(type)}
|
||||
<select
|
||||
class="budibase__input"
|
||||
bind:value={workflowBlock.args[parameter]}>
|
||||
{#each type as option}
|
||||
<option value={option}>{option}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{:else if type === 'component'}
|
||||
<ComponentSelector bind:value={workflowBlock.args[parameter]} />
|
||||
{:else if type === 'accessLevel'}
|
||||
<select
|
||||
class="budibase__input"
|
||||
bind:value={workflowBlock.args[parameter]}>
|
||||
<option value="ADMIN">Admin</option>
|
||||
<option value="POWER_USER">Power User</option>
|
||||
</select>
|
||||
{:else if type === 'password'}
|
||||
<input
|
||||
type="password"
|
||||
class="budibase__input"
|
||||
bind:value={workflowBlock.args[parameter]} />
|
||||
{:else if type === 'number'}
|
||||
<input
|
||||
type="number"
|
||||
class="budibase__input"
|
||||
bind:value={workflowBlock.args[parameter]} />
|
||||
{:else if type === 'longText'}
|
||||
<textarea
|
||||
type="text"
|
||||
class="budibase__input"
|
||||
bind:value={workflowBlock.args[parameter]} />
|
||||
{:else if type === 'model'}
|
||||
<ModelSelector bind:value={workflowBlock.args[parameter]} />
|
||||
{:else if type === 'record'}
|
||||
<RecordSelector bind:value={workflowBlock.args[parameter]} />
|
||||
{:else if type === 'string'}
|
||||
<input
|
||||
type="text"
|
||||
class="budibase__input"
|
||||
bind:value={workflowBlock.args[parameter]} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<style>
|
||||
.block-field {
|
||||
border-radius: 3px;
|
||||
background: var(--light-grey);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
label {
|
||||
text-transform: capitalize;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 150px;
|
||||
font-family: inherit;
|
||||
padding: 5px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1 @@
|
|||
export { default as SetupPanel } from "./SetupPanel.svelte"
|
|
@ -0,0 +1,90 @@
|
|||
<script>
|
||||
import { onMount } from "svelte"
|
||||
import { workflowStore, backendUiStore } from "builderStore"
|
||||
import { notifier } from "@beyonk/svelte-notifications"
|
||||
import Flowchart from "./flowchart/FlowChart.svelte"
|
||||
import api from "builderStore/api"
|
||||
|
||||
let selectedWorkflow
|
||||
let uiTree
|
||||
let instanceId = $backendUiStore.selectedDatabase._id
|
||||
|
||||
$: selectedWorkflow = $workflowStore.currentWorkflow
|
||||
|
||||
$: workflowLive = selectedWorkflow && selectedWorkflow.workflow.live
|
||||
|
||||
$: uiTree = selectedWorkflow ? selectedWorkflow.createUiTree() : []
|
||||
|
||||
$: instanceId = $backendUiStore.selectedDatabase._id
|
||||
|
||||
function onSelect(block) {
|
||||
workflowStore.update(state => {
|
||||
state.selectedWorkflowBlock = block
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
function setWorkflowLive(live) {
|
||||
const { workflow } = selectedWorkflow
|
||||
workflow.live = live
|
||||
workflowStore.actions.save({ instanceId, workflow })
|
||||
if (live) {
|
||||
notifier.info(`Workflow ${workflow.name} enabled.`)
|
||||
} else {
|
||||
notifier.danger(`Workflow ${workflow.name} disabled.`)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<section>
|
||||
<Flowchart blocks={uiTree} {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"
|
||||
on:click={() => setWorkflowLive(true)}>
|
||||
<i class="ri-play-fill" />
|
||||
</button>
|
||||
{/if}
|
||||
</footer>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
footer > button {
|
||||
border-radius: 100%;
|
||||
color: var(--white);
|
||||
width: 76px;
|
||||
height: 76px;
|
||||
border: none;
|
||||
background: #adaec4;
|
||||
font-size: 45px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 24px;
|
||||
}
|
||||
|
||||
.play-button.highlighted {
|
||||
background: var(--primary);
|
||||
}
|
||||
|
||||
.stop-button.highlighted {
|
||||
background: var(--coral);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,9 @@
|
|||
<svg
|
||||
width="9"
|
||||
height="75"
|
||||
viewBox="0 0 9 75"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.0625 70H9L4.5 75L0 70H3.9375V65H5.0625V70Z" fill="#ADAEC4" />
|
||||
<rect x="4" width="1" height="65" fill="#ADAEC4" />
|
||||
</svg>
|
After Width: | Height: | Size: 241 B |
|
@ -0,0 +1,24 @@
|
|||
<script>
|
||||
import FlowItem from "./FlowItem.svelte"
|
||||
import Arrow from "./Arrow.svelte"
|
||||
|
||||
export let blocks = []
|
||||
export let onSelect
|
||||
</script>
|
||||
|
||||
<section class="canvas">
|
||||
{#each blocks as block, idx}
|
||||
<FlowItem {onSelect} {block} />
|
||||
{#if idx !== blocks.length - 1}
|
||||
<Arrow />
|
||||
{/if}
|
||||
{/each}
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.canvas {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,73 @@
|
|||
<script>
|
||||
import { fade } from "svelte/transition"
|
||||
|
||||
export let onSelect
|
||||
export let block
|
||||
|
||||
function selectBlock() {
|
||||
onSelect(block)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div transition:fade class={`${block.type} hoverable`} on:click={selectBlock}>
|
||||
<header>
|
||||
{#if block.type === 'TRIGGER'}
|
||||
<i class="ri-lightbulb-fill" />
|
||||
When this happens...
|
||||
{:else if block.type === 'ACTION'}
|
||||
<i class="ri-flashlight-fill" />
|
||||
Do this...
|
||||
{:else if block.type === 'LOGIC'}
|
||||
<i class="ri-pause-fill" />
|
||||
Only continue if...
|
||||
{/if}
|
||||
</header>
|
||||
<hr />
|
||||
<p>
|
||||
{@html block.body}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div {
|
||||
width: 320px;
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
transition: 0.3s all;
|
||||
box-shadow: 0 4px 30px 0 rgba(57, 60, 68, 0.08);
|
||||
background-color: var(--font);
|
||||
font-size: 16px;
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
header {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
header i {
|
||||
font-size: 20px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.ACTION {
|
||||
background-color: var(--white);
|
||||
color: var(--font);
|
||||
}
|
||||
|
||||
.TRIGGER {
|
||||
background-color: var(--font);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.LOGIC {
|
||||
background-color: var(--secondary);
|
||||
color: var(--font);
|
||||
}
|
||||
|
||||
div:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,81 @@
|
|||
<script>
|
||||
import { onMount } from "svelte"
|
||||
import { backendUiStore, workflowStore } from "builderStore"
|
||||
import { WorkflowList } from "../"
|
||||
import WorkflowBlock from "./WorkflowBlock.svelte"
|
||||
import api from "builderStore/api"
|
||||
import blockDefinitions from "../blockDefinitions"
|
||||
|
||||
let selectedTab = "TRIGGER"
|
||||
let definitions = []
|
||||
|
||||
$: definitions = Object.entries(blockDefinitions[selectedTab])
|
||||
|
||||
$: {
|
||||
if (
|
||||
$workflowStore.currentWorkflow.hasTrigger() &&
|
||||
selectedTab === "TRIGGER"
|
||||
) {
|
||||
selectedTab = "ACTION"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<section>
|
||||
<div class="subtabs">
|
||||
{#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">
|
||||
{#each definitions as [actionId, blockDefinition]}
|
||||
<WorkflowBlock {blockDefinition} {actionId} blockType={selectedTab} />
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.subtabs {
|
||||
margin-top: 27px;
|
||||
display: grid;
|
||||
grid-gap: 5px;
|
||||
grid-auto-flow: column;
|
||||
grid-auto-columns: 1fr 1fr 1fr;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.subtabs span {
|
||||
transition: 0.3s all;
|
||||
text-align: center;
|
||||
color: var(--dark-grey);
|
||||
font-weight: 500;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.subtabs span.selected {
|
||||
background: var(--dark-grey);
|
||||
color: var(--white);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.subtabs span:not(.selected) {
|
||||
color: var(--dark-grey);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,56 @@
|
|||
<script>
|
||||
import { workflowStore } from "builderStore"
|
||||
|
||||
export let blockType
|
||||
export let blockDefinition
|
||||
export let actionId
|
||||
|
||||
function addBlockToWorkflow() {
|
||||
workflowStore.actions.addBlockToWorkflow({
|
||||
...blockDefinition,
|
||||
args: blockDefinition.args || {},
|
||||
actionId,
|
||||
type: blockType,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="workflow-block hoverable" on:click={addBlockToWorkflow}>
|
||||
<div>
|
||||
<i class={blockDefinition.icon} />
|
||||
</div>
|
||||
<div class="workflow-text">
|
||||
<h4>{blockDefinition.name}</h4>
|
||||
<p>{blockDefinition.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.workflow-block {
|
||||
display: flex;
|
||||
padding: 20px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.workflow-text {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
i {
|
||||
background: var(--secondary);
|
||||
color: var(--dark-grey);
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 12px;
|
||||
color: var(--dark-grey);
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,89 @@
|
|||
<script>
|
||||
import { store, backendUiStore, workflowStore } from "builderStore"
|
||||
import { notifier } from "@beyonk/svelte-notifications"
|
||||
import api from "builderStore/api"
|
||||
import ActionButton from "components/common/ActionButton.svelte"
|
||||
|
||||
export let onClosed
|
||||
|
||||
let name
|
||||
|
||||
$: valid = !!name
|
||||
$: instanceId = $backendUiStore.selectedDatabase._id
|
||||
$: appId = $store.appId
|
||||
|
||||
async function createWorkflow() {
|
||||
await workflowStore.actions.create({
|
||||
name,
|
||||
instanceId,
|
||||
})
|
||||
onClosed()
|
||||
notifier.success(`Workflow ${name} created.`)
|
||||
}
|
||||
</script>
|
||||
|
||||
<header>
|
||||
<i class="ri-stackshare-line" />
|
||||
Create Workflow
|
||||
</header>
|
||||
<div>
|
||||
<label class="uk-form-label" for="form-stacked-text">Name</label>
|
||||
<input class="uk-input" type="text" bind:value={name} />
|
||||
</div>
|
||||
<footer>
|
||||
<a href="https://docs.budibase.com">
|
||||
<i class="ri-information-line" />
|
||||
Learn about workflows
|
||||
</a>
|
||||
<ActionButton alert on:click={onClosed}>Cancel</ActionButton>
|
||||
<ActionButton disabled={!valid} on:click={createWorkflow}>Save</ActionButton>
|
||||
</footer>
|
||||
|
||||
<style>
|
||||
header {
|
||||
font-size: 24px;
|
||||
color: var(--font);
|
||||
font-weight: bold;
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
header i {
|
||||
margin-right: 10px;
|
||||
font-size: 20px;
|
||||
background: var(--secondary);
|
||||
color: var(--dark-grey);
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
div {
|
||||
padding: 0 30px 30px 30px;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
footer {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-gap: 5px;
|
||||
grid-auto-columns: 3fr 1fr 1fr;
|
||||
padding: 20px;
|
||||
background: #fafafa;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
footer a {
|
||||
color: var(--primary);
|
||||
font-size: 14px;
|
||||
vertical-align: middle;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
footer i {
|
||||
font-size: 20px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,126 @@
|
|||
<script>
|
||||
import Modal from "svelte-simple-modal"
|
||||
import { notifier } from "@beyonk/svelte-notifications"
|
||||
import { onMount, getContext } from "svelte"
|
||||
import { backendUiStore, workflowStore } from "builderStore"
|
||||
import api from "builderStore/api"
|
||||
import CreateWorkflowModal from "./CreateWorkflowModal.svelte"
|
||||
|
||||
const { open, close } = getContext("simple-modal")
|
||||
|
||||
$: currentWorkflowId =
|
||||
$workflowStore.currentWorkflow &&
|
||||
$workflowStore.currentWorkflow.workflow._id
|
||||
|
||||
function newWorkflow() {
|
||||
open(
|
||||
CreateWorkflowModal,
|
||||
{
|
||||
onClosed: close,
|
||||
},
|
||||
{ styleContent: { padding: "0" } }
|
||||
)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
workflowStore.actions.fetch($backendUiStore.selectedDatabase._id)
|
||||
})
|
||||
|
||||
async function saveWorkflow() {
|
||||
const workflow = $workflowStore.currentWorkflow.workflow
|
||||
await workflowStore.actions.save({
|
||||
instanceId: $backendUiStore.selectedDatabase._id,
|
||||
workflow,
|
||||
})
|
||||
notifier.success(`Workflow ${workflow.name} saved.`)
|
||||
}
|
||||
</script>
|
||||
|
||||
<section>
|
||||
<button class="new-workflow-button hoverable" on:click={newWorkflow}>
|
||||
Create New Workflow
|
||||
</button>
|
||||
<ul>
|
||||
{#each $workflowStore.workflows as workflow}
|
||||
<li
|
||||
class="workflow-item"
|
||||
class:selected={workflow._id === currentWorkflowId}
|
||||
on:click={() => workflowStore.actions.select(workflow)}>
|
||||
<i class="ri-stackshare-line" class:live={workflow.live} />
|
||||
{workflow.name}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{#if $workflowStore.currentWorkflow}
|
||||
<button class="new-workflow-button hoverable" on:click={saveWorkflow}>
|
||||
Save Workflow
|
||||
</button>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<style>
|
||||
section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
i {
|
||||
color: #adaec4;
|
||||
}
|
||||
|
||||
i:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.live {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
li {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.workflow-item {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 3px;
|
||||
height: 32px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.workflow-item i {
|
||||
font-size: 24px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.workflow-item:hover {
|
||||
cursor: pointer;
|
||||
background: var(--secondary);
|
||||
}
|
||||
|
||||
.workflow-item.selected {
|
||||
background: var(--secondary);
|
||||
}
|
||||
|
||||
.new-workflow-button {
|
||||
font-family: Roboto;
|
||||
width: 100%;
|
||||
border: solid 1px #f2f2f2;
|
||||
border-radius: 2px;
|
||||
background: var(--white);
|
||||
height: 32px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.new-workflow-button:hover {
|
||||
background: var(--light-grey);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,50 @@
|
|||
<script>
|
||||
import { onMount } from "svelte"
|
||||
import { backendUiStore, workflowStore } from "builderStore"
|
||||
import { WorkflowList, BlockList } from "./"
|
||||
import api from "builderStore/api"
|
||||
import blockDefinitions from "./blockDefinitions"
|
||||
|
||||
let selectedTab = "WORKFLOWS"
|
||||
let definitions = []
|
||||
</script>
|
||||
|
||||
<header>
|
||||
<span
|
||||
class="hoverable workflow-header"
|
||||
class:selected={selectedTab === 'WORKFLOWS'}
|
||||
on:click={() => (selectedTab = 'WORKFLOWS')}>
|
||||
Workflows
|
||||
</span>
|
||||
{#if $workflowStore.currentWorkflow}
|
||||
<span
|
||||
class="hoverable"
|
||||
class:selected={selectedTab === 'ADD'}
|
||||
on:click={() => (selectedTab = 'ADD')}>
|
||||
Add
|
||||
</span>
|
||||
{/if}
|
||||
</header>
|
||||
{#if selectedTab === 'WORKFLOWS'}
|
||||
<WorkflowList />
|
||||
{:else if selectedTab === 'ADD'}
|
||||
<BlockList />
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
header {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.workflow-header {
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
span:not(.selected) {
|
||||
color: var(--dark-grey);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,170 @@
|
|||
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: {
|
||||
name: "Save Record",
|
||||
tagline: "<b>Save</b> a <b>{{record.model.name}}</b> record",
|
||||
icon: "ri-save-3-fill",
|
||||
description: "Save a record to your database.",
|
||||
environment: "SERVER",
|
||||
params: {
|
||||
record: "record",
|
||||
},
|
||||
args: {
|
||||
record: {},
|
||||
},
|
||||
},
|
||||
DELETE_RECORD: {
|
||||
description: "Delete a record from your database.",
|
||||
icon: "ri-delete-bin-line",
|
||||
name: "Delete Record",
|
||||
tagline: "<b>Delete</b> a <b>{{record.model.name}}</b> record",
|
||||
environment: "SERVER",
|
||||
params: {
|
||||
record: "record",
|
||||
},
|
||||
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: {
|
||||
description: "Create a new user.",
|
||||
tagline: "Create user <b>{{username}}</b>",
|
||||
icon: "ri-user-add-fill",
|
||||
name: "Create User",
|
||||
environment: "SERVER",
|
||||
params: {
|
||||
username: "string",
|
||||
password: "password",
|
||||
accessLevelId: "accessLevel",
|
||||
},
|
||||
},
|
||||
SEND_EMAIL: {
|
||||
description: "Send an email.",
|
||||
tagline: "Send email to <b>{{to}}</b>",
|
||||
icon: "ri-mail-open-fill",
|
||||
name: "Send Email",
|
||||
environment: "SERVER",
|
||||
params: {
|
||||
to: "string",
|
||||
from: "string",
|
||||
subject: "longText",
|
||||
text: "longText",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const TRIGGER = {
|
||||
RECORD_SAVED: {
|
||||
name: "Record Saved",
|
||||
event: "record:save",
|
||||
icon: "ri-save-line",
|
||||
tagline: "Record is added to <b>{{model.name}}</b>",
|
||||
description: "Save a record to your database.",
|
||||
environment: "SERVER",
|
||||
params: {
|
||||
model: "model",
|
||||
},
|
||||
},
|
||||
RECORD_DELETED: {
|
||||
name: "Record Deleted",
|
||||
event: "record:delete",
|
||||
icon: "ri-delete-bin-line",
|
||||
tagline: "Record is deleted from <b>{{model.name}}</b>",
|
||||
description: "Fired when a record is deleted from your database.",
|
||||
environment: "SERVER",
|
||||
params: {
|
||||
model: "model",
|
||||
},
|
||||
},
|
||||
// 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 {
|
||||
ACTION,
|
||||
TRIGGER,
|
||||
LOGIC,
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export { default as WorkflowPanel } from "./WorkflowPanel.svelte"
|
||||
export { default as BlockList } from "./BlockList/BlockList.svelte"
|
||||
export { default as WorkflowList } from "./WorkflowList/WorkflowList.svelte"
|
|
@ -0,0 +1,3 @@
|
|||
export { default as WorkflowBuilder } from "./WorkflowBuilder/WorkflowBuilder.svelte"
|
||||
export { default as SetupPanel } from "./SetupPanel/SetupPanel.svelte"
|
||||
export { default as WorkflowPanel } from "./WorkflowPanel/WorkflowPanel.svelte"
|
|
@ -0,0 +1,647 @@
|
|||
body, html {
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
overflow: hidden;
|
||||
background-repeat: repeat;
|
||||
background-size: 30px 30px;
|
||||
background-color: #FBFBFB;
|
||||
height: 100%;
|
||||
}
|
||||
#navigation {
|
||||
height: 71px;
|
||||
background-color: #FFF;
|
||||
border: 1px solid #E8E8EF;
|
||||
width: 100%;
|
||||
display: table;
|
||||
box-sizing: border-box;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
z-index: 9
|
||||
}
|
||||
#back {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 100px;
|
||||
background-color: #F1F4FC;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin-top: 12px;
|
||||
margin-right: 10px
|
||||
}
|
||||
#back img {
|
||||
margin-top: 13px;
|
||||
}
|
||||
#names {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
#title {
|
||||
font-family: Roboto;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
color: #393C44;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
#subtitle {
|
||||
font-family: Roboto;
|
||||
color: #808292;
|
||||
font-size: 14px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
#leftside {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-left: 20px;
|
||||
}
|
||||
#centerswitch {
|
||||
position: absolute;
|
||||
width: 222px;
|
||||
left: 50%;
|
||||
margin-left: -111px;
|
||||
top: 15px;
|
||||
}
|
||||
#leftswitch {
|
||||
border: 1px solid #E8E8EF;
|
||||
background-color: #FBFBFB;
|
||||
width: 111px;
|
||||
height: 39px;
|
||||
line-height: 39px;
|
||||
border-radius: 5px 0px 0px 5px;
|
||||
font-family: Roboto;
|
||||
color: #393C44;
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
#rightswitch {
|
||||
font-family: Roboto;
|
||||
color: #808292;
|
||||
border-radius: 0px 5px 5px 0px;
|
||||
border: 1px solid #E8E8EF;
|
||||
height: 39px;
|
||||
width: 102px;
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
line-height: 39px;
|
||||
text-align: center;
|
||||
margin-left: -5px;
|
||||
}
|
||||
#discard {
|
||||
font-family: Roboto;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
color: #A6A6B3;
|
||||
width: 95px;
|
||||
height: 38px;
|
||||
border: 1px solid #E8E8EF;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
line-height: 38px;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
transition: all .2s cubic-bezier(.05,.03,.35,1);
|
||||
}
|
||||
#discard:hover {
|
||||
cursor: pointer;
|
||||
opacity: .7;
|
||||
}
|
||||
#publish {
|
||||
font-family: Roboto;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
color: #FFF;
|
||||
background-color: #217CE8;
|
||||
border-radius: 5px;
|
||||
width: 143px;
|
||||
height: 38px;
|
||||
margin-left: 10px;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
text-align: center;
|
||||
line-height: 38px;
|
||||
margin-right: 20px;
|
||||
transition: all .2s cubic-bezier(.05,.03,.35,1);
|
||||
}
|
||||
#publish:hover {
|
||||
cursor: pointer;
|
||||
opacity: .7;
|
||||
}
|
||||
#buttonsright {
|
||||
float: right;
|
||||
margin-top: 15px;
|
||||
}
|
||||
#leftcard {
|
||||
width: 363px;
|
||||
background-color: #FFF;
|
||||
border: 1px solid #E8E8EF;
|
||||
box-sizing: border-box;
|
||||
padding-top: 85px;
|
||||
padding-left: 20px;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
}
|
||||
#search input {
|
||||
width: 318px;
|
||||
height: 40px;
|
||||
background-color: #FFF;
|
||||
border: 1px solid #E8E8EF;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0px 2px 8px rgba(34,34,87,0.05);
|
||||
border-radius: 5px;
|
||||
text-indent: 35px;
|
||||
font-family: Roboto;
|
||||
font-size: 16px;
|
||||
}
|
||||
::-webkit-input-placeholder { /* Edge */
|
||||
color: #C9C9D5;
|
||||
}
|
||||
|
||||
:-ms-input-placeholder { /* Internet Explorer 10-11 */
|
||||
color: #C9C9D5
|
||||
}
|
||||
|
||||
::placeholder {
|
||||
color: #C9C9D5;
|
||||
}
|
||||
#search img {
|
||||
position: absolute;
|
||||
margin-top: 10px;
|
||||
width: 18px;
|
||||
margin-left: 12px;
|
||||
}
|
||||
#header {
|
||||
font-size: 20px;
|
||||
font-family: Roboto;
|
||||
font-weight: bold;
|
||||
color: #393C44;
|
||||
}
|
||||
#subnav {
|
||||
border-bottom: 1px solid #E8E8EF;
|
||||
width: calc(100% + 20px);
|
||||
margin-left: -20px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.navdisabled {
|
||||
transition: all .3s cubic-bezier(.05,.03,.35,1);
|
||||
}
|
||||
.navdisabled:hover {
|
||||
cursor: pointer;
|
||||
opacity: .5;
|
||||
}
|
||||
.navactive {
|
||||
color: #393C44!important;
|
||||
}
|
||||
#triggers {
|
||||
margin-left: 20px;
|
||||
font-family: Roboto;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
color: #808292;
|
||||
width: calc(88% / 3);
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
display: inline-block;
|
||||
float: left;
|
||||
}
|
||||
.navactive:after {
|
||||
display: block;
|
||||
content: "";
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background-color: #217CE8;
|
||||
margin-top: -4px;
|
||||
}
|
||||
#actions {
|
||||
display: inline-block;
|
||||
font-family: Roboto;
|
||||
font-weight: 500;
|
||||
color: #808292;
|
||||
font-size: 14px;
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
width: calc(88% / 3);
|
||||
text-align: center;
|
||||
float: left;
|
||||
}
|
||||
#loggers {
|
||||
width: calc(88% / 3);
|
||||
display: inline-block;
|
||||
font-family: Roboto;
|
||||
font-weight: 500;
|
||||
color: #808292;
|
||||
font-size: 14px;
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
text-align: center;
|
||||
}
|
||||
#footer {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
padding-left: 20px;
|
||||
line-height: 40px;
|
||||
bottom: 0;
|
||||
width: 362px;
|
||||
border: 1px solid #E8E8EF;
|
||||
height: 67px;
|
||||
box-sizing: border-box;
|
||||
background-color: #FFF;
|
||||
font-family: Roboto;
|
||||
font-size: 14px;
|
||||
}
|
||||
#footer a {
|
||||
text-decoration: none;
|
||||
color: #393C44;
|
||||
transition: all .2s cubic-bezier(.05,.03,.35,1);
|
||||
}
|
||||
#footer a:hover {
|
||||
opacity: .5;
|
||||
}
|
||||
#footer span {
|
||||
color: #808292;
|
||||
}
|
||||
#footer p {
|
||||
display: inline-block;
|
||||
color: #808292;
|
||||
}
|
||||
#footer img {
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
.blockelem:first-child {
|
||||
margin-top: 20px
|
||||
}
|
||||
.blockelem {
|
||||
padding-top: 10px;
|
||||
width: 318px;
|
||||
border: 1px solid transparent;
|
||||
transition-property: box-shadow, height;
|
||||
transition-duration: .2s;
|
||||
transition-timing-function: cubic-bezier(.05,.03,.35,1);
|
||||
border-radius: 5px;
|
||||
box-shadow: 0px 0px 30px rgba(22, 33, 74, 0);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.blockelem:hover {
|
||||
box-shadow: 0px 4px 30px rgba(22, 33, 74, 0.08);
|
||||
border-radius: 5px;
|
||||
background-color: #FFF;
|
||||
cursor: pointer;
|
||||
}
|
||||
.grabme, .blockico {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.grabme {
|
||||
margin-top: 10px;
|
||||
margin-left: 10px;
|
||||
margin-bottom: -14px;
|
||||
width: 15px;
|
||||
}
|
||||
#blocklist {
|
||||
height: calc(100% - 220px);
|
||||
overflow: auto;
|
||||
}
|
||||
#proplist {
|
||||
height: calc(100% - 305px);
|
||||
overflow: auto;
|
||||
margin-top: -30px;
|
||||
padding-top: 30px;
|
||||
}
|
||||
.blockin {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin-left: 12px;
|
||||
}
|
||||
.blockico {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background-color: #F1F4FC;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.blockico i {
|
||||
font-size: 24px;
|
||||
color: var(--dark-grey);
|
||||
}
|
||||
|
||||
.blockico span {
|
||||
height: 100%;
|
||||
width: 0px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.blockico img {
|
||||
vertical-align: middle;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
display: inline-block;
|
||||
}
|
||||
.blocktext {
|
||||
display: inline-block;
|
||||
width: 220px;
|
||||
vertical-align: top;
|
||||
margin-left: 12px
|
||||
}
|
||||
.blocktitle {
|
||||
margin: 0px!important;
|
||||
padding: 0px!important;
|
||||
font-family: Roboto;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
color: #393C44;
|
||||
}
|
||||
.blockdesc {
|
||||
margin-top: 5px;
|
||||
font-family: Roboto;
|
||||
color: #808292;
|
||||
font-size: 14px;
|
||||
line-height: 21px;
|
||||
}
|
||||
.blockdisabled {
|
||||
background-color: #F0F2F9;
|
||||
opacity: .5;
|
||||
}
|
||||
#closecard {
|
||||
position: absolute;
|
||||
margin-left: 340px;
|
||||
background-color: #FFF;
|
||||
border-radius: 0px 5px 5px 0px;
|
||||
border-bottom: 1px solid #E8E8EF;
|
||||
border-right: 1px solid #E8E8EF;
|
||||
border-top: 1px solid #E8E8EF;
|
||||
width: 53px;
|
||||
height: 53px;
|
||||
text-align: center;
|
||||
z-index: 10;
|
||||
}
|
||||
#closecard img {
|
||||
margin-top: 15px
|
||||
}
|
||||
#canvas {
|
||||
border: 1px solid green;
|
||||
position: absolute;
|
||||
width: calc(100% - 361px);
|
||||
height: calc(100% - 71px);
|
||||
top: 71px;
|
||||
left: 361px;
|
||||
z-index: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
#propwrap {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: 311px;
|
||||
height: 100%;
|
||||
padding-left: 20px;
|
||||
overflow: hidden;
|
||||
z-index: -2;
|
||||
}
|
||||
#properties {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 311px;
|
||||
background-color: #FFF;
|
||||
right: -150px;
|
||||
opacity: 0;
|
||||
z-index: 2;
|
||||
top: 0px;
|
||||
box-shadow: -4px 0px 40px rgba(26, 26, 73, 0);
|
||||
padding-left: 20px;
|
||||
transition: all .25s cubic-bezier(.05,.03,.35,1);
|
||||
}
|
||||
.itson {
|
||||
z-index: 2!important;
|
||||
}
|
||||
.expanded {
|
||||
right: 0!important;
|
||||
opacity: 1!important;
|
||||
box-shadow: -4px 0px 40px rgba(26, 26, 73, 0.05);
|
||||
z-index: 2;
|
||||
}
|
||||
#header2 {
|
||||
font-size: 20px;
|
||||
font-family: Roboto;
|
||||
font-weight: bold;
|
||||
color: #393C44;
|
||||
margin-top: 101px;
|
||||
}
|
||||
#close {
|
||||
margin-top: 100px;
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
z-index: 9999;
|
||||
transition: all .25s cubic-bezier(.05,.03,.35,1);
|
||||
}
|
||||
#close:hover {
|
||||
cursor: pointer;
|
||||
opacity: .7;
|
||||
}
|
||||
#propswitch {
|
||||
border-bottom: 1px solid #E8E8EF;
|
||||
width: 331px;
|
||||
margin-top: 10px;
|
||||
margin-left: -20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
#dataprop {
|
||||
font-family: Roboto;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
color: #393C44;
|
||||
width: calc(88% / 3);
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
display: inline-block;
|
||||
float: left;
|
||||
margin-left: 20px;
|
||||
}
|
||||
#dataprop:after {
|
||||
display: block;
|
||||
content: "";
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background-color: #217CE8;
|
||||
margin-top: -4px;
|
||||
}
|
||||
#alertprop {
|
||||
display: inline-block;
|
||||
font-family: Roboto;
|
||||
font-weight: 500;
|
||||
color: #808292;
|
||||
font-size: 14px;
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
width: calc(88% / 3);
|
||||
text-align: center;
|
||||
float: left;
|
||||
}
|
||||
#logsprop {
|
||||
width: calc(88% / 3);
|
||||
display: inline-block;
|
||||
font-family: Roboto;
|
||||
font-weight: 500;
|
||||
color: #808292;
|
||||
font-size: 14px;
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
text-align: center;
|
||||
}
|
||||
.inputlabel {
|
||||
font-family: Roboto;
|
||||
font-size: 14px;
|
||||
color: #253134;
|
||||
}
|
||||
.dropme {
|
||||
background-color: #FFF;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #E8E8EF;
|
||||
box-shadow: 0px 2px 8px rgba(34, 34, 87, 0.05);
|
||||
font-family: Roboto;
|
||||
font-size: 14px;
|
||||
color: #253134;
|
||||
text-indent: 20px;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
width: 287px;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
.dropme img {
|
||||
margin-top: 17px;
|
||||
float: right;
|
||||
margin-right: 15px;
|
||||
}
|
||||
.checkus {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.checkus img {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.checkus p {
|
||||
display: inline-block;
|
||||
font-family: Roboto;
|
||||
font-size: 14px;
|
||||
vertical-align: middle;
|
||||
margin-left: 10px;
|
||||
}
|
||||
#divisionthing {
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
background-color: #E8E8EF;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
bottom: 80;
|
||||
}
|
||||
#removeblock {
|
||||
border-radius: 5px;
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
font-family: Roboto;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
width: 287px;
|
||||
height: 38px;
|
||||
line-height: 38px;
|
||||
color: #253134;
|
||||
border: 1px solid #E8E8EF;
|
||||
transition: all .3s cubic-bezier(.05,.03,.35,1);
|
||||
}
|
||||
#removeblock:hover {
|
||||
cursor: pointer;
|
||||
opacity: .5;
|
||||
}
|
||||
.noselect {
|
||||
-webkit-touch-callout: none; /* iOS Safari */
|
||||
-webkit-user-select: none; /* Safari */
|
||||
-khtml-user-select: none; /* Konqueror HTML */
|
||||
-moz-user-select: none; /* Old versions of Firefox */
|
||||
-ms-user-select: none; /* Internet Explorer/Edge */
|
||||
user-select: none; /* Non-prefixed version, currently
|
||||
supported by Chrome, Opera and Firefox */
|
||||
}
|
||||
.blockyname {
|
||||
font-family: Roboto;
|
||||
font-weight: 500;
|
||||
color: #253134;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-left: 8px;
|
||||
font-size: 16px;
|
||||
}
|
||||
.blockyleft img {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.blockyright {
|
||||
display: inline-block;
|
||||
float: right;
|
||||
vertical-align: middle;
|
||||
margin-right: 20px;
|
||||
margin-top: 10px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
background-color: #FFF;
|
||||
transition: all .3s cubic-bezier(.05,.03,.35,1);
|
||||
z-index: 10;
|
||||
}
|
||||
.blockyright:hover {
|
||||
background-color: #F1F4FC;
|
||||
cursor: pointer;
|
||||
}
|
||||
.blockyright img {
|
||||
margin-top: 12px;
|
||||
}
|
||||
.blockyleft {
|
||||
display: inline-block;
|
||||
margin-left: 20px;
|
||||
}
|
||||
.blockydiv {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: #E9E9EF;
|
||||
}
|
||||
.blockyinfo {
|
||||
font-family: Roboto;
|
||||
font-size: 14px;
|
||||
color: #808292;
|
||||
margin-top: 15px;
|
||||
text-indent: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.blockyinfo span {
|
||||
color: #253134;
|
||||
font-weight: 500;
|
||||
display: inline-block;
|
||||
border-bottom: 1px solid #D3DCEA;
|
||||
line-height: 20px;
|
||||
text-indent: 0px;
|
||||
}
|
||||
.block {
|
||||
background-color: #FFF;
|
||||
margin-top: 0px!important;
|
||||
box-shadow: 0px 4px 30px rgba(22, 33, 74, 0.05);
|
||||
}
|
||||
.selectedblock {
|
||||
border: 2px solid #217CE8;
|
||||
box-shadow: 0px 4px 30px rgba(22, 33, 74, 0.08);
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 832px) {
|
||||
#centerswitch {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@media only screen and (max-width: 560px) {
|
||||
#names {
|
||||
display: none;
|
||||
}
|
||||
}
|
|
@ -74,6 +74,17 @@
|
|||
|
||||
--background-button: #f9f9f9;
|
||||
--button-text: #0055ff;
|
||||
|
||||
/* Budibase Styleguide Colors */
|
||||
--primary: #0055ff;
|
||||
--secondary: #f1f4fc;
|
||||
--color: #393c44;
|
||||
--light-grey: #fbfbfb;
|
||||
--dark-grey: #808192;
|
||||
--medium-grey: #e8e8ef;
|
||||
--background: rgb(251, 251, 251);
|
||||
--font: #393c44;
|
||||
--coral: #eb5757;
|
||||
}
|
||||
|
||||
html, body {
|
||||
|
|
|
@ -87,6 +87,11 @@
|
|||
flex-direction: column;
|
||||
}
|
||||
|
||||
a {
|
||||
text-transform: none;
|
||||
color: var(--ink-lighter);
|
||||
}
|
||||
|
||||
.top-nav {
|
||||
flex: 0 0 auto;
|
||||
height: 60px;
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
<script>
|
||||
store.setCurrentPage($params.page)
|
||||
</script>
|
|
@ -0,0 +1,42 @@
|
|||
<script>
|
||||
import { workflowStore } from "builderStore"
|
||||
import { WorkflowPanel, SetupPanel } from "components/workflow"
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
<div class="nav">
|
||||
<WorkflowPanel />
|
||||
</div>
|
||||
<div class="content">
|
||||
<slot />
|
||||
</div>
|
||||
<div class="nav">
|
||||
<SetupPanel />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.content {
|
||||
position: relative;
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
.root {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1 1 auto;
|
||||
margin: 20px 40px;
|
||||
}
|
||||
|
||||
.nav {
|
||||
padding: 20px;
|
||||
overflow: auto;
|
||||
width: 275px;
|
||||
border: 1px solid var(--medium-grey);
|
||||
background: var(--white);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,5 @@
|
|||
<script>
|
||||
import { WorkflowBuilder } from "components/workflow"
|
||||
</script>
|
||||
|
||||
<WorkflowBuilder />
|
|
@ -1,6 +1,5 @@
|
|||
import { createProps } from "../src/components/userInterface/pagesParsing/createProps"
|
||||
import { keys, some } from "lodash/fp"
|
||||
import { BB_STATE_BINDINGPATH } from "@budibase/client/src/state/parseBinding"
|
||||
import { stripStandardProps } from "./testData"
|
||||
|
||||
describe("createDefaultProps", () => {
|
||||
|
@ -94,17 +93,6 @@ describe("createDefaultProps", () => {
|
|||
expect(props.onClick).toEqual([])
|
||||
})
|
||||
|
||||
it("should create a object with empty state when prop def is 'state' ", () => {
|
||||
const comp = getcomponent()
|
||||
comp.props.data = "state"
|
||||
|
||||
const { props, errors } = createProps(comp)
|
||||
|
||||
expect(errors).toEqual([])
|
||||
expect(props.data[BB_STATE_BINDINGPATH]).toBeDefined()
|
||||
expect(props.data[BB_STATE_BINDINGPATH]).toBe("")
|
||||
})
|
||||
|
||||
it("should create a object children array when children == true ", () => {
|
||||
const comp = getcomponent()
|
||||
comp.children = true
|
||||
|
|
|
@ -16,6 +16,5 @@
|
|||
},
|
||||
"_code": ""
|
||||
},
|
||||
"_css": "",
|
||||
"uiFunctions": ""
|
||||
"_css": ""
|
||||
}
|
||||
|
|
|
@ -16,6 +16,5 @@
|
|||
},
|
||||
"_code": ""
|
||||
},
|
||||
"_css": "",
|
||||
"uiFunctions": ""
|
||||
"_css": ""
|
||||
}
|
||||
|
|
|
@ -16,6 +16,5 @@
|
|||
},
|
||||
"_code": ""
|
||||
},
|
||||
"_css": "",
|
||||
"uiFunctions": ""
|
||||
"_css": ""
|
||||
}
|
||||
|
|
|
@ -39,6 +39,7 @@
|
|||
"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"
|
||||
|
|
|
@ -1,43 +1,33 @@
|
|||
import { ERROR } from "../state/standardState"
|
||||
import { loadRecord } from "./loadRecord"
|
||||
import { listRecords } from "./listRecords"
|
||||
import { authenticate } from "./authenticate"
|
||||
import { saveRecord } from "./saveRecord"
|
||||
import { triggerWorkflow } from "./workflow"
|
||||
|
||||
export const createApi = ({ rootPath = "", setState, getState }) => {
|
||||
const apiCall = method => ({
|
||||
url,
|
||||
body,
|
||||
notFound,
|
||||
badRequest,
|
||||
forbidden,
|
||||
}) => {
|
||||
return fetch(`${rootPath}${url}`, {
|
||||
const apiCall = method => async ({ url, body }) => {
|
||||
const response = await fetch(`${rootPath}${url}`, {
|
||||
method: method,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: body && JSON.stringify(body),
|
||||
credentials: "same-origin",
|
||||
}).then(r => {
|
||||
switch (r.status) {
|
||||
case 200:
|
||||
return r.json()
|
||||
case 404:
|
||||
return error(notFound || `${url} Not found`)
|
||||
case 400:
|
||||
return error(badRequest || `${url} Bad Request`)
|
||||
case 403:
|
||||
return error(forbidden || `${url} Forbidden`)
|
||||
default:
|
||||
if (
|
||||
r.status.toString().startsWith("2") ||
|
||||
r.status.toString().startsWith("3")
|
||||
)
|
||||
return r.json()
|
||||
else return error(`${url} - ${r.statusText}`)
|
||||
}
|
||||
})
|
||||
|
||||
switch (response.status) {
|
||||
case 200:
|
||||
return response.json()
|
||||
case 404:
|
||||
return error(`${url} Not found`)
|
||||
case 400:
|
||||
return error(`${url} Bad Request`)
|
||||
case 403:
|
||||
return error(`${url} Forbidden`)
|
||||
default:
|
||||
if (response.status >= 200 && response.status < 400) {
|
||||
return response.json()
|
||||
}
|
||||
|
||||
return error(`${url} - ${response.statusText}`)
|
||||
}
|
||||
}
|
||||
|
||||
const post = apiCall("POST")
|
||||
|
@ -47,10 +37,9 @@ export const createApi = ({ rootPath = "", setState, getState }) => {
|
|||
|
||||
const ERROR_MEMBER = "##error"
|
||||
const error = message => {
|
||||
const e = {}
|
||||
e[ERROR_MEMBER] = message
|
||||
setState(ERROR, message)
|
||||
return e
|
||||
const err = { [ERROR_MEMBER]: message }
|
||||
setState("##error_message", message)
|
||||
return err
|
||||
}
|
||||
|
||||
const isSuccess = obj => !obj || !obj[ERROR_MEMBER]
|
||||
|
@ -68,9 +57,7 @@ export const createApi = ({ rootPath = "", setState, getState }) => {
|
|||
}
|
||||
|
||||
return {
|
||||
loadRecord: loadRecord(apiOpts),
|
||||
listRecords: listRecords(apiOpts),
|
||||
authenticate: authenticate(apiOpts),
|
||||
saveRecord: saveRecord(apiOpts),
|
||||
triggerWorkflow: triggerWorkflow(apiOpts),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
import { trimSlash } from "../common/trimSlash"
|
||||
|
||||
export const listRecords = api => async ({ indexKey, statePath }) => {
|
||||
if (!indexKey) {
|
||||
api.error("Load Record: record key not set")
|
||||
return
|
||||
}
|
||||
|
||||
if (!statePath) {
|
||||
api.error("Load Record: state path not set")
|
||||
return
|
||||
}
|
||||
|
||||
const records = await api.get({
|
||||
url: `/api/listRecords/${trimSlash(indexKey)}`,
|
||||
})
|
||||
|
||||
if (api.isSuccess(records)) api.setState(statePath, records)
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
import { trimSlash } from "../common/trimSlash"
|
||||
|
||||
export const loadRecord = api => async ({ recordKey, statePath }) => {
|
||||
if (!recordKey) {
|
||||
api.error("Load Record: record key not set")
|
||||
return
|
||||
}
|
||||
|
||||
if (!statePath) {
|
||||
api.error("Load Record: state path not set")
|
||||
return
|
||||
}
|
||||
|
||||
const record = await api.get({
|
||||
url: `/api/record/${trimSlash(recordKey)}`,
|
||||
})
|
||||
|
||||
if (api.isSuccess(record)) api.setState(statePath, record)
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
import { trimSlash } from "../common/trimSlash"
|
||||
|
||||
export const saveRecord = api => async ({ statePath }) => {
|
||||
if (!statePath) {
|
||||
api.error("Load Record: state path not set")
|
||||
return
|
||||
}
|
||||
|
||||
const recordtoSave = api.getState(statePath)
|
||||
|
||||
if (!recordtoSave) {
|
||||
api.error(`there is no record in state: ${statePath}`)
|
||||
return
|
||||
}
|
||||
|
||||
if (!recordtoSave.key) {
|
||||
api.error(
|
||||
`item in state does not appear to be a record - it has no key (${statePath})`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const savedRecord = await api.post({
|
||||
url: `/api/record/${trimSlash(recordtoSave.key)}`,
|
||||
body: recordtoSave,
|
||||
})
|
||||
|
||||
if (api.isSuccess(savedRecord)) api.setState(statePath, savedRecord)
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
import { setState } from "../../state/setState"
|
||||
|
||||
const delay = ms => new Promise(resolve => setTimeout(resolve, ms))
|
||||
|
||||
export default {
|
||||
SET_STATE: ({ context, args, id }) => {
|
||||
setState(...Object.values(args))
|
||||
context = {
|
||||
...context,
|
||||
[id]: args,
|
||||
}
|
||||
return context
|
||||
},
|
||||
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
|
||||
}
|
||||
},
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
import { get } from "svelte/store"
|
||||
import mustache from "mustache"
|
||||
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] = mustache.render(argValue, {
|
||||
context: this.context,
|
||||
state: get(appStore),
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
/**
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export const trimSlash = str => str.replace(/^\/+|\/+$/g, "")
|
|
@ -1,27 +1,22 @@
|
|||
import { writable } from "svelte/store"
|
||||
import { attachChildren } from "./render/attachChildren"
|
||||
import { createTreeNode } from "./render/prepareRenderComponent"
|
||||
import { screenRouter } from "./render/screenRouter"
|
||||
import { createStateManager } from "./state/stateManager"
|
||||
|
||||
export const createApp = (
|
||||
export const createApp = ({
|
||||
componentLibraries,
|
||||
frontendDefinition,
|
||||
user,
|
||||
uiFunctions,
|
||||
window
|
||||
) => {
|
||||
window,
|
||||
}) => {
|
||||
let routeTo
|
||||
let currentUrl
|
||||
let screenStateManager
|
||||
|
||||
const onScreenSlotRendered = screenSlotNode => {
|
||||
const onScreenSelected = (screen, store, url) => {
|
||||
const onScreenSelected = (screen, url) => {
|
||||
const stateManager = createStateManager({
|
||||
store,
|
||||
frontendDefinition,
|
||||
componentLibraries,
|
||||
uiFunctions,
|
||||
onScreenSlotRendered: () => {},
|
||||
routeTo,
|
||||
appRootPath: frontendDefinition.appRootPath,
|
||||
|
@ -38,11 +33,11 @@ export const createApp = (
|
|||
currentUrl = url
|
||||
}
|
||||
|
||||
routeTo = screenRouter(
|
||||
frontendDefinition.screens,
|
||||
routeTo = screenRouter({
|
||||
screens: frontendDefinition.screens,
|
||||
onScreenSelected,
|
||||
frontendDefinition.appRootPath
|
||||
)
|
||||
appRootPath: frontendDefinition.appRootPath,
|
||||
})
|
||||
const fallbackPath = window.location.pathname.replace(
|
||||
frontendDefinition.appRootPath,
|
||||
""
|
||||
|
@ -53,7 +48,6 @@ export const createApp = (
|
|||
const attachChildrenParams = stateManager => {
|
||||
const getInitialiseParams = treeNode => ({
|
||||
componentLibraries,
|
||||
uiFunctions,
|
||||
treeNode,
|
||||
onScreenSlotRendered,
|
||||
setupState: stateManager.setup,
|
||||
|
@ -65,10 +59,8 @@ export const createApp = (
|
|||
|
||||
let rootTreeNode
|
||||
const pageStateManager = createStateManager({
|
||||
store: writable({ _bbuser: user }),
|
||||
frontendDefinition,
|
||||
componentLibraries,
|
||||
uiFunctions,
|
||||
onScreenSlotRendered,
|
||||
appRootPath: frontendDefinition.appRootPath,
|
||||
// seems weird, but the routeTo variable may not be available at this point
|
||||
|
@ -82,7 +74,6 @@ export const createApp = (
|
|||
rootTreeNode.props = {
|
||||
_children: [page.props],
|
||||
}
|
||||
rootTreeNode.rootElement = target
|
||||
const getInitialiseParams = attachChildrenParams(pageStateManager)
|
||||
const initChildParams = getInitialiseParams(rootTreeNode)
|
||||
|
||||
|
|
|
@ -10,9 +10,7 @@ export const loadBudibase = async opts => {
|
|||
// const _localStorage = (opts && opts.localStorage) || localStorage
|
||||
|
||||
const frontendDefinition = _window["##BUDIBASE_FRONTEND_DEFINITION##"]
|
||||
const uiFunctions = _window["##BUDIBASE_FRONTEND_FUNCTIONS##"]
|
||||
|
||||
// TODO: update
|
||||
const user = {}
|
||||
|
||||
const componentLibraryModules = (opts && opts.componentLibraries) || {}
|
||||
|
@ -36,14 +34,12 @@ export const loadBudibase = async opts => {
|
|||
pageStore,
|
||||
routeTo,
|
||||
rootNode,
|
||||
} = createApp(
|
||||
componentLibraryModules,
|
||||
} = createApp({
|
||||
componentLibraries: componentLibraryModules,
|
||||
frontendDefinition,
|
||||
user,
|
||||
uiFunctions || {},
|
||||
_window,
|
||||
rootNode
|
||||
)
|
||||
window,
|
||||
})
|
||||
|
||||
const route = _window.location
|
||||
? _window.location.pathname.replace(frontendDefinition.appRootPath, "")
|
||||
|
|
|
@ -5,12 +5,10 @@ import deepEqual from "deep-equal"
|
|||
|
||||
export const attachChildren = initialiseOpts => (htmlElement, options) => {
|
||||
const {
|
||||
uiFunctions,
|
||||
componentLibraries,
|
||||
treeNode,
|
||||
onScreenSlotRendered,
|
||||
setupState,
|
||||
getCurrentState,
|
||||
} = initialiseOpts
|
||||
|
||||
const anchor = options && options.anchor ? options.anchor : null
|
||||
|
@ -31,8 +29,6 @@ export const attachChildren = initialiseOpts => (htmlElement, options) => {
|
|||
}
|
||||
}
|
||||
|
||||
// htmlElement.classList.add(`lay-${treeNode.props._id}`)
|
||||
|
||||
const childNodes = []
|
||||
for (let childProps of treeNode.props._children) {
|
||||
const { componentName, libName } = splitName(childProps._component)
|
||||
|
@ -45,10 +41,8 @@ export const attachChildren = initialiseOpts => (htmlElement, options) => {
|
|||
props: childProps,
|
||||
parentNode: treeNode,
|
||||
ComponentConstructor,
|
||||
uiFunctions,
|
||||
htmlElement,
|
||||
anchor,
|
||||
getCurrentState,
|
||||
})
|
||||
|
||||
for (let childNode of childNodesThisIteration) {
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
import { appStore } from "../state/store"
|
||||
import mustache from "mustache"
|
||||
|
||||
export const prepareRenderComponent = ({
|
||||
ComponentConstructor,
|
||||
uiFunctions,
|
||||
htmlElement,
|
||||
anchor,
|
||||
props,
|
||||
parentNode,
|
||||
getCurrentState,
|
||||
}) => {
|
||||
const func = props._id ? uiFunctions[props._id] : undefined
|
||||
|
||||
const parentContext = (parentNode && parentNode.context) || {}
|
||||
|
||||
let nodesToRender = []
|
||||
|
@ -39,16 +38,27 @@ export const prepareRenderComponent = ({
|
|||
if (props._id && thisNode.rootElement) {
|
||||
thisNode.rootElement.classList.add(`${componentName}-${props._id}`)
|
||||
}
|
||||
|
||||
// make this node listen to the store
|
||||
if (thisNode.stateBound) {
|
||||
const unsubscribe = appStore.subscribe(state => {
|
||||
const storeBoundProps = { ...initialProps._bb.props }
|
||||
for (let prop in storeBoundProps) {
|
||||
const propValue = storeBoundProps[prop]
|
||||
if (typeof propValue === "string") {
|
||||
storeBoundProps[prop] = mustache.render(propValue, {
|
||||
state,
|
||||
})
|
||||
}
|
||||
}
|
||||
thisNode.component.$set(storeBoundProps)
|
||||
})
|
||||
thisNode.unsubscribe = unsubscribe
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (func) {
|
||||
const state = getCurrentState()
|
||||
const routeParams = state["##routeParams"]
|
||||
func(createNodeAndRender, parentContext, getCurrentState(), routeParams)
|
||||
} else {
|
||||
createNodeAndRender()
|
||||
}
|
||||
|
||||
return nodesToRender
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import regexparam from "regexparam"
|
||||
import { writable } from "svelte/store"
|
||||
import { routerStore } from "../state/store"
|
||||
|
||||
export const screenRouter = (screens, onScreenSelected, appRootPath) => {
|
||||
export const screenRouter = ({ screens, onScreenSelected, appRootPath }) => {
|
||||
const makeRootedPath = url => {
|
||||
if (appRootPath) {
|
||||
if (url) return `${appRootPath}${url.startsWith("/") ? "" : "/"}${url}`
|
||||
|
@ -40,13 +40,14 @@ export const screenRouter = (screens, onScreenSelected, appRootPath) => {
|
|||
})
|
||||
}
|
||||
|
||||
const storeInitial = {}
|
||||
storeInitial["##routeParams"] = params
|
||||
const store = writable(storeInitial)
|
||||
routerStore.update(state => {
|
||||
state["##routeParams"] = params
|
||||
return state
|
||||
})
|
||||
|
||||
const screenIndex = current !== -1 ? current : fallback
|
||||
|
||||
onScreenSelected(screens[screenIndex], store, _url)
|
||||
onScreenSelected(screens[screenIndex], _url)
|
||||
|
||||
try {
|
||||
!url.state && history.pushState(_url, null, _url)
|
||||
|
@ -55,29 +56,8 @@ export const screenRouter = (screens, onScreenSelected, appRootPath) => {
|
|||
}
|
||||
}
|
||||
|
||||
function click(e) {
|
||||
const x = e.target.closest("a")
|
||||
const y = x && x.getAttribute("href")
|
||||
|
||||
if (
|
||||
e.ctrlKey ||
|
||||
e.metaKey ||
|
||||
e.altKey ||
|
||||
e.shiftKey ||
|
||||
e.button ||
|
||||
e.defaultPrevented
|
||||
)
|
||||
return
|
||||
|
||||
if (!y || x.target || x.host !== location.host) return
|
||||
|
||||
e.preventDefault()
|
||||
route(y)
|
||||
}
|
||||
|
||||
addEventListener("popstate", route)
|
||||
addEventListener("pushstate", route)
|
||||
addEventListener("click", click)
|
||||
|
||||
return route
|
||||
}
|
||||
|
|
|
@ -1,16 +1,13 @@
|
|||
import { getStateOrValue } from "./getState"
|
||||
import { setState, setStateFromBinding } from "./setState"
|
||||
import { trimSlash } from "../common/trimSlash"
|
||||
import { isBound } from "./parseBinding"
|
||||
import { setState } from "./setState"
|
||||
import { attachChildren } from "../render/attachChildren"
|
||||
import { getContext, setContext } from "./getSetContext"
|
||||
|
||||
export const trimSlash = str => str.replace(/^\/+|\/+$/g, "")
|
||||
|
||||
export const bbFactory = ({
|
||||
store,
|
||||
getCurrentState,
|
||||
frontendDefinition,
|
||||
componentLibraries,
|
||||
uiFunctions,
|
||||
onScreenSlotRendered,
|
||||
}) => {
|
||||
const relativeUrl = url => {
|
||||
|
@ -51,11 +48,9 @@ export const bbFactory = ({
|
|||
return (treeNode, setupState) => {
|
||||
const attachParams = {
|
||||
componentLibraries,
|
||||
uiFunctions,
|
||||
treeNode,
|
||||
onScreenSlotRendered,
|
||||
setupState,
|
||||
getCurrentState,
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -63,17 +58,12 @@ export const bbFactory = ({
|
|||
context: treeNode.context,
|
||||
props: treeNode.props,
|
||||
call: safeCallEvent,
|
||||
setStateFromBinding: (binding, value) =>
|
||||
setStateFromBinding(store, binding, value),
|
||||
setState: (path, value) => setState(store, path, value),
|
||||
getStateOrValue: (prop, currentContext) =>
|
||||
getStateOrValue(getCurrentState(), prop, currentContext),
|
||||
setState,
|
||||
getContext: getContext(treeNode),
|
||||
setContext: setContext(treeNode),
|
||||
store: store,
|
||||
relativeUrl,
|
||||
api,
|
||||
isBound,
|
||||
parent,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,71 +0,0 @@
|
|||
import { ERROR } from "./standardState"
|
||||
|
||||
export const getNewChildRecordToState = (coreApi, setState) => ({
|
||||
recordKey,
|
||||
collectionName,
|
||||
childRecordType,
|
||||
statePath,
|
||||
}) => {
|
||||
const error = errorHandler(setState)
|
||||
try {
|
||||
if (!recordKey) {
|
||||
error("getNewChild > recordKey not set")
|
||||
return
|
||||
}
|
||||
|
||||
if (!collectionName) {
|
||||
error("getNewChild > collectionName not set")
|
||||
return
|
||||
}
|
||||
|
||||
if (!childRecordType) {
|
||||
error("getNewChild > childRecordType not set")
|
||||
return
|
||||
}
|
||||
|
||||
if (!statePath) {
|
||||
error("getNewChild > statePath not set")
|
||||
return
|
||||
}
|
||||
|
||||
const rec = coreApi.recordApi.getNewChild(
|
||||
recordKey,
|
||||
collectionName,
|
||||
childRecordType
|
||||
)
|
||||
setState(statePath, rec)
|
||||
} catch (e) {
|
||||
error(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
export const getNewRecordToState = (coreApi, setState) => ({
|
||||
collectionKey,
|
||||
childRecordType,
|
||||
statePath,
|
||||
}) => {
|
||||
const error = errorHandler(setState)
|
||||
try {
|
||||
if (!collectionKey) {
|
||||
error("getNewChild > collectionKey not set")
|
||||
return
|
||||
}
|
||||
|
||||
if (!childRecordType) {
|
||||
error("getNewChild > childRecordType not set")
|
||||
return
|
||||
}
|
||||
|
||||
if (!statePath) {
|
||||
error("getNewChild > statePath not set")
|
||||
return
|
||||
}
|
||||
|
||||
const rec = coreApi.recordApi.getNew(collectionKey, childRecordType)
|
||||
setState(statePath, rec)
|
||||
} catch (e) {
|
||||
error(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
const errorHandler = setState => message => setState(ERROR, message)
|
|
@ -6,35 +6,24 @@ import { createApi } from "../api"
|
|||
|
||||
export const EVENT_TYPE_MEMBER_NAME = "##eventHandlerType"
|
||||
|
||||
export const eventHandlers = (store, rootPath, routeTo) => {
|
||||
export const eventHandlers = (rootPath, routeTo) => {
|
||||
const handler = (parameters, execute) => ({
|
||||
execute,
|
||||
parameters,
|
||||
})
|
||||
|
||||
const setStateWithStore = (path, value) => setState(store, path, value)
|
||||
|
||||
let currentState
|
||||
store.subscribe(state => {
|
||||
currentState = state
|
||||
})
|
||||
|
||||
const api = createApi({
|
||||
rootPath,
|
||||
setState: setStateWithStore,
|
||||
getState: (path, fallback) => getState(currentState, path, fallback),
|
||||
setState,
|
||||
getState: (path, fallback) => getState(path, fallback),
|
||||
})
|
||||
|
||||
const setStateHandler = ({ path, value }) => setState(store, path, value)
|
||||
const setStateHandler = ({ path, value }) => setState(path, value)
|
||||
|
||||
return {
|
||||
"Set State": handler(["path", "value"], setStateHandler),
|
||||
"Load Record": handler(["recordKey", "statePath"], api.loadRecord),
|
||||
"List Records": handler(["indexKey", "statePath"], api.listRecords),
|
||||
"Save Record": handler(["statePath"], api.saveRecord),
|
||||
"Navigate To": handler(["url"], param => routeTo(param && param.url)),
|
||||
|
||||
Authenticate: handler(["username", "password"], api.authenticate),
|
||||
"Trigger Workflow": handler(["workflow"], api.triggerWorkflow),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,46 +1,9 @@
|
|||
import { isUndefined, isObject } from "lodash/fp"
|
||||
import { parseBinding, isStoreBinding } from "./parseBinding"
|
||||
import { get } from "svelte/store"
|
||||
import getOr from "lodash/fp/getOr"
|
||||
import { appStore } from "./store"
|
||||
|
||||
export const getState = (s, path, fallback) => {
|
||||
if (!s) return fallback
|
||||
export const getState = (path, fallback) => {
|
||||
if (!path || path.length === 0) return fallback
|
||||
|
||||
if (path === "$") return s
|
||||
|
||||
const pathParts = path.split(".")
|
||||
const safeGetPath = (obj, currentPartIndex = 0) => {
|
||||
const currentKey = pathParts[currentPartIndex]
|
||||
|
||||
if (pathParts.length - 1 == currentPartIndex) {
|
||||
const value = obj[currentKey]
|
||||
if (isUndefined(value)) return fallback
|
||||
else return value
|
||||
}
|
||||
|
||||
if (
|
||||
obj[currentKey] === null ||
|
||||
obj[currentKey] === undefined ||
|
||||
!isObject(obj[currentKey])
|
||||
) {
|
||||
return fallback
|
||||
}
|
||||
|
||||
return safeGetPath(obj[currentKey], currentPartIndex + 1)
|
||||
}
|
||||
|
||||
return safeGetPath(s)
|
||||
}
|
||||
|
||||
export const getStateOrValue = (globalState, prop, currentContext) => {
|
||||
if (!prop) return prop
|
||||
|
||||
const binding = parseBinding(prop)
|
||||
|
||||
if (binding) {
|
||||
const stateToUse = isStoreBinding(binding) ? globalState : currentContext
|
||||
|
||||
return getState(stateToUse, binding.path, binding.fallback)
|
||||
}
|
||||
|
||||
return prop
|
||||
return getOr(fallback, path, get(appStore))
|
||||
}
|
||||
|
|
|
@ -1,67 +0,0 @@
|
|||
export const BB_STATE_BINDINGPATH = "##bbstate"
|
||||
export const BB_STATE_BINDINGSOURCE = "##bbsource"
|
||||
export const BB_STATE_FALLBACK = "##bbstatefallback"
|
||||
|
||||
export const isBound = prop => !!parseBinding(prop)
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {object|string|number} prop - component property to parse for a dynamic state binding
|
||||
* @returns {object|boolean}
|
||||
*/
|
||||
export const parseBinding = prop => {
|
||||
if (!prop) return false
|
||||
|
||||
if (isBindingExpression(prop)) {
|
||||
return parseBindingExpression(prop)
|
||||
}
|
||||
|
||||
if (isAlreadyBinding(prop)) {
|
||||
return {
|
||||
path: prop.path,
|
||||
source: prop.source || "store",
|
||||
fallback: prop.fallback,
|
||||
}
|
||||
}
|
||||
|
||||
if (hasBindingObject(prop)) {
|
||||
return {
|
||||
path: prop[BB_STATE_BINDINGPATH],
|
||||
fallback: prop[BB_STATE_FALLBACK] || "",
|
||||
source: prop[BB_STATE_BINDINGSOURCE] || "store",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const isStoreBinding = binding => binding && binding.source === "store"
|
||||
export const isContextBinding = binding =>
|
||||
binding && binding.source === "context"
|
||||
export const isEventBinding = binding => binding && binding.source === "event"
|
||||
|
||||
const hasBindingObject = prop =>
|
||||
typeof prop === "object" && prop[BB_STATE_BINDINGPATH] !== undefined
|
||||
|
||||
const isAlreadyBinding = prop => typeof prop === "object" && prop.path
|
||||
|
||||
const isBindingExpression = prop =>
|
||||
typeof prop === "string" &&
|
||||
(prop.startsWith("state.") ||
|
||||
prop.startsWith("context.") ||
|
||||
prop.startsWith("event.") ||
|
||||
prop.startsWith("route."))
|
||||
|
||||
const parseBindingExpression = prop => {
|
||||
let [source, ...rest] = prop.split(".")
|
||||
let path = rest.join(".")
|
||||
|
||||
if (source === "route") {
|
||||
source = "state"
|
||||
path = `##routeParams.${path}`
|
||||
}
|
||||
|
||||
return {
|
||||
fallback: "", // TODO: provide fallback support
|
||||
source,
|
||||
path,
|
||||
}
|
||||
}
|
|
@ -1,38 +1,11 @@
|
|||
import { isObject } from "lodash/fp"
|
||||
import { parseBinding } from "./parseBinding"
|
||||
import set from "lodash/fp/set"
|
||||
import { appStore } from "./store"
|
||||
|
||||
export const setState = (store, path, value) => {
|
||||
export const setState = (path, value) => {
|
||||
if (!path || path.length === 0) return
|
||||
|
||||
const pathParts = path.split(".")
|
||||
|
||||
const safeSetPath = (state, currentPartIndex = 0) => {
|
||||
const currentKey = pathParts[currentPartIndex]
|
||||
|
||||
if (pathParts.length - 1 == currentPartIndex) {
|
||||
state[currentKey] = value
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
state[currentKey] === null ||
|
||||
state[currentKey] === undefined ||
|
||||
!isObject(state[currentKey])
|
||||
) {
|
||||
state[currentKey] = {}
|
||||
}
|
||||
|
||||
safeSetPath(state[currentKey], currentPartIndex + 1)
|
||||
}
|
||||
|
||||
store.update(state => {
|
||||
safeSetPath(state)
|
||||
appStore.update(state => {
|
||||
state = set(path, value, state)
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
export const setStateFromBinding = (store, binding, value) => {
|
||||
const parsedBinding = parseBinding(binding)
|
||||
if (!parsedBinding) return
|
||||
return setState(store, parsedBinding.path, value)
|
||||
}
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
export const ERROR = "##error_message"
|
|
@ -4,10 +4,9 @@ import {
|
|||
EVENT_TYPE_MEMBER_NAME,
|
||||
} from "./eventHandlers"
|
||||
import { bbFactory } from "./bbComponentApi"
|
||||
import { getState } from "./getState"
|
||||
import { attachChildren } from "../render/attachChildren"
|
||||
|
||||
import { parseBinding } from "./parseBinding"
|
||||
import mustache from "mustache"
|
||||
import { get } from "svelte/store"
|
||||
import { appStore } from "./store"
|
||||
|
||||
const doNothing = () => {}
|
||||
doNothing.isPlaceholder = true
|
||||
|
@ -18,182 +17,62 @@ const isMetaProp = propName =>
|
|||
propName === "_id" ||
|
||||
propName === "_style" ||
|
||||
propName === "_code" ||
|
||||
propName === "_codeMeta"
|
||||
propName === "_codeMeta" ||
|
||||
propName === "_styles"
|
||||
|
||||
export const createStateManager = ({
|
||||
store,
|
||||
appRootPath,
|
||||
frontendDefinition,
|
||||
componentLibraries,
|
||||
uiFunctions,
|
||||
onScreenSlotRendered,
|
||||
routeTo,
|
||||
}) => {
|
||||
let handlerTypes = eventHandlers(store, appRootPath, routeTo)
|
||||
let handlerTypes = eventHandlers(appRootPath, routeTo)
|
||||
let currentState
|
||||
|
||||
// any nodes that have props that are bound to the store
|
||||
let nodesBoundByProps = []
|
||||
|
||||
// any node whose children depend on code, that uses the store
|
||||
let nodesWithCodeBoundChildren = []
|
||||
|
||||
const getCurrentState = () => currentState
|
||||
const registerBindings = _registerBindings(
|
||||
nodesBoundByProps,
|
||||
nodesWithCodeBoundChildren
|
||||
)
|
||||
|
||||
const bb = bbFactory({
|
||||
store,
|
||||
store: appStore,
|
||||
getCurrentState,
|
||||
frontendDefinition,
|
||||
componentLibraries,
|
||||
uiFunctions,
|
||||
onScreenSlotRendered,
|
||||
})
|
||||
|
||||
const setup = _setup(handlerTypes, getCurrentState, registerBindings, bb)
|
||||
|
||||
const unsubscribe = store.subscribe(
|
||||
onStoreStateUpdated({
|
||||
setCurrentState: s => (currentState = s),
|
||||
getCurrentState,
|
||||
nodesWithCodeBoundChildren,
|
||||
nodesBoundByProps,
|
||||
uiFunctions,
|
||||
componentLibraries,
|
||||
onScreenSlotRendered,
|
||||
setupState: setup,
|
||||
})
|
||||
)
|
||||
const setup = _setup({ handlerTypes, getCurrentState, bb, store: appStore })
|
||||
|
||||
return {
|
||||
setup,
|
||||
destroy: () => unsubscribe(),
|
||||
destroy: () => {},
|
||||
getCurrentState,
|
||||
store,
|
||||
store: appStore,
|
||||
}
|
||||
}
|
||||
|
||||
const onStoreStateUpdated = ({
|
||||
setCurrentState,
|
||||
getCurrentState,
|
||||
nodesWithCodeBoundChildren,
|
||||
nodesBoundByProps,
|
||||
uiFunctions,
|
||||
componentLibraries,
|
||||
onScreenSlotRendered,
|
||||
setupState,
|
||||
}) => s => {
|
||||
setCurrentState(s)
|
||||
|
||||
// the original array gets changed by components' destroy()
|
||||
// so we make a clone and check if they are still in the original
|
||||
const nodesWithBoundChildren_clone = [...nodesWithCodeBoundChildren]
|
||||
for (let node of nodesWithBoundChildren_clone) {
|
||||
if (!nodesWithCodeBoundChildren.includes(node)) continue
|
||||
attachChildren({
|
||||
uiFunctions,
|
||||
componentLibraries,
|
||||
treeNode: node,
|
||||
onScreenSlotRendered,
|
||||
setupState,
|
||||
getCurrentState,
|
||||
})(node.rootElement, { hydrate: true, force: true })
|
||||
}
|
||||
|
||||
for (let node of nodesBoundByProps) {
|
||||
setNodeState(s, node)
|
||||
}
|
||||
}
|
||||
|
||||
const _registerBindings = (nodesBoundByProps, nodesWithCodeBoundChildren) => (
|
||||
node,
|
||||
bindings
|
||||
) => {
|
||||
if (bindings.length > 0) {
|
||||
node.bindings = bindings
|
||||
nodesBoundByProps.push(node)
|
||||
const onDestroy = () => {
|
||||
nodesBoundByProps = nodesBoundByProps.filter(n => n === node)
|
||||
node.onDestroy = node.onDestroy.filter(d => d === onDestroy)
|
||||
}
|
||||
node.onDestroy.push(onDestroy)
|
||||
}
|
||||
if (
|
||||
node.props._children &&
|
||||
node.props._children.filter(c => c._codeMeta && c._codeMeta.dependsOnStore)
|
||||
.length > 0
|
||||
) {
|
||||
nodesWithCodeBoundChildren.push(node)
|
||||
const onDestroy = () => {
|
||||
nodesWithCodeBoundChildren = nodesWithCodeBoundChildren.filter(
|
||||
n => n === node
|
||||
)
|
||||
node.onDestroy = node.onDestroy.filter(d => d === onDestroy)
|
||||
}
|
||||
node.onDestroy.push(onDestroy)
|
||||
}
|
||||
}
|
||||
|
||||
const setNodeState = (storeState, node) => {
|
||||
if (!node.component) return
|
||||
const newProps = { ...node.bindings.initialProps }
|
||||
|
||||
for (let binding of node.bindings) {
|
||||
const val = getState(storeState, binding.path, binding.fallback)
|
||||
|
||||
if (val === undefined && newProps[binding.propName] !== undefined) {
|
||||
delete newProps[binding.propName]
|
||||
}
|
||||
|
||||
if (val !== undefined) {
|
||||
newProps[binding.propName] = val
|
||||
}
|
||||
}
|
||||
|
||||
node.component.$set(newProps)
|
||||
}
|
||||
|
||||
const _setup = (
|
||||
handlerTypes,
|
||||
getCurrentState,
|
||||
registerBindings,
|
||||
bb
|
||||
) => node => {
|
||||
const _setup = ({ handlerTypes, getCurrentState, bb, store }) => node => {
|
||||
const props = node.props
|
||||
const context = node.context || {}
|
||||
const initialProps = { ...props }
|
||||
const storeBoundProps = []
|
||||
const currentStoreState = getCurrentState()
|
||||
const currentStoreState = get(appStore)
|
||||
|
||||
for (let propName in props) {
|
||||
if (isMetaProp(propName)) continue
|
||||
|
||||
const propValue = props[propName]
|
||||
|
||||
const binding = parseBinding(propValue)
|
||||
const isBound = !!binding
|
||||
// A little bit of a hack - won't bind if the string doesn't start with {{
|
||||
const isBound = typeof propValue === "string" && propValue.startsWith("{{")
|
||||
|
||||
if (isBound) binding.propName = propName
|
||||
if (isBound) {
|
||||
initialProps[propName] = mustache.render(propValue, {
|
||||
state: currentStoreState,
|
||||
context,
|
||||
})
|
||||
|
||||
if (isBound && binding.source === "state") {
|
||||
storeBoundProps.push(binding)
|
||||
|
||||
initialProps[propName] = !currentStoreState
|
||||
? binding.fallback
|
||||
: getState(
|
||||
currentStoreState,
|
||||
binding.path,
|
||||
binding.fallback,
|
||||
binding.source
|
||||
)
|
||||
if (!node.stateBound) {
|
||||
node.stateBound = true
|
||||
}
|
||||
|
||||
if (isBound && binding.source === "context") {
|
||||
initialProps[propName] = !context
|
||||
? propValue
|
||||
: getState(context, binding.path, binding.fallback, binding.source)
|
||||
}
|
||||
|
||||
if (isEventType(propValue)) {
|
||||
|
@ -203,33 +82,24 @@ const _setup = (
|
|||
handlerType: event[EVENT_TYPE_MEMBER_NAME],
|
||||
parameters: event.parameters,
|
||||
}
|
||||
|
||||
const resolvedParams = {}
|
||||
for (let paramName in handlerInfo.parameters) {
|
||||
const paramValue = handlerInfo.parameters[paramName]
|
||||
const paramBinding = parseBinding(paramValue)
|
||||
if (!paramBinding) {
|
||||
resolvedParams[paramName] = () => paramValue
|
||||
continue
|
||||
}
|
||||
|
||||
let paramValueSource
|
||||
|
||||
if (paramBinding.source === "context") paramValueSource = context
|
||||
if (paramBinding.source === "state")
|
||||
paramValueSource = getCurrentState()
|
||||
if (paramBinding.source === "context") paramValueSource = context
|
||||
|
||||
// The new dynamic event parameter bound to the relevant source
|
||||
resolvedParams[paramName] = () =>
|
||||
getState(paramValueSource, paramBinding.path, paramBinding.fallback)
|
||||
mustache.render(paramValue, {
|
||||
state: getCurrentState(),
|
||||
context,
|
||||
})
|
||||
}
|
||||
|
||||
handlerInfo.parameters = resolvedParams
|
||||
handlersInfos.push(handlerInfo)
|
||||
}
|
||||
|
||||
if (handlersInfos.length === 0) initialProps[propName] = doNothing
|
||||
else {
|
||||
if (handlersInfos.length === 0) {
|
||||
initialProps[propName] = doNothing
|
||||
} else {
|
||||
initialProps[propName] = async context => {
|
||||
for (let handlerInfo of handlersInfos) {
|
||||
const handler = makeHandler(handlerTypes, handlerInfo)
|
||||
|
@ -240,9 +110,7 @@ const _setup = (
|
|||
}
|
||||
}
|
||||
|
||||
registerBindings(node, storeBoundProps)
|
||||
|
||||
const setup = _setup(handlerTypes, getCurrentState, registerBindings, bb)
|
||||
const setup = _setup({ handlerTypes, getCurrentState, bb, store })
|
||||
initialProps._bb = bb(node, setup)
|
||||
|
||||
return initialProps
|
||||
|
|
|
@ -1,283 +0,0 @@
|
|||
import {
|
||||
isEventType,
|
||||
eventHandlers,
|
||||
EVENT_TYPE_MEMBER_NAME,
|
||||
} from "./eventHandlers"
|
||||
import { bbFactory } from "./bbComponentApi"
|
||||
import { getState } from "./getState"
|
||||
import { attachChildren } from "../render/attachChildren"
|
||||
|
||||
import { parseBinding } from "./parseBinding"
|
||||
|
||||
const doNothing = () => {}
|
||||
doNothing.isPlaceholder = true
|
||||
|
||||
const isMetaProp = propName =>
|
||||
propName === "_component" ||
|
||||
propName === "_children" ||
|
||||
propName === "_id" ||
|
||||
propName === "_style" ||
|
||||
propName === "_code" ||
|
||||
propName === "_codeMeta"
|
||||
|
||||
export const createStateManager = ({
|
||||
store,
|
||||
coreApi,
|
||||
rootPath,
|
||||
frontendDefinition,
|
||||
componentLibraries,
|
||||
uiFunctions,
|
||||
onScreenSlotRendered,
|
||||
routeTo,
|
||||
}) => {
|
||||
let handlerTypes = eventHandlers(store, coreApi, rootPath, routeTo)
|
||||
let currentState
|
||||
|
||||
// any nodes that have props that are bound to the store
|
||||
let nodesBoundByProps = []
|
||||
|
||||
// any node whose children depend on code, that uses the store
|
||||
let nodesWithCodeBoundChildren = []
|
||||
|
||||
const getCurrentState = () => currentState
|
||||
const registerBindings = _registerBindings(
|
||||
nodesBoundByProps,
|
||||
nodesWithCodeBoundChildren
|
||||
)
|
||||
const bb = bbFactory({
|
||||
store,
|
||||
getCurrentState,
|
||||
frontendDefinition,
|
||||
componentLibraries,
|
||||
uiFunctions,
|
||||
onScreenSlotRendered,
|
||||
})
|
||||
|
||||
const setup = _setup(handlerTypes, getCurrentState, registerBindings, bb)
|
||||
|
||||
const unsubscribe = store.subscribe(
|
||||
onStoreStateUpdated({
|
||||
setCurrentState: s => (currentState = s),
|
||||
getCurrentState,
|
||||
nodesWithCodeBoundChildren,
|
||||
nodesBoundByProps,
|
||||
uiFunctions,
|
||||
componentLibraries,
|
||||
onScreenSlotRendered,
|
||||
setupState: setup,
|
||||
})
|
||||
)
|
||||
|
||||
return {
|
||||
setup,
|
||||
destroy: () => unsubscribe(),
|
||||
getCurrentState,
|
||||
store,
|
||||
}
|
||||
}
|
||||
|
||||
const onStoreStateUpdated = ({
|
||||
setCurrentState,
|
||||
getCurrentState,
|
||||
nodesWithCodeBoundChildren,
|
||||
nodesBoundByProps,
|
||||
uiFunctions,
|
||||
componentLibraries,
|
||||
onScreenSlotRendered,
|
||||
setupState,
|
||||
}) => s => {
|
||||
setCurrentState(s)
|
||||
|
||||
// the original array gets changed by components' destroy()
|
||||
// so we make a clone and check if they are still in the original
|
||||
const nodesWithBoundChildren_clone = [...nodesWithCodeBoundChildren]
|
||||
for (let node of nodesWithBoundChildren_clone) {
|
||||
if (!nodesWithCodeBoundChildren.includes(node)) continue
|
||||
attachChildren({
|
||||
uiFunctions,
|
||||
componentLibraries,
|
||||
treeNode: node,
|
||||
onScreenSlotRendered,
|
||||
setupState,
|
||||
getCurrentState,
|
||||
})(node.rootElement, { hydrate: true, force: true })
|
||||
}
|
||||
|
||||
for (let node of nodesBoundByProps) {
|
||||
setNodeState(s, node)
|
||||
}
|
||||
}
|
||||
|
||||
const _registerBindings = (nodesBoundByProps, nodesWithCodeBoundChildren) => (
|
||||
node,
|
||||
bindings
|
||||
) => {
|
||||
if (bindings.length > 0) {
|
||||
node.bindings = bindings
|
||||
nodesBoundByProps.push(node)
|
||||
const onDestroy = () => {
|
||||
nodesBoundByProps = nodesBoundByProps.filter(n => n === node)
|
||||
node.onDestroy = node.onDestroy.filter(d => d === onDestroy)
|
||||
}
|
||||
node.onDestroy.push(onDestroy)
|
||||
}
|
||||
if (
|
||||
node.props._children &&
|
||||
node.props._children.filter(c => c._codeMeta && c._codeMeta.dependsOnStore)
|
||||
.length > 0
|
||||
) {
|
||||
nodesWithCodeBoundChildren.push(node)
|
||||
const onDestroy = () => {
|
||||
nodesWithCodeBoundChildren = nodesWithCodeBoundChildren.filter(
|
||||
n => n === node
|
||||
)
|
||||
node.onDestroy = node.onDestroy.filter(d => d === onDestroy)
|
||||
}
|
||||
node.onDestroy.push(onDestroy)
|
||||
}
|
||||
}
|
||||
|
||||
const setNodeState = (storeState, node) => {
|
||||
if (!node.component) return
|
||||
const newProps = { ...node.bindings.initialProps }
|
||||
|
||||
for (let binding of node.bindings) {
|
||||
const val = getState(storeState, binding.path, binding.fallback)
|
||||
|
||||
if (val === undefined && newProps[binding.propName] !== undefined) {
|
||||
delete newProps[binding.propName]
|
||||
}
|
||||
|
||||
if (val !== undefined) {
|
||||
newProps[binding.propName] = val
|
||||
}
|
||||
}
|
||||
|
||||
node.component.$set(newProps)
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind a components event handler parameters to state, context or the event itself.
|
||||
* @param {Array} eventHandlerProp - event handler array from component definition
|
||||
*/
|
||||
function bindComponentEventHandlers(
|
||||
eventHandlerProp,
|
||||
context,
|
||||
getCurrentState
|
||||
) {
|
||||
const boundEventHandlers = []
|
||||
for (let event of eventHandlerProp) {
|
||||
const boundEventHandler = {
|
||||
handlerType: event[EVENT_TYPE_MEMBER_NAME],
|
||||
parameters: event.parameters,
|
||||
}
|
||||
|
||||
const boundParameters = {}
|
||||
for (let paramName in boundEventHandler.parameters) {
|
||||
const paramValue = boundEventHandler.parameters[paramName]
|
||||
const paramBinding = parseBinding(paramValue)
|
||||
if (!paramBinding) {
|
||||
boundParameters[paramName] = () => paramValue
|
||||
continue
|
||||
}
|
||||
|
||||
let paramValueSource
|
||||
|
||||
if (paramBinding.source === "context") paramValueSource = context
|
||||
if (paramBinding.source === "state") paramValueSource = getCurrentState()
|
||||
|
||||
// The new dynamic event parameter bound to the relevant source
|
||||
boundParameters[paramName] = eventContext =>
|
||||
getState(
|
||||
paramBinding.source === "event" ? eventContext : paramValueSource,
|
||||
paramBinding.path,
|
||||
paramBinding.fallback
|
||||
)
|
||||
}
|
||||
|
||||
boundEventHandler.parameters = boundParameters
|
||||
boundEventHandlers.push(boundEventHandlers)
|
||||
|
||||
return boundEventHandlers
|
||||
}
|
||||
}
|
||||
|
||||
const _setup = (
|
||||
handlerTypes,
|
||||
getCurrentState,
|
||||
registerBindings,
|
||||
bb
|
||||
) => node => {
|
||||
const props = node.props
|
||||
const context = node.context || {}
|
||||
const initialProps = { ...props }
|
||||
const storeBoundProps = []
|
||||
const currentStoreState = getCurrentState()
|
||||
|
||||
for (let propName in props) {
|
||||
if (isMetaProp(propName)) continue
|
||||
|
||||
const propValue = props[propName]
|
||||
|
||||
const binding = parseBinding(propValue)
|
||||
const isBound = !!binding
|
||||
|
||||
if (isBound) binding.propName = propName
|
||||
|
||||
if (isBound && binding.source === "state") {
|
||||
storeBoundProps.push(binding)
|
||||
|
||||
initialProps[propName] = !currentStoreState
|
||||
? binding.fallback
|
||||
: getState(
|
||||
currentStoreState,
|
||||
binding.path,
|
||||
binding.fallback,
|
||||
binding.source
|
||||
)
|
||||
}
|
||||
|
||||
if (isBound && binding.source === "context") {
|
||||
initialProps[propName] = !context
|
||||
? propValue
|
||||
: getState(context, binding.path, binding.fallback, binding.source)
|
||||
}
|
||||
|
||||
if (isEventType(propValue)) {
|
||||
const boundEventHandlers = bindComponentEventHandlers(
|
||||
propValue,
|
||||
context,
|
||||
getCurrentState
|
||||
)
|
||||
|
||||
if (boundEventHandlers.length === 0) {
|
||||
initialProps[propName] = doNothing
|
||||
} else {
|
||||
initialProps[propName] = async context => {
|
||||
for (let handlerInfo of boundEventHandlers) {
|
||||
const handler = makeHandler(handlerTypes, handlerInfo)
|
||||
await handler(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registerBindings(node, storeBoundProps)
|
||||
|
||||
const setup = _setup(handlerTypes, getCurrentState, registerBindings, bb)
|
||||
initialProps._bb = bb(node, setup)
|
||||
|
||||
return initialProps
|
||||
}
|
||||
|
||||
const makeHandler = (handlerTypes, handlerInfo) => {
|
||||
const handlerType = handlerTypes[handlerInfo.handlerType]
|
||||
return context => {
|
||||
const parameters = {}
|
||||
for (let paramName in handlerInfo.parameters) {
|
||||
parameters[paramName] = handlerInfo.parameters[paramName](context)
|
||||
}
|
||||
handlerType.execute(parameters)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import { writable } from "svelte/store"
|
||||
|
||||
const appStore = writable({})
|
||||
appStore.actions = {}
|
||||
|
||||
const routerStore = writable({})
|
||||
routerStore.actions = {}
|
||||
|
||||
export { appStore, routerStore }
|
|
@ -1,363 +0,0 @@
|
|||
import { load, makePage, makeScreen } from "./testAppDef"
|
||||
import { EVENT_TYPE_MEMBER_NAME } from "../src/state/eventHandlers"
|
||||
|
||||
describe("initialiseApp (binding)", () => {
|
||||
it("should populate root element prop from store value", async () => {
|
||||
const { dom } = await load(
|
||||
makePage({
|
||||
_component: "testlib/div",
|
||||
className: {
|
||||
"##bbstate": "divClassName",
|
||||
"##bbsource": "state",
|
||||
"##bbstatefallback": "default",
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
const rootDiv = dom.window.document.body.children[0]
|
||||
expect(rootDiv.className.includes("default")).toBe(true)
|
||||
})
|
||||
|
||||
it("should update root element from store", async () => {
|
||||
const { dom, app } = await load(
|
||||
makePage({
|
||||
_component: "testlib/div",
|
||||
className: {
|
||||
"##bbstate": "divClassName",
|
||||
"##bbsource": "state",
|
||||
"##bbstatefallback": "default",
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
app.pageStore().update(s => {
|
||||
s.divClassName = "newvalue"
|
||||
return s
|
||||
})
|
||||
|
||||
const rootDiv = dom.window.document.body.children[0]
|
||||
expect(rootDiv.className.includes("newvalue")).toBe(true)
|
||||
})
|
||||
|
||||
it("should update root element from store, using binding expression", async () => {
|
||||
const { dom, app } = await load(
|
||||
makePage({
|
||||
_component: "testlib/div",
|
||||
className: "state.divClassName",
|
||||
})
|
||||
)
|
||||
|
||||
app.pageStore().update(s => {
|
||||
s.divClassName = "newvalue"
|
||||
return s
|
||||
})
|
||||
|
||||
const rootDiv = dom.window.document.body.children[0]
|
||||
expect(rootDiv.className.includes("newvalue")).toBe(true)
|
||||
})
|
||||
|
||||
it("should populate child component with store value", async () => {
|
||||
const { dom } = await load(
|
||||
makePage({
|
||||
_component: "testlib/div",
|
||||
_children: [
|
||||
{
|
||||
_component: "testlib/h1",
|
||||
text: {
|
||||
"##bbstate": "headerOneText",
|
||||
"##bbsource": "state",
|
||||
"##bbstatefallback": "header one",
|
||||
},
|
||||
},
|
||||
{
|
||||
_component: "testlib/h1",
|
||||
text: {
|
||||
"##bbstate": "headerTwoText",
|
||||
"##bbsource": "state",
|
||||
"##bbstatefallback": "header two",
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
|
||||
const rootDiv = dom.window.document.body.children[0]
|
||||
|
||||
expect(rootDiv.children.length).toBe(2)
|
||||
expect(rootDiv.children[0].tagName).toBe("H1")
|
||||
expect(rootDiv.children[0].innerText).toBe("header one")
|
||||
expect(rootDiv.children[1].tagName).toBe("H1")
|
||||
expect(rootDiv.children[1].innerText).toBe("header two")
|
||||
})
|
||||
|
||||
it("should populate child component with store value", async () => {
|
||||
const { dom, app } = await load(
|
||||
makePage({
|
||||
_component: "testlib/div",
|
||||
_children: [
|
||||
{
|
||||
_component: "testlib/h1",
|
||||
text: {
|
||||
"##bbstate": "headerOneText",
|
||||
"##bbsource": "state",
|
||||
"##bbstatefallback": "header one",
|
||||
},
|
||||
},
|
||||
{
|
||||
_component: "testlib/h1",
|
||||
text: {
|
||||
"##bbstate": "headerTwoText",
|
||||
"##bbsource": "state",
|
||||
"##bbstatefallback": "header two",
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
|
||||
app.pageStore().update(s => {
|
||||
s.headerOneText = "header 1 - new val"
|
||||
s.headerTwoText = "header 2 - new val"
|
||||
return s
|
||||
})
|
||||
|
||||
const rootDiv = dom.window.document.body.children[0]
|
||||
|
||||
expect(rootDiv.children.length).toBe(2)
|
||||
expect(rootDiv.children[0].tagName).toBe("H1")
|
||||
expect(rootDiv.children[0].innerText).toBe("header 1 - new val")
|
||||
expect(rootDiv.children[1].tagName).toBe("H1")
|
||||
expect(rootDiv.children[1].innerText).toBe("header 2 - new val")
|
||||
})
|
||||
|
||||
it("should populate screen child with store value", async () => {
|
||||
const { dom, app } = await load(
|
||||
makePage({
|
||||
_component: "testlib/div",
|
||||
_children: [
|
||||
{
|
||||
_component: "##builtin/screenslot",
|
||||
text: "header one",
|
||||
},
|
||||
],
|
||||
}),
|
||||
[
|
||||
makeScreen("/", {
|
||||
_component: "testlib/div",
|
||||
className: "screen-class",
|
||||
_children: [
|
||||
{
|
||||
_component: "testlib/h1",
|
||||
text: {
|
||||
"##bbstate": "headerOneText",
|
||||
"##bbsource": "state",
|
||||
"##bbstatefallback": "header one",
|
||||
},
|
||||
},
|
||||
{
|
||||
_component: "testlib/h1",
|
||||
text: {
|
||||
"##bbstate": "headerTwoText",
|
||||
"##bbsource": "state",
|
||||
"##bbstatefallback": "header two",
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
]
|
||||
)
|
||||
|
||||
app.screenStore().update(s => {
|
||||
s.headerOneText = "header 1 - new val"
|
||||
s.headerTwoText = "header 2 - new val"
|
||||
return s
|
||||
})
|
||||
|
||||
const rootDiv = dom.window.document.body.children[0]
|
||||
expect(rootDiv.children.length).toBe(1)
|
||||
|
||||
const screenRoot = rootDiv.children[0]
|
||||
|
||||
expect(screenRoot.children.length).toBe(1)
|
||||
expect(screenRoot.children[0].children.length).toBe(2)
|
||||
expect(screenRoot.children[0].children[0].innerText).toBe(
|
||||
"header 1 - new val"
|
||||
)
|
||||
expect(screenRoot.children[0].children[1].innerText).toBe(
|
||||
"header 2 - new val"
|
||||
)
|
||||
})
|
||||
|
||||
it("should fire events", async () => {
|
||||
const { dom, app } = await load(
|
||||
makePage({
|
||||
_component: "testlib/button",
|
||||
onClick: [
|
||||
event("Set State", {
|
||||
path: "address",
|
||||
value: "123 Main Street",
|
||||
}),
|
||||
],
|
||||
})
|
||||
)
|
||||
|
||||
const button = dom.window.document.body.children[0]
|
||||
expect(button.tagName).toBe("BUTTON")
|
||||
|
||||
let storeAddress
|
||||
app.pageStore().subscribe(s => {
|
||||
storeAddress = s.address
|
||||
})
|
||||
button.dispatchEvent(new dom.window.Event("click"))
|
||||
expect(storeAddress).toBe("123 Main Street")
|
||||
})
|
||||
|
||||
it("should alter event parameters based on store values", async () => {
|
||||
const { dom, app } = await load(
|
||||
makePage({
|
||||
_component: "testlib/button",
|
||||
onClick: [
|
||||
event("Set State", {
|
||||
path: "address",
|
||||
value: {
|
||||
"##bbstate": "sourceaddress",
|
||||
"##bbsource": "state",
|
||||
"##bbstatefallback": "fallback address",
|
||||
},
|
||||
}),
|
||||
],
|
||||
})
|
||||
)
|
||||
|
||||
const button = dom.window.document.body.children[0]
|
||||
expect(button.tagName).toBe("BUTTON")
|
||||
|
||||
let storeAddress
|
||||
app.pageStore().subscribe(s => {
|
||||
storeAddress = s.address
|
||||
})
|
||||
|
||||
button.dispatchEvent(new dom.window.Event("click"))
|
||||
expect(storeAddress).toBe("fallback address")
|
||||
|
||||
app.pageStore().update(s => {
|
||||
s.sourceaddress = "new address"
|
||||
return s
|
||||
})
|
||||
|
||||
button.dispatchEvent(new dom.window.Event("click"))
|
||||
expect(storeAddress).toBe("new address")
|
||||
})
|
||||
|
||||
it("should take event parameters from context values", async () => {
|
||||
const { dom, app } = await load(
|
||||
makePage({
|
||||
_component: "testlib/button",
|
||||
_id: "with_context",
|
||||
onClick: [
|
||||
event("Set State", {
|
||||
path: "address",
|
||||
value: {
|
||||
"##bbstate": "testKey",
|
||||
"##bbsource": "context",
|
||||
"##bbstatefallback": "fallback address",
|
||||
},
|
||||
}),
|
||||
],
|
||||
})
|
||||
)
|
||||
|
||||
const button = dom.window.document.body.children[0]
|
||||
expect(button.tagName).toBe("BUTTON")
|
||||
|
||||
let storeAddress
|
||||
app.pageStore().subscribe(s => {
|
||||
storeAddress = s.address
|
||||
})
|
||||
|
||||
button.dispatchEvent(new dom.window.Event("click"))
|
||||
expect(storeAddress).toBe("test value")
|
||||
})
|
||||
})
|
||||
|
||||
it("should rerender components when their code is bound to the store ", async () => {
|
||||
const { dom, app } = await load(
|
||||
makePage({
|
||||
_component: "testlib/div",
|
||||
_children: [
|
||||
{
|
||||
_component: "testlib/div",
|
||||
_id: "n_clones_based_on_store",
|
||||
className: "child_div",
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
|
||||
const rootDiv = dom.window.document.body.children[0]
|
||||
expect(rootDiv.tagName).toBe("DIV")
|
||||
expect(rootDiv.children.length).toBe(0)
|
||||
|
||||
app.pageStore().update(s => {
|
||||
s.componentCount = 3
|
||||
return s
|
||||
})
|
||||
|
||||
expect(rootDiv.children.length).toBe(3)
|
||||
expect(rootDiv.children[0].className.includes("child_div")).toBe(true)
|
||||
|
||||
app.pageStore().update(s => {
|
||||
s.componentCount = 5
|
||||
return s
|
||||
})
|
||||
|
||||
expect(rootDiv.children.length).toBe(5)
|
||||
expect(rootDiv.children[0].className.includes("child_div")).toBe(true)
|
||||
|
||||
app.pageStore().update(s => {
|
||||
s.componentCount = 0
|
||||
return s
|
||||
})
|
||||
|
||||
expect(rootDiv.children.length).toBe(0)
|
||||
})
|
||||
|
||||
it("should be able to read value from context, passed fromm parent, through code", async () => {
|
||||
const { dom, app } = await load(
|
||||
makePage({
|
||||
_component: "testlib/div",
|
||||
_children: [
|
||||
{
|
||||
_component: "testlib/div",
|
||||
_id: "n_clones_based_on_store",
|
||||
className: {
|
||||
"##bbstate": "index",
|
||||
"##bbsource": "context",
|
||||
"##bbstatefallback": "nothing",
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
|
||||
const rootDiv = dom.window.document.body.children[0]
|
||||
expect(rootDiv.tagName).toBe("DIV")
|
||||
expect(rootDiv.children.length).toBe(0)
|
||||
|
||||
app.pageStore().update(s => {
|
||||
s.componentCount = 3
|
||||
return s
|
||||
})
|
||||
|
||||
expect(rootDiv.children.length).toBe(3)
|
||||
expect(rootDiv.children[0].className.includes("index_0")).toBe(true)
|
||||
expect(rootDiv.children[1].className.includes("index_1")).toBe(true)
|
||||
expect(rootDiv.children[2].className.includes("index_2")).toBe(true)
|
||||
})
|
||||
|
||||
const event = (handlerType, parameters) => {
|
||||
const e = {}
|
||||
e[EVENT_TYPE_MEMBER_NAME] = handlerType
|
||||
e.parameters = parameters
|
||||
return e
|
||||
}
|
|
@ -1,74 +0,0 @@
|
|||
import { load, makePage } from "./testAppDef"
|
||||
|
||||
describe("controlFlow", () => {
|
||||
it("should display simple div, with always true render function", async () => {
|
||||
const { dom } = await load(
|
||||
makePage({
|
||||
_component: "testlib/div",
|
||||
className: "my-test-class",
|
||||
_id: "always_render",
|
||||
})
|
||||
)
|
||||
|
||||
expect(dom.window.document.body.children.length).toBe(1)
|
||||
const child = dom.window.document.body.children[0]
|
||||
expect(child.className.includes("my-test-class")).toBeTruthy()
|
||||
})
|
||||
|
||||
it("should not display div, with always false render function", async () => {
|
||||
const { dom } = await load(
|
||||
makePage({
|
||||
_component: "testlib/div",
|
||||
className: "my-test-class",
|
||||
_id: "never_render",
|
||||
})
|
||||
)
|
||||
|
||||
expect(dom.window.document.body.children.length).toBe(0)
|
||||
})
|
||||
|
||||
it("should display 3 divs in a looped render function", async () => {
|
||||
const { dom } = await load(
|
||||
makePage({
|
||||
_component: "testlib/div",
|
||||
className: "my-test-class",
|
||||
_id: "three_clones",
|
||||
})
|
||||
)
|
||||
|
||||
expect(dom.window.document.body.children.length).toBe(3)
|
||||
|
||||
const child0 = dom.window.document.body.children[0]
|
||||
expect(child0.className.includes("my-test-class")).toBeTruthy()
|
||||
|
||||
const child1 = dom.window.document.body.children[1]
|
||||
expect(child1.className.includes("my-test-class")).toBeTruthy()
|
||||
|
||||
const child2 = dom.window.document.body.children[2]
|
||||
expect(child2.className.includes("my-test-class")).toBeTruthy()
|
||||
})
|
||||
|
||||
it("should display 3 div, in a looped render, as children", async () => {
|
||||
const { dom } = await load(
|
||||
makePage({
|
||||
_component: "testlib/div",
|
||||
_children: [
|
||||
{
|
||||
_component: "testlib/div",
|
||||
className: "my-test-class",
|
||||
_id: "three_clones",
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
|
||||
expect(dom.window.document.body.children.length).toBe(1)
|
||||
|
||||
const rootDiv = dom.window.document.body.children[0]
|
||||
expect(rootDiv.children.length).toBe(3)
|
||||
|
||||
expect(rootDiv.children[0].className.includes("my-test-class")).toBeTruthy()
|
||||
expect(rootDiv.children[1].className.includes("my-test-class")).toBeTruthy()
|
||||
expect(rootDiv.children[2].className.includes("my-test-class")).toBeTruthy()
|
||||
})
|
||||
})
|
|
@ -13,7 +13,7 @@ export const load = async (page, screens, url, appRootPath) => {
|
|||
autoAssignIds(s.props)
|
||||
}
|
||||
setAppDef(dom.window, page, screens)
|
||||
addWindowGlobals(dom.window, page, screens, appRootPath, uiFunctions, {
|
||||
addWindowGlobals(dom.window, page, screens, appRootPath, {
|
||||
hierarchy: {},
|
||||
actions: [],
|
||||
triggers: [],
|
||||
|
@ -27,13 +27,12 @@ export const load = async (page, screens, url, appRootPath) => {
|
|||
return { dom, app }
|
||||
}
|
||||
|
||||
const addWindowGlobals = (window, page, screens, appRootPath, uiFunctions) => {
|
||||
const addWindowGlobals = (window, page, screens, appRootPath) => {
|
||||
window["##BUDIBASE_FRONTEND_DEFINITION##"] = {
|
||||
page,
|
||||
screens,
|
||||
appRootPath,
|
||||
}
|
||||
window["##BUDIBASE_FRONTEND_FUNCTIONS##"] = uiFunctions
|
||||
}
|
||||
|
||||
export const makePage = props => ({ props })
|
||||
|
@ -184,28 +183,3 @@ const maketestlib = window => ({
|
|||
opts.target.appendChild(node)
|
||||
},
|
||||
})
|
||||
|
||||
const uiFunctions = {
|
||||
never_render: () => {},
|
||||
|
||||
always_render: render => {
|
||||
render()
|
||||
},
|
||||
|
||||
three_clones: render => {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
render()
|
||||
}
|
||||
},
|
||||
|
||||
with_context: render => {
|
||||
render({ testKey: "test value" })
|
||||
},
|
||||
|
||||
n_clones_based_on_store: (render, _, state) => {
|
||||
const n = state.componentCount || 0
|
||||
for (let i = 0; i < n; i++) {
|
||||
render({ index: `index_${i}` })
|
||||
}
|
||||
},
|
||||
}
|
||||
|
|
|
@ -1,80 +0,0 @@
|
|||
export default ({ indexes, helpers }) =>
|
||||
indexes.map(i => ({
|
||||
name: `Table based on view: ${i.name} `,
|
||||
props: tableProps(
|
||||
i,
|
||||
helpers.indexSchema(i).filter(c => !excludedColumns.includes(c.name))
|
||||
),
|
||||
}))
|
||||
|
||||
const excludedColumns = ["id", "key", "sortKey", "type", "isNew"]
|
||||
|
||||
const tableProps = (index, indexSchema) => ({
|
||||
_component: "@budibase/materialdesign-components/Datatable",
|
||||
_children: [
|
||||
{
|
||||
_component: "@budibase/materialdesign-components/DatatableHead",
|
||||
_children: [
|
||||
{
|
||||
_component: "@budibase/materialdesign-components/DatatableRow",
|
||||
isHeader: true,
|
||||
_children: columnHeaders(indexSchema),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
_component: "@budibase/materialdesign-components/DatatableBody",
|
||||
_children: [
|
||||
{
|
||||
_code: rowCode(index),
|
||||
_component: "@budibase/materialdesign-components/DatatableRow",
|
||||
_children: dataCells(index, indexSchema),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
onLoad: [
|
||||
{
|
||||
"##eventHandlerType": "List Records",
|
||||
parameters: {
|
||||
indexKey: index.nodeKey(),
|
||||
statePath: index.name,
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const columnHeaders = indexSchema =>
|
||||
indexSchema.map(col => ({
|
||||
_component: "@budibase/materialdesign-components/DatatableCell",
|
||||
isHeader: true,
|
||||
_children: [
|
||||
{
|
||||
_component: "@budibase/standard-components/text",
|
||||
type: "none",
|
||||
text: col.name,
|
||||
formattingTag: "<b> - bold",
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
const dataCells = (index, indexSchema) =>
|
||||
indexSchema.map(col => ({
|
||||
_component: "@budibase/materialdesign-components/DatatableCell",
|
||||
_children: [
|
||||
{
|
||||
_component: "@budibase/standard-components/text",
|
||||
type: "none",
|
||||
text: `context.${dataItem(index)}.${col.name}`,
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
const dataItem = index => `${index.name}_item`
|
||||
const dataCollection = index => `state.${index.name}`
|
||||
const rowCode = index =>
|
||||
`
|
||||
if (!${dataCollection(index)}) return
|
||||
|
||||
for (let ${dataItem(index)} of ${dataCollection(index)})
|
||||
render( { ${dataItem(index)} } )`
|
|
@ -1,149 +0,0 @@
|
|||
export default ({ records }) =>
|
||||
records.map(r => ({
|
||||
name: `Form for Record: ${r.nodeName()}`,
|
||||
props: outerContainer(r),
|
||||
}))
|
||||
|
||||
const outerContainer = record => ({
|
||||
_component: "@budibase/standard-components/container",
|
||||
_code: "",
|
||||
type: "div",
|
||||
onLoad: [
|
||||
{
|
||||
"##eventHandlerType": "Get New Record",
|
||||
parameters: {
|
||||
collectionKey: record.collectionNodeKey(),
|
||||
childRecordType: record.name,
|
||||
statePath: record.name,
|
||||
},
|
||||
},
|
||||
],
|
||||
_children: [
|
||||
heading(record),
|
||||
...record.fields.map(f => field(record, f)),
|
||||
buttons(record),
|
||||
],
|
||||
})
|
||||
|
||||
const heading = record => ({
|
||||
_component: "@budibase/materialdesign-components/H3",
|
||||
text: capitalize(record.name),
|
||||
})
|
||||
|
||||
const field = (record, f) => {
|
||||
if (f.type === "bool") return checkbox(record, f)
|
||||
if (
|
||||
f.type === "string" &&
|
||||
f.typeOptions &&
|
||||
f.typeOptions.values &&
|
||||
f.typeOptions.values.length > 0
|
||||
)
|
||||
return select(record, f)
|
||||
return textField(record, f)
|
||||
}
|
||||
|
||||
const textField = (record, f) => ({
|
||||
_component: "@budibase/materialdesign-components/Textfield",
|
||||
label: f.label,
|
||||
variant: "filled",
|
||||
disabled: false,
|
||||
fullwidth: false,
|
||||
colour: "primary",
|
||||
maxLength:
|
||||
f.typeOptions && f.typeOptions.maxLength ? f.typeOptions.maxLength : 0,
|
||||
placeholder: f.label,
|
||||
value: fieldValueBinding(record, f),
|
||||
})
|
||||
|
||||
const checkbox = (record, f) => ({
|
||||
_component: "@budibase/materialdesign-components/Checkbox",
|
||||
label: f.label,
|
||||
checked: fieldValueBinding(record, f),
|
||||
})
|
||||
|
||||
const select = (record, f) => ({
|
||||
_component: "@budibase/materialdesign-components/Select",
|
||||
value: fieldValueBinding(record, f),
|
||||
_children: f.typeOptions.values.map(val => ({
|
||||
_component: "@budibase/materialdesign-components/ListItem",
|
||||
value: val,
|
||||
text: val,
|
||||
})),
|
||||
})
|
||||
|
||||
const fieldValueBinding = (record, f) => `state.${record.name}.${f.name}`
|
||||
|
||||
const capitalize = s => s.charAt(0).toUpperCase() + s.slice(1)
|
||||
|
||||
const buttons = record => ({
|
||||
_component: "@budibase/standard-components/container",
|
||||
borderWidth: "1px 0px 0px 0px",
|
||||
borderColor: "lightgray",
|
||||
borderStyle: "solid",
|
||||
_styles: {
|
||||
position: {
|
||||
column: ["", ""],
|
||||
row: ["", ""],
|
||||
margin: ["", "", "", ""],
|
||||
padding: ["30px", "", "", ""],
|
||||
height: [""],
|
||||
width: [""],
|
||||
zindex: [""],
|
||||
},
|
||||
layout: {
|
||||
templaterows: [""],
|
||||
templatecolumns: [""],
|
||||
},
|
||||
},
|
||||
_children: [
|
||||
{
|
||||
_component: "@budibase/materialdesign-components/Button",
|
||||
onClick: [
|
||||
{
|
||||
"##eventHandlerType": "Save Record",
|
||||
parameters: {
|
||||
statePath: `${record.name}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
"##eventHandlerType": "Navigate To",
|
||||
parameters: {
|
||||
url: `/${record.name}s`,
|
||||
},
|
||||
},
|
||||
],
|
||||
variant: "raised",
|
||||
colour: "primary",
|
||||
size: "medium",
|
||||
text: `Save ${capitalize(record.name)}`,
|
||||
},
|
||||
{
|
||||
_component: "@budibase/materialdesign-components/Button",
|
||||
_styles: {
|
||||
position: {
|
||||
row: ["", ""],
|
||||
column: ["", ""],
|
||||
padding: ["", "", "", ""],
|
||||
margin: ["", "", "", "10px"],
|
||||
width: [""],
|
||||
height: [""],
|
||||
zindex: [""],
|
||||
},
|
||||
layout: {
|
||||
templatecolumns: [""],
|
||||
templaterows: [""],
|
||||
},
|
||||
},
|
||||
onClick: [
|
||||
{
|
||||
"##eventHandlerType": "Navigate To",
|
||||
parameters: {
|
||||
url: `/${record.name}s`,
|
||||
},
|
||||
},
|
||||
],
|
||||
colour: "secondary",
|
||||
text: "Cancel",
|
||||
},
|
||||
],
|
||||
})
|
|
@ -15,8 +15,6 @@ export {
|
|||
DatatableCell,
|
||||
DatatableRow,
|
||||
} from "./Datatable"
|
||||
export { default as indexDatatable } from "./Templates/indexDatatable"
|
||||
export { default as recordForm } from "./Templates/recordForm"
|
||||
export { List, ListItem } from "./List"
|
||||
export { Menu } from "./Menu"
|
||||
export { Select } from "./Select"
|
||||
|
|
|
@ -44,6 +44,7 @@
|
|||
"@budibase/client": "^0.0.32",
|
||||
"@budibase/core": "^0.0.32",
|
||||
"@koa/router": "^8.0.0",
|
||||
"@sendgrid/mail": "^7.1.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"dotenv": "^8.2.0",
|
||||
"electron-is-dev": "^1.2.0",
|
||||
|
@ -59,6 +60,7 @@
|
|||
"koa-session": "^5.12.0",
|
||||
"koa-static": "^5.0.0",
|
||||
"lodash": "^4.17.13",
|
||||
"mustache": "^4.0.1",
|
||||
"pino-pretty": "^4.0.0",
|
||||
"pouchdb": "^7.2.1",
|
||||
"pouchdb-all-dbs": "^1.0.2",
|
||||
|
|
|
@ -24,14 +24,15 @@ exports.authenticate = async ctx => {
|
|||
|
||||
// Check the user exists in the instance DB by username
|
||||
const instanceDb = new CouchDB(instanceId)
|
||||
const { rows } = await instanceDb.query("database/by_username", {
|
||||
include_docs: true,
|
||||
username,
|
||||
})
|
||||
|
||||
if (rows.length === 0) ctx.throw(500, `User does not exist.`)
|
||||
|
||||
const dbUser = rows[0].doc
|
||||
let dbUser
|
||||
try {
|
||||
dbUser = await instanceDb.get(`user_${username}`)
|
||||
} catch (_) {
|
||||
// do not want to throw a 404 - as this could be
|
||||
// used to dtermine valid usernames
|
||||
ctx.throw(401, "Invalid Credentials")
|
||||
}
|
||||
|
||||
// authenticate
|
||||
if (await bcrypt.compare(password, dbUser.password)) {
|
||||
|
|
|
@ -29,6 +29,16 @@ exports.create = async function(ctx) {
|
|||
emit([doc.type], doc._id)
|
||||
}.toString(),
|
||||
},
|
||||
by_workflow_trigger: {
|
||||
map: function(doc) {
|
||||
if (doc.type === "workflow") {
|
||||
const trigger = doc.definition.trigger
|
||||
if (trigger) {
|
||||
emit([trigger.event], trigger)
|
||||
}
|
||||
}
|
||||
}.toString(),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
@ -42,6 +42,12 @@ exports.save = async function(ctx) {
|
|||
record.type = "record"
|
||||
const response = await db.post(record)
|
||||
record._rev = response.rev
|
||||
|
||||
ctx.eventEmitter &&
|
||||
ctx.eventEmitter.emit(`record:save`, {
|
||||
record,
|
||||
instanceId: ctx.params.instanceId,
|
||||
})
|
||||
ctx.body = record
|
||||
ctx.status = 200
|
||||
ctx.message = `${model.name} created successfully`
|
||||
|
@ -81,6 +87,7 @@ exports.destroy = async function(ctx) {
|
|||
return
|
||||
}
|
||||
ctx.body = await db.remove(ctx.params.recordId, ctx.params.revId)
|
||||
ctx.eventEmitter && ctx.eventEmitter.emit(`record:delete`, record)
|
||||
}
|
||||
|
||||
exports.validate = async function(ctx) {
|
||||
|
|
|
@ -11,7 +11,8 @@ const controller = {
|
|||
if (
|
||||
!name.startsWith("all") &&
|
||||
name !== "by_type" &&
|
||||
name !== "by_username"
|
||||
name !== "by_username" &&
|
||||
name !== "by_workflow_trigger"
|
||||
) {
|
||||
response.push({
|
||||
name,
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
const recordController = require("../../record")
|
||||
|
||||
module.exports = async function saveRecord({ args, instanceId }) {
|
||||
const { model, ...record } = args.record
|
||||
|
||||
const ctx = {
|
||||
params: {
|
||||
instanceId,
|
||||
modelId: model._id,
|
||||
},
|
||||
request: {
|
||||
body: record,
|
||||
},
|
||||
}
|
||||
|
||||
await recordController.save(ctx)
|
||||
|
||||
try {
|
||||
return {
|
||||
record: ctx.body,
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return {
|
||||
record: null,
|
||||
error: err.message,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
const CouchDB = require("../../db")
|
||||
const newid = require("../../db/newid")
|
||||
const CouchDB = require("../../../db")
|
||||
const newid = require("../../../db/newid")
|
||||
|
||||
exports.create = async function(ctx) {
|
||||
const db = new CouchDB(ctx.params.instanceId)
|
||||
|
@ -7,24 +7,6 @@ exports.create = async function(ctx) {
|
|||
|
||||
workflow._id = newid()
|
||||
|
||||
// TODO: Possibly validate the workflow against a schema
|
||||
|
||||
// // validation with ajv
|
||||
// const model = await db.get(record.modelId)
|
||||
// const validate = ajv.compile({
|
||||
// properties: model.schema,
|
||||
// })
|
||||
// const valid = validate(record)
|
||||
|
||||
// if (!valid) {
|
||||
// ctx.status = 400
|
||||
// ctx.body = {
|
||||
// status: 400,
|
||||
// errors: validate.errors,
|
||||
// }
|
||||
// return
|
||||
// }
|
||||
|
||||
workflow.type = "workflow"
|
||||
const response = await db.post(workflow)
|
||||
workflow._rev = response.rev
|
||||
|
@ -41,23 +23,51 @@ exports.create = async function(ctx) {
|
|||
|
||||
exports.update = async function(ctx) {
|
||||
const db = new CouchDB(ctx.params.instanceId)
|
||||
ctx.body = await db.get(ctx.params.recordId)
|
||||
const workflow = ctx.request.body
|
||||
|
||||
const response = await db.put(workflow)
|
||||
workflow._rev = response.rev
|
||||
|
||||
ctx.status = 200
|
||||
ctx.body = {
|
||||
message: `Workflow ${workflow._id} updated successfully.`,
|
||||
workflow: {
|
||||
...workflow,
|
||||
_rev: response.rev,
|
||||
_id: response.id,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
exports.fetch = async function(ctx) {
|
||||
const db = new CouchDB(ctx.params.instanceId)
|
||||
const response = await db.query(`database/by_type`, {
|
||||
type: "workflow",
|
||||
key: ["workflow"],
|
||||
include_docs: true,
|
||||
})
|
||||
ctx.body = response.rows.map(row => row.doc)
|
||||
}
|
||||
|
||||
exports.find = async function(ctx) {
|
||||
const db = new CouchDB(ctx.params.instanceId)
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
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) {
|
||||
const db = new CouchDB(ctx.params.instanceId)
|
||||
ctx.body = await db.remove(ctx.params.id, ctx.params.rev)
|
|
@ -1,9 +1,11 @@
|
|||
const {
|
||||
createClientDatabase,
|
||||
createApplication,
|
||||
createInstance,
|
||||
destroyClientDatabase,
|
||||
builderEndpointShouldBlockNormalUsers,
|
||||
supertest,
|
||||
defaultHeaders
|
||||
defaultHeaders,
|
||||
} = require("./couchTestUtils")
|
||||
|
||||
describe("/applications", () => {
|
||||
|
@ -37,6 +39,18 @@ describe("/applications", () => {
|
|||
expect(res.res.statusMessage).toEqual("Application My App created successfully")
|
||||
expect(res.body._id).toBeDefined()
|
||||
})
|
||||
|
||||
it("should apply authorization to endpoint", async () => {
|
||||
const otherApplication = await createApplication(request)
|
||||
const instance = await createInstance(request, otherApplication._id)
|
||||
await builderEndpointShouldBlockNormalUsers({
|
||||
request,
|
||||
method: "POST",
|
||||
url: `/api/applications`,
|
||||
instanceId: instance._id,
|
||||
body: { name: "My App" }
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("fetch", () => {
|
||||
|
@ -53,6 +67,17 @@ describe("/applications", () => {
|
|||
|
||||
expect(res.body.length).toBe(2)
|
||||
})
|
||||
|
||||
it("should apply authorization to endpoint", async () => {
|
||||
const otherApplication = await createApplication(request)
|
||||
const instance = await createInstance(request, otherApplication._id)
|
||||
await builderEndpointShouldBlockNormalUsers({
|
||||
request,
|
||||
method: "GET",
|
||||
url: `/api/applications`,
|
||||
instanceId: instance._id,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
|
|
@ -2,7 +2,10 @@ const CouchDB = require("../../../db")
|
|||
const { create, destroy } = require("../../../db/clientDb")
|
||||
const supertest = require("supertest")
|
||||
const app = require("../../../app")
|
||||
const { POWERUSER_LEVEL_ID } = require("../../../utilities/accessLevels")
|
||||
const {
|
||||
POWERUSER_LEVEL_ID,
|
||||
generateAdminPermissions,
|
||||
} = require("../../../utilities/accessLevels")
|
||||
|
||||
const TEST_CLIENT_ID = "test-client-id"
|
||||
|
||||
|
@ -82,8 +85,8 @@ exports.createInstance = async (request, appId) => {
|
|||
exports.createUser = async (
|
||||
request,
|
||||
instanceId,
|
||||
username = "bill",
|
||||
password = "bills_password"
|
||||
username = "babs",
|
||||
password = "babs_password"
|
||||
) => {
|
||||
const res = await request
|
||||
.post(`/api/${instanceId}/users`)
|
||||
|
@ -97,6 +100,150 @@ exports.createUser = async (
|
|||
return res.body
|
||||
}
|
||||
|
||||
const createUserWithOnePermission = async (
|
||||
request,
|
||||
instanceId,
|
||||
permName,
|
||||
itemId
|
||||
) => {
|
||||
let permissions = await generateAdminPermissions(instanceId)
|
||||
permissions = permissions.filter(
|
||||
p => p.name === permName && p.itemId === itemId
|
||||
)
|
||||
|
||||
return await createUserWithPermissions(
|
||||
request,
|
||||
instanceId,
|
||||
permissions,
|
||||
"onePermOnlyUser"
|
||||
)
|
||||
}
|
||||
|
||||
const createUserWithAdminPermissions = async (request, instanceId) => {
|
||||
let permissions = await generateAdminPermissions(instanceId)
|
||||
|
||||
return await createUserWithPermissions(
|
||||
request,
|
||||
instanceId,
|
||||
permissions,
|
||||
"adminUser"
|
||||
)
|
||||
}
|
||||
|
||||
const createUserWithAllPermissionExceptOne = async (
|
||||
request,
|
||||
instanceId,
|
||||
permName,
|
||||
itemId
|
||||
) => {
|
||||
let permissions = await generateAdminPermissions(instanceId)
|
||||
permissions = permissions.filter(
|
||||
p => !(p.name === permName && p.itemId === itemId)
|
||||
)
|
||||
|
||||
return await createUserWithPermissions(
|
||||
request,
|
||||
instanceId,
|
||||
permissions,
|
||||
"allPermsExceptOneUser"
|
||||
)
|
||||
}
|
||||
|
||||
const createUserWithPermissions = async (
|
||||
request,
|
||||
instanceId,
|
||||
permissions,
|
||||
username
|
||||
) => {
|
||||
const accessRes = await request
|
||||
.post(`/api/${instanceId}/accesslevels`)
|
||||
.send({ name: "TestLevel", permissions })
|
||||
.set(exports.defaultHeaders)
|
||||
|
||||
const password = `password_${username}`
|
||||
await request
|
||||
.post(`/api/${instanceId}/users`)
|
||||
.set(exports.defaultHeaders)
|
||||
.send({
|
||||
name: username,
|
||||
username,
|
||||
password,
|
||||
accessLevelId: accessRes.body._id,
|
||||
})
|
||||
|
||||
const db = new CouchDB(instanceId)
|
||||
const designDoc = await db.get("_design/database")
|
||||
|
||||
const loginResult = await request
|
||||
.post(`/api/authenticate`)
|
||||
.set("Referer", `http://localhost:4001/${designDoc.metadata.applicationId}`)
|
||||
.send({ username, password })
|
||||
|
||||
// returning necessary request headers
|
||||
return {
|
||||
Accept: "application/json",
|
||||
Cookie: loginResult.headers["set-cookie"],
|
||||
}
|
||||
}
|
||||
|
||||
exports.testPermissionsForEndpoint = async ({
|
||||
request,
|
||||
method,
|
||||
url,
|
||||
body,
|
||||
instanceId,
|
||||
permissionName,
|
||||
itemId,
|
||||
}) => {
|
||||
const headers = await createUserWithOnePermission(
|
||||
request,
|
||||
instanceId,
|
||||
permissionName,
|
||||
itemId
|
||||
)
|
||||
|
||||
await createRequest(request, method, url, body)
|
||||
.set(headers)
|
||||
.expect(200)
|
||||
|
||||
const noPermsHeaders = await createUserWithAllPermissionExceptOne(
|
||||
request,
|
||||
instanceId,
|
||||
permissionName,
|
||||
itemId
|
||||
)
|
||||
|
||||
await createRequest(request, method, url, body)
|
||||
.set(noPermsHeaders)
|
||||
.expect(403)
|
||||
}
|
||||
|
||||
exports.builderEndpointShouldBlockNormalUsers = async ({
|
||||
request,
|
||||
method,
|
||||
url,
|
||||
body,
|
||||
instanceId,
|
||||
}) => {
|
||||
const headers = await createUserWithAdminPermissions(request, instanceId)
|
||||
|
||||
await createRequest(request, method, url, body)
|
||||
.set(headers)
|
||||
.expect(403)
|
||||
}
|
||||
|
||||
const createRequest = (request, method, url, body) => {
|
||||
let req
|
||||
|
||||
if (method === "POST") req = request.post(url).send(body)
|
||||
else if (method === "GET") req = request.get(url)
|
||||
else if (method === "DELETE") req = request.delete(url)
|
||||
else if (method === "PATCH") req = request.patch(url).send(body)
|
||||
else if (method === "PUT") req = request.put(url).send(body)
|
||||
|
||||
return req
|
||||
}
|
||||
|
||||
exports.insertDocument = async (databaseId, document) => {
|
||||
const { id, ...documentFields } = document
|
||||
return await new CouchDB(databaseId).put({ _id: id, ...documentFields })
|
||||
|
|
|
@ -4,7 +4,8 @@ const {
|
|||
supertest,
|
||||
createClientDatabase,
|
||||
createApplication ,
|
||||
defaultHeaders
|
||||
defaultHeaders,
|
||||
builderEndpointShouldBlockNormalUsers
|
||||
} = require("./couchTestUtils")
|
||||
|
||||
describe("/models", () => {
|
||||
|
@ -48,6 +49,22 @@ describe("/models", () => {
|
|||
done();
|
||||
});
|
||||
})
|
||||
|
||||
it("should apply authorization to endpoint", async () => {
|
||||
await builderEndpointShouldBlockNormalUsers({
|
||||
request,
|
||||
method: "POST",
|
||||
url: `/api/${instance._id}/models`,
|
||||
instanceId: instance._id,
|
||||
body: {
|
||||
name: "TestModel",
|
||||
key: "name",
|
||||
schema: {
|
||||
name: { type: "string" }
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
describe("fetch", () => {
|
||||
|
@ -71,6 +88,16 @@ describe("/models", () => {
|
|||
done();
|
||||
});
|
||||
})
|
||||
|
||||
it("should apply authorization to endpoint", async () => {
|
||||
await builderEndpointShouldBlockNormalUsers({
|
||||
request,
|
||||
method: "GET",
|
||||
url: `/api/${instance._id}/models`,
|
||||
instanceId: instance._id,
|
||||
})
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
describe("destroy", () => {
|
||||
|
@ -92,5 +119,15 @@ describe("/models", () => {
|
|||
done();
|
||||
});
|
||||
})
|
||||
|
||||
it("should apply authorization to endpoint", async () => {
|
||||
await builderEndpointShouldBlockNormalUsers({
|
||||
request,
|
||||
method: "DELETE",
|
||||
url: `/api/${instance._id}/models/${testModel._id}/${testModel._rev}`,
|
||||
instanceId: instance._id,
|
||||
})
|
||||
})
|
||||
|
||||
});
|
||||
});
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue